Merge pull request #24817 from backstage/healthcheck-router
Healthcheck service
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Added a new Root Health Service which adds new endpoints for health checks.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Added mock for the Root Health Service in `mockServices`.
|
||||
@@ -28,6 +28,7 @@ import { coreServices } from '@backstage/backend-plugin-api';
|
||||
- [Permissions Service](./permissions.md) - Permission system integration for authorization of user actions.
|
||||
- [Plugin Metadata Service](./plugin-metadata.md) - Built-in service for accessing metadata about the current plugin.
|
||||
- [Root Config Service](./root-config.md) - Access to static configuration.
|
||||
- [Root Health Service](./root-health.md) - Health check endpoints for the backend.
|
||||
- [Root Http Router Service](./root-http-router.md) - HTTP route registration for root services.
|
||||
- [Root Lifecycle Service](./root-lifecycle.md) - Registration of backend startup and shutdown lifecycle hooks.
|
||||
- [Root Logger Service](./root-logger.md) - Root-level logging.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: root-health
|
||||
title: Root Health Service
|
||||
sidebar_label: Health
|
||||
description: Documentation for the Health service
|
||||
---
|
||||
|
||||
The Root Health service provides some health check endpoints for the backend. By default, the `rootHttpRouter` exposes a `/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness` endpoints, which return a JSON object with the status of the backend services according the implementation of the Root Health Service.
|
||||
|
||||
## Configuring the service
|
||||
|
||||
The following example shows how you can override the root health service implementation.
|
||||
|
||||
```ts
|
||||
import { RootHealthService, coreServices } from '@backstage/backend-plugin-api';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
class MyRootHealthService implements RootHealthService {
|
||||
async getLiveness() {
|
||||
// provide your own implementation
|
||||
return { status: 200, payload: { status: 'ok' } };
|
||||
}
|
||||
|
||||
async getReadiness() {
|
||||
// provide your own implementation
|
||||
return { status: 200, payload: { status: 'ok' } };
|
||||
}
|
||||
}
|
||||
|
||||
backend.add(
|
||||
createServiceFactory({
|
||||
service: coreServices.rootHealth,
|
||||
deps: {},
|
||||
async factory({}) {
|
||||
return new MyRootHealthService();
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
## API Report File for "@backstage/backend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { RootHealthService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export const rootHealthServiceFactory: () => ServiceFactory<
|
||||
RootHealthService,
|
||||
'root'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -121,6 +121,8 @@ export interface RootHttpRouterConfigureContext {
|
||||
// (undocumented)
|
||||
config: RootConfigService;
|
||||
// (undocumented)
|
||||
healthRouter: RequestHandler;
|
||||
// (undocumented)
|
||||
lifecycle: LifecycleService;
|
||||
// (undocumented)
|
||||
logger: LoggerService;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"./logger": "./src/entrypoints/logger/index.ts",
|
||||
"./permissions": "./src/entrypoints/permissions/index.ts",
|
||||
"./rootConfig": "./src/entrypoints/rootConfig/index.ts",
|
||||
"./rootHealth": "./src/entrypoints/rootHealth/index.ts",
|
||||
"./rootHttpRouter": "./src/entrypoints/rootHttpRouter/index.ts",
|
||||
"./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts",
|
||||
"./rootLogger": "./src/entrypoints/rootLogger/index.ts",
|
||||
@@ -72,6 +73,9 @@
|
||||
"rootConfig": [
|
||||
"src/entrypoints/rootConfig/index.ts"
|
||||
],
|
||||
"rootHealth": [
|
||||
"src/entrypoints/rootHealth/index.ts"
|
||||
],
|
||||
"rootHttpRouter": [
|
||||
"src/entrypoints/rootHttpRouter/index.ts"
|
||||
],
|
||||
|
||||
@@ -30,6 +30,7 @@ import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
|
||||
import { loggerServiceFactory } from '@backstage/backend-defaults/logger';
|
||||
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
|
||||
import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig';
|
||||
import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth';
|
||||
import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter';
|
||||
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
|
||||
import { rootLoggerServiceFactory } from '@backstage/backend-defaults/rootLogger';
|
||||
@@ -50,6 +51,7 @@ export const defaultServiceFactories = [
|
||||
lifecycleServiceFactory(),
|
||||
loggerServiceFactory(),
|
||||
permissionsServiceFactory(),
|
||||
rootHealthServiceFactory(),
|
||||
rootHttpRouterServiceFactory(),
|
||||
rootLifecycleServiceFactory(),
|
||||
rootLoggerServiceFactory(),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 { rootHealthServiceFactory } from './rootHealthServiceFactory';
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 { mockServices } from '@backstage/backend-test-utils';
|
||||
import { DefaultRootHealthService } from './rootHealthServiceFactory';
|
||||
|
||||
describe('DefaultRootHealthService', () => {
|
||||
describe('readiness', () => {
|
||||
it(`should return a 500 response if the server hasn't started yet`, async () => {
|
||||
const service = new DefaultRootHealthService({
|
||||
lifecycle: mockServices.rootLifecycle.mock(),
|
||||
});
|
||||
await expect(service.getReadiness()).resolves.toEqual({
|
||||
status: 503,
|
||||
payload: {
|
||||
message: 'Backend has not started yet',
|
||||
status: 'error',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 if the server has started', async () => {
|
||||
let mockServerStartedFn = () => {};
|
||||
|
||||
const lifecycle = mockServices.rootLifecycle.mock({
|
||||
addStartupHook: jest.fn(fn => (mockServerStartedFn = fn)),
|
||||
});
|
||||
|
||||
const service = new DefaultRootHealthService({
|
||||
lifecycle,
|
||||
});
|
||||
|
||||
mockServerStartedFn();
|
||||
|
||||
await expect(service.getReadiness()).resolves.toEqual({
|
||||
status: 200,
|
||||
payload: { status: 'ok' },
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return a 500 response if the server has stopped`, async () => {
|
||||
let mockServerStartedFn = () => {};
|
||||
let mockServerStoppedFn = () => {};
|
||||
|
||||
const lifecycle = mockServices.rootLifecycle.mock({
|
||||
addStartupHook: jest.fn(fn => (mockServerStartedFn = fn)),
|
||||
addShutdownHook: jest.fn(fn => (mockServerStoppedFn = fn)),
|
||||
});
|
||||
|
||||
const service = new DefaultRootHealthService({
|
||||
lifecycle,
|
||||
});
|
||||
|
||||
mockServerStartedFn();
|
||||
mockServerStoppedFn();
|
||||
await expect(service.getReadiness()).resolves.toEqual({
|
||||
status: 503,
|
||||
payload: {
|
||||
message: 'Backend has not started yet',
|
||||
status: 'error',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('liveness', () => {
|
||||
it('should return 200 if the server has started', async () => {
|
||||
const service = new DefaultRootHealthService({
|
||||
lifecycle: mockServices.rootLifecycle.mock(),
|
||||
});
|
||||
|
||||
await expect(service.getLiveness()).resolves.toEqual({
|
||||
status: 200,
|
||||
payload: { status: 'ok' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
RootHealthService,
|
||||
RootLifecycleService,
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @internal */
|
||||
export class DefaultRootHealthService implements RootHealthService {
|
||||
#isRunning = false;
|
||||
|
||||
constructor(readonly options: { lifecycle: RootLifecycleService }) {
|
||||
options.lifecycle.addStartupHook(() => {
|
||||
this.#isRunning = true;
|
||||
});
|
||||
options.lifecycle.addShutdownHook(() => {
|
||||
this.#isRunning = false;
|
||||
});
|
||||
}
|
||||
|
||||
async getLiveness(): Promise<{ status: number; payload?: any }> {
|
||||
return { status: 200, payload: { status: 'ok' } };
|
||||
}
|
||||
|
||||
async getReadiness(): Promise<{ status: number; payload?: any }> {
|
||||
if (!this.#isRunning) {
|
||||
return {
|
||||
status: 503,
|
||||
payload: { message: 'Backend has not started yet', status: 'error' },
|
||||
};
|
||||
}
|
||||
|
||||
return { status: 200, payload: { status: 'ok' } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const rootHealthServiceFactory = createServiceFactory({
|
||||
service: coreServices.rootHealth,
|
||||
deps: {
|
||||
lifecycle: coreServices.rootLifecycle,
|
||||
},
|
||||
async factory({ lifecycle }) {
|
||||
return new DefaultRootHealthService({ lifecycle });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { RootHealthService } from '@backstage/backend-plugin-api';
|
||||
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 Router from 'express-promise-router';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export function createHealthRouter(options: { health: RootHealthService }) {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'.backstage/health/v1/readiness',
|
||||
async (_request: Request, response: Response) => {
|
||||
const { status, payload } = await options.health.getReadiness();
|
||||
response.status(status).json(payload);
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
'.backstage/health/v1/liveness',
|
||||
async (_request: Request, response: Response) => {
|
||||
const { status, payload } = await options.health.getLiveness();
|
||||
response.status(status).json(payload);
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
+8
-1
@@ -29,6 +29,7 @@ import {
|
||||
readHttpServerOptions,
|
||||
} from './http';
|
||||
import { DefaultRootHttpRouter } from './DefaultRootHttpRouter';
|
||||
import { createHealthRouter } from './createHealthRouter';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -41,6 +42,7 @@ export interface RootHttpRouterConfigureContext {
|
||||
config: RootConfigService;
|
||||
logger: LoggerService;
|
||||
lifecycle: LifecycleService;
|
||||
healthRouter: RequestHandler;
|
||||
applyDefaults: () => void;
|
||||
}
|
||||
|
||||
@@ -75,8 +77,9 @@ export const rootHttpRouterServiceFactory = createServiceFactory(
|
||||
config: coreServices.rootConfig,
|
||||
rootLogger: coreServices.rootLogger,
|
||||
lifecycle: coreServices.rootLifecycle,
|
||||
health: coreServices.rootHealth,
|
||||
},
|
||||
async factory({ config, rootLogger, lifecycle }) {
|
||||
async factory({ config, rootLogger, lifecycle, health }) {
|
||||
const { indexPath, configure = defaultConfigure } = options ?? {};
|
||||
const logger = rootLogger.child({ service: 'rootHttpRouter' });
|
||||
const app = express();
|
||||
@@ -84,6 +87,8 @@ export const rootHttpRouterServiceFactory = createServiceFactory(
|
||||
const router = DefaultRootHttpRouter.create({ indexPath });
|
||||
const middleware = MiddlewareFactory.create({ config, logger });
|
||||
const routes = router.handler();
|
||||
|
||||
const healthRouter = createHealthRouter({ health });
|
||||
const server = await createHttpServer(
|
||||
app,
|
||||
readHttpServerOptions(config.getOptionalConfig('backend')),
|
||||
@@ -98,11 +103,13 @@ export const rootHttpRouterServiceFactory = createServiceFactory(
|
||||
config,
|
||||
logger,
|
||||
lifecycle,
|
||||
healthRouter,
|
||||
applyDefaults() {
|
||||
app.use(middleware.helmet());
|
||||
app.use(middleware.cors());
|
||||
app.use(middleware.compression());
|
||||
app.use(middleware.logging());
|
||||
app.use(healthRouter);
|
||||
app.use(routes);
|
||||
app.use(middleware.notFound());
|
||||
app.use(middleware.error());
|
||||
|
||||
@@ -194,6 +194,7 @@ export namespace coreServices {
|
||||
const rootConfig: ServiceRef<RootConfigService, 'root'>;
|
||||
const database: ServiceRef<DatabaseService, 'plugin'>;
|
||||
const discovery: ServiceRef<DiscoveryService, 'plugin'>;
|
||||
const rootHealth: ServiceRef<RootHealthService, 'root'>;
|
||||
const httpAuth: ServiceRef<HttpAuthService, 'plugin'>;
|
||||
const httpRouter: ServiceRef<HttpRouterService, 'plugin'>;
|
||||
const lifecycle: ServiceRef<LifecycleService, 'plugin'>;
|
||||
@@ -500,6 +501,18 @@ export function resolveSafeChildPath(base: string, path: string): string;
|
||||
// @public
|
||||
export interface RootConfigService extends Config {}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RootHealthService {
|
||||
getLiveness(): Promise<{
|
||||
status: number;
|
||||
payload?: JsonValue;
|
||||
}>;
|
||||
getReadiness(): Promise<{
|
||||
status: number;
|
||||
payload?: JsonValue;
|
||||
}>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface RootHttpRouterService {
|
||||
use(path: string, handler: Handler): void;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 { JsonValue } from '@backstage/types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface RootHealthService {
|
||||
/**
|
||||
* Get the liveness status of the backend.
|
||||
*/
|
||||
getLiveness(): Promise<{ status: number; payload?: JsonValue }>;
|
||||
/**
|
||||
* Get the readiness status of the backend.
|
||||
*/
|
||||
getReadiness(): Promise<{ status: number; payload?: JsonValue }>;
|
||||
}
|
||||
@@ -102,6 +102,13 @@ export namespace coreServices {
|
||||
import('./DiscoveryService').DiscoveryService
|
||||
>({ id: 'core.discovery' });
|
||||
|
||||
/**
|
||||
* The service reference for the plugin scoped {@link RootHealthService}.
|
||||
*/
|
||||
export const rootHealth = createServiceRef<
|
||||
import('./RootHealthService').RootHealthService
|
||||
>({ id: 'core.rootHealth', scope: 'root' });
|
||||
|
||||
/**
|
||||
* Authentication of HTTP requests.
|
||||
*
|
||||
|
||||
@@ -32,6 +32,7 @@ export type {
|
||||
export type { RootConfigService } from './RootConfigService';
|
||||
export type { DatabaseService } from './DatabaseService';
|
||||
export type { DiscoveryService } from './DiscoveryService';
|
||||
export type { RootHealthService } from './RootHealthService';
|
||||
export type {
|
||||
HttpRouterService,
|
||||
HttpRouterServiceAuthPolicy,
|
||||
|
||||
@@ -32,6 +32,7 @@ import { LifecycleService } from '@backstage/backend-plugin-api';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { PermissionsService } from '@backstage/backend-plugin-api';
|
||||
import { RootConfigService } from '@backstage/backend-plugin-api';
|
||||
import { RootHealthService } from '@backstage/backend-plugin-api';
|
||||
import { RootHttpRouterFactoryOptions } from '@backstage/backend-defaults/rootHttpRouter';
|
||||
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
|
||||
import { RootLifecycleService } from '@backstage/backend-plugin-api';
|
||||
@@ -280,6 +281,15 @@ export namespace mockServices {
|
||||
) => ServiceFactory<RootConfigService, 'root'>;
|
||||
}
|
||||
// (undocumented)
|
||||
export namespace rootHealth {
|
||||
const // (undocumented)
|
||||
factory: () => ServiceFactory<RootHealthService, 'root'>;
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?: Partial<RootHealthService> | undefined,
|
||||
) => ServiceMock<RootHealthService>;
|
||||
}
|
||||
// (undocumented)
|
||||
export namespace rootHttpRouter {
|
||||
const // (undocumented)
|
||||
factory: (
|
||||
|
||||
@@ -24,6 +24,7 @@ import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter
|
||||
import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
|
||||
import { loggerServiceFactory } from '@backstage/backend-defaults/logger';
|
||||
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
|
||||
import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth';
|
||||
import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter';
|
||||
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
|
||||
import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler';
|
||||
@@ -344,6 +345,14 @@ export namespace mockServices {
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace rootHealth {
|
||||
export const factory = rootHealthServiceFactory;
|
||||
export const mock = simpleMock(coreServices.rootHealth, () => ({
|
||||
getLiveness: jest.fn(),
|
||||
getReadiness: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export namespace httpRouter {
|
||||
export const factory = httpRouterServiceFactory;
|
||||
export const mock = simpleMock(coreServices.httpRouter, () => ({
|
||||
|
||||
Reference in New Issue
Block a user