Add a backend-common package with common concerns such as logging

This commit is contained in:
Fredrik Adelöw
2020-04-29 09:28:50 +02:00
parent a92f662d88
commit 77d48d8794
19 changed files with 722 additions and 13 deletions
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class StatusCodeError extends Error {
public statusCode: number;
constructor(statusCode: number, message?: string) {
super(message);
this.statusCode = statusCode;
}
}
export class InvalidRequestError extends StatusCodeError {
constructor(message?: string) {
super(400, message || 'Invalid Request');
}
}
export class NotFoundError extends StatusCodeError {
constructor(message?: string) {
super(404, message || 'Not Found');
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './errors';
export * from './logging';
export * from './middleware';
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './logger';
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import winston, { Logger } from 'winston';
export let logger: Logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? winston.format.json()
: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.simple(),
),
defaultMeta: { service: 'backstage' },
transports: [
new winston.transports.Console({
silent:
process.env.JEST_WORKER_ID !== undefined && !process.env.LOG_LEVEL,
}),
],
});
export function setLogger(newLogger: Logger) {
logger = newLogger;
}
@@ -0,0 +1,48 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import request from 'supertest';
import { errorHandler } from './errorHandler';
import { StatusCodeError } from '../errors';
describe('errorHandler', () => {
it('gives default code and message', async () => {
const app = express();
app.use('/breaks', () => {
throw new Error('some message');
});
app.use(errorHandler());
const response = await request(app).get('/breaks');
expect(response.status).toBe(500);
expect(response.text).toBe('some message');
});
it('takes code from StatusCodeError', async () => {
const app = express();
app.use('/breaks', () => {
throw new StatusCodeError(432, 'Some Message');
});
app.use(errorHandler());
const response = await request(app).get('/breaks');
expect(response.status).toBe(432);
expect(response.text).toContain('Some Message');
});
});
@@ -0,0 +1,53 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
/**
* Express middleware to handle errors during request processing.
*
* This is commonly the second to last middleware in the chain (before the
* notFoundHandler). It special cases StatusCodeError errors to expose their
* embedded status codes.
*
*
*/
export function errorHandler(): ErrorRequestHandler {
/* eslint-disable @typescript-eslint/no-unused-vars */
return (
error: Error,
_request: Request,
response: Response,
_next: NextFunction,
) => {
const status = getStatusCode(error);
const message = error.message || 'Internal Server Error';
response.status(status).send(message);
};
}
function getStatusCode(error: Error): number {
const errorStatusCode = (error as any).statusCode;
if (
typeof errorStatusCode === 'number' &&
errorStatusCode >= 100 &&
errorStatusCode <= 599
) {
return errorStatusCode;
}
return 500;
}
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './errorHandler';
export * from './notFoundHandler';
export * from './requestLoggingHandler';
@@ -0,0 +1,33 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import request from 'supertest';
import { notFoundHandler } from './notFoundHandler';
describe('notFoundHandler', () => {
it('handles only missing routes', async () => {
const app = express();
app.use('/exists', (_, res) => res.status(200).send());
app.use(notFoundHandler());
const existsResponse = await request(app).get('/exists');
const doesNotExistResponse = await request(app).get('/doesNotExist');
expect(existsResponse.status).toBe(200);
expect(doesNotExistResponse.status).toBe(404);
});
});
@@ -0,0 +1,32 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NextFunction, Request, RequestHandler, Response } from 'express';
/**
* Express middleware to handle requests for missing routes.
*
* Should be used as the very last handler in the chain, as it unconditionally
* returns a 404 status.
*
* @returns An Apollo request handler
*/
export function notFoundHandler(): RequestHandler {
/* eslint-disable @typescript-eslint/no-unused-vars */
return (_request: Request, response: Response, _next: NextFunction) => {
response.status(404).send('Not Found');
};
}
@@ -0,0 +1,38 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import request from 'supertest';
import { requestLoggingHandler } from './requestLoggingHandler';
describe('requestLoggingHandler', () => {
it('emits logs for each request', async () => {
const logger = jest.fn();
const app = express();
app.use(requestLoggingHandler(logger));
app.use('/exists1', (_, res) => res.status(200).send());
app.use('/exists2', (_, res) => res.status(201).send());
const r = request(app);
await r.get('/exists1');
await r.get('/exists2');
expect(logger).toHaveBeenCalledTimes(2);
expect(logger).toHaveBeenNthCalledWith(1, expect.stringContaining('200'));
expect(logger).toHaveBeenNthCalledWith(2, expect.stringContaining('201'));
});
});
@@ -0,0 +1,38 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RequestHandler } from 'express';
import morgan from 'morgan';
import { logger as commonLogger } from '../logging';
/**
* Logs incoming requests.
*
* @param logger An optional logger to use. If not specified, the default logger is used.
* @returns An Apollo request handler
*/
export function requestLoggingHandler(
logger?: (message: String) => void,
): RequestHandler {
const actualLogger = logger || commonLogger.info;
return morgan('combined', {
stream: {
write(message: String) {
actualLogger(message);
},
},
});
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require('jest-fetch-mock').enableMocks();