feat: add backend status check handler middleware

The statusCheckHandler is a simple express middleware that helps
implement healthcheck/readiness endpoints. It can provided an optional
status function that should return a promise that resolves in either
true or false. The response will be either a 200 or 503 respectively.
Any errors that occur will be forwarded to next.
This commit is contained in:
Andrew Thauer
2020-07-11 15:38:16 -04:00
parent 3158c2390b
commit 88e3581ebc
6 changed files with 114 additions and 5 deletions
@@ -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);
}
};
}
@@ -17,7 +17,7 @@
import { ConfigReader } from '@backstage/config';
import compression from 'compression';
import cors from 'cors';
import express, { Router } from 'express';
import express, { Router, RequestHandler } from 'express';
import helmet from 'helmet';
import { Server } from 'http';
import stoppable from 'stoppable';
@@ -40,7 +40,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private host: string | undefined;
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private routers: [string, Router][];
private routers: [string, Router | RequestHandler][];
// Reference to the module where builder is created - needed for hot module
// reloading
private module: NodeModule;
@@ -92,7 +92,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
addRouter(root: string, router: Router): ServiceBuilder {
addRouter(root: string, router: Router | RequestHandler): ServiceBuilder {
this.routers.push([root, router]);
return this;
}
+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.
+2
View File
@@ -25,6 +25,7 @@
import {
createServiceBuilder,
getRootLogger,
statusCheckHandler,
useHotMemoize,
} from '@backstage/backend-common';
import { ConfigReader, AppConfig } from '@backstage/config';
@@ -73,6 +74,7 @@ async function main() {
const service = createServiceBuilder(module)
.loadConfig(configReader)
.addRouter('/healthcheck', await statusCheckHandler())
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/rollbar', await rollbar(rollbarEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))