From 88e3581ebcec6111189388d433f843ce6e48c06a Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Sat, 11 Jul 2020 15:38:16 -0400 Subject: [PATCH] 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. --- .../backend-common/src/middleware/index.ts | 1 + .../src/middleware/statusCheckHandler.test.ts | 55 +++++++++++++++++++ .../src/middleware/statusCheckHandler.ts | 51 +++++++++++++++++ .../src/service/lib/ServiceBuilderImpl.ts | 6 +- packages/backend-common/src/service/types.ts | 4 +- packages/backend/src/index.ts | 2 + 6 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 packages/backend-common/src/middleware/statusCheckHandler.test.ts create mode 100644 packages/backend-common/src/middleware/statusCheckHandler.ts diff --git a/packages/backend-common/src/middleware/index.ts b/packages/backend-common/src/middleware/index.ts index 083b36c3e9..76c52d8830 100644 --- a/packages/backend-common/src/middleware/index.ts +++ b/packages/backend-common/src/middleware/index.ts @@ -17,3 +17,4 @@ export * from './errorHandler'; export * from './notFoundHandler'; export * from './requestLoggingHandler'; +export * from './statusCheckHandler'; diff --git a/packages/backend-common/src/middleware/statusCheckHandler.test.ts b/packages/backend-common/src/middleware/statusCheckHandler.test.ts new file mode 100644 index 0000000000..7ed1a65b58 --- /dev/null +++ b/packages/backend-common/src/middleware/statusCheckHandler.test.ts @@ -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); + }); +}); diff --git a/packages/backend-common/src/middleware/statusCheckHandler.ts b/packages/backend-common/src/middleware/statusCheckHandler.ts new file mode 100644 index 0000000000..3f62f04f59 --- /dev/null +++ b/packages/backend-common/src/middleware/statusCheckHandler.ts @@ -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; + +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 { + 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); + } + }; +} diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 4a51fb195c..eb07c03af5 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -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; } diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index b39df27527..070794969f 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -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. diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d060791f98..9764f633b5 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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))