Merge pull request #1592 from andrewthauer/backend-status-checks

[Backend-Common] Add backend status check handler middleware
This commit is contained in:
Patrik Oldsberg
2020-07-27 12:53:47 +02:00
committed by GitHub
10 changed files with 230 additions and 2 deletions
+1
View File
@@ -35,6 +35,7 @@
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"helmet": "^3.22.0",
"morgan": "^1.10.0",
"stoppable": "^1.1.0",
@@ -17,3 +17,4 @@
export * from './errorHandler';
export * from './notFoundHandler';
export * from './requestLoggingHandler';
export * from './statusCheckHandler';
@@ -0,0 +1,55 @@
/*
* 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 { statusCheckHandler } from './statusCheckHandler';
describe('statusCheckHandler', () => {
it('gives status 200 when using default', async () => {
const app = express();
app.use('/healthcheck', await statusCheckHandler());
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(200);
expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
});
it('gives status 200 when status function returns true', async () => {
const app = express();
const status = { foo: 'bar' };
const statusCheck = () => Promise.resolve(status);
app.use('/healthcheck', await statusCheckHandler({ statusCheck }));
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(200);
expect(response.text).toBe(JSON.stringify(status));
});
it('gives status 500 when status check throws an error', async () => {
const app = express();
const statusCheck = () => {
throw Error('error!');
};
app.use('/healthcheck', await statusCheckHandler({ statusCheck }));
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(500);
});
});
@@ -0,0 +1,51 @@
/*
* 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, Response, RequestHandler } from 'express';
export type StatusCheck = () => Promise<any>;
export interface StatusCheckHandlerOptions {
/**
* Optional status function which returns a message.
*/
statusCheck?: StatusCheck;
}
/**
* Express middleware for status checks.
*
* This is commonly used to implement healthcheck and readiness routes.
*
* @param options An optional configuration object.
* @returns An Express error request handler
*/
export async function statusCheckHandler(
options: StatusCheckHandlerOptions = {},
): Promise<RequestHandler> {
const statusCheck: StatusCheck = options.statusCheck
? options.statusCheck
: () => Promise.resolve({ status: 'ok' });
return async (_request: Request, response: Response, next: NextFunction) => {
try {
const status = await statusCheck();
response.status(200).header('').send(status);
} catch (err) {
next(err);
}
};
}
@@ -0,0 +1,56 @@
/*
* 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 * as winston from 'winston';
import request from 'supertest';
import { createStatusCheckRouter } from './createStatusCheckRouter';
describe('createStatusCheckRouter', () => {
const logger = winston.createLogger();
it('gives status 200 when using default path', async () => {
const app = express();
app.use('', await createStatusCheckRouter({ logger }));
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(200);
expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
});
it('gives status 200 when using custom path', async () => {
const app = express();
app.use('', await createStatusCheckRouter({ logger, path: '/ready' }));
const response = await request(app).get('/ready');
expect(response.status).toBe(200);
expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
});
it('gives status 500 when status check throws an error', async () => {
const app = express();
const statusCheck = () => {
throw Error('error!');
};
app.use('', await createStatusCheckRouter({ logger, statusCheck }));
const response = await request(app).get('/healthcheck');
expect(response.status).toBe(500);
});
});
@@ -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 { Logger } from 'winston';
import Router from 'express-promise-router';
import express from 'express';
import { errorHandler, statusCheckHandler, StatusCheck } from '../middleware';
export interface StatusCheckRouterOptions {
logger: Logger;
path?: string;
statusCheck?: StatusCheck;
}
export async function createStatusCheckRouter(
options: StatusCheckRouterOptions,
): Promise<express.Router> {
const router = Router();
const { path = '/healthcheck', statusCheck } = options;
router.use(path, await statusCheckHandler({ statusCheck }));
router.use(errorHandler());
return router;
}
@@ -15,4 +15,5 @@
*/
export { createServiceBuilder } from './createServiceBuilder';
export { createStatusCheckRouter } from './createStatusCheckRouter';
export type { ServiceBuilder } from './types';
+2 -2
View File
@@ -16,7 +16,7 @@
import { ConfigReader } from '@backstage/config';
import cors from 'cors';
import { Router } from 'express';
import { Router, RequestHandler } from 'express';
import { Server } from 'http';
import { Logger } from 'winston';
@@ -64,7 +64,7 @@ export type ServiceBuilder = {
* @param root The root URL to bind to (e.g. "/api/function1")
* @param router An express router
*/
addRouter(root: string, router: Router): ServiceBuilder;
addRouter(root: string, router: Router | RequestHandler): ServiceBuilder;
/**
* Starts the server using the given settings.
+3
View File
@@ -30,6 +30,7 @@ import {
import { ConfigReader, AppConfig } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import knex from 'knex';
import healthcheck from './plugins/healthcheck';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
import identity from './plugins/identity';
@@ -63,6 +64,7 @@ async function main() {
const configReader = ConfigReader.fromConfigs(configs);
const createEnv = makeCreateEnv(configs);
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
@@ -75,6 +77,7 @@ async function main() {
const service = createServiceBuilder(module)
.loadConfig(configReader)
.addRouter('', await healthcheck(healthcheckEnv))
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/rollbar', await rollbar(rollbarEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
@@ -0,0 +1,22 @@
/*
* 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 { createStatusCheckRouter } from '@backstage/backend-common';
import { PluginEnvironment } from '../types';
export default async function createRouter({ logger }: PluginEnvironment) {
return await createStatusCheckRouter({ logger, path: '/healthcheck' });
}