mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
feat: include readiness check option
This commit is contained in:
parent
e9d2b30603
commit
4d599a2118
@ -786,6 +786,11 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||
options.prometheusImpactMetricsApi ||
|
||||
process.env.PROMETHEUS_IMPACT_METRICS_API;
|
||||
|
||||
const checkDbOnReady =
|
||||
typeof options.checkDbOnReady === 'boolean'
|
||||
? options.checkDbOnReady
|
||||
: parseEnvVarBoolean(process.env.CHECK_DB_ON_READY, false);
|
||||
|
||||
return {
|
||||
db,
|
||||
session,
|
||||
@ -830,5 +835,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||
userInactivityThresholdInDays,
|
||||
buildDate: process.env.BUILD_DATE,
|
||||
unleashFrontendToken,
|
||||
checkDbOnReady,
|
||||
};
|
||||
}
|
||||
|
||||
@ -169,6 +169,7 @@ export * from './public-signup-token-schema.js';
|
||||
export * from './public-signup-token-update-schema.js';
|
||||
export * from './public-signup-tokens-schema.js';
|
||||
export * from './push-variants-schema.js';
|
||||
export * from './ready-check-schema.js';
|
||||
export * from './record-ui-error-schema.js';
|
||||
export * from './release-plan-milestone-schema.js';
|
||||
export * from './release-plan-milestone-strategy-schema.js';
|
||||
|
||||
22
src/lib/openapi/spec/ready-check-schema.ts
Normal file
22
src/lib/openapi/spec/ready-check-schema.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const readyCheckSchema = {
|
||||
$id: '#/components/schemas/readyCheckSchema',
|
||||
type: 'object',
|
||||
description:
|
||||
'Used by service orchestrators to decide whether this Unleash instance should be considered ready to serve requests.',
|
||||
additionalProperties: false,
|
||||
required: ['health'],
|
||||
properties: {
|
||||
health: {
|
||||
description:
|
||||
'The readiness state this Unleash instance is in. GOOD if the server is up and running. It never returns BAD; if the server is unhealthy you will get an unsuccessful http response.',
|
||||
type: 'string',
|
||||
enum: ['GOOD', 'BAD'],
|
||||
example: 'GOOD',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type ReadyCheckSchema = FromSchema<typeof readyCheckSchema>;
|
||||
@ -28,11 +28,11 @@ afterEach(() => {
|
||||
getLogger.setMuteError(false);
|
||||
});
|
||||
|
||||
test('should give 200 when ready', async () => {
|
||||
test('should give 200 when healthy', async () => {
|
||||
await request.get('/health').expect(200);
|
||||
});
|
||||
|
||||
test('should give health=GOOD when ready', async () => {
|
||||
test('should give health=GOOD when healthy', async () => {
|
||||
expect.assertions(2);
|
||||
await request
|
||||
.get('/health')
|
||||
|
||||
@ -8,6 +8,7 @@ import Controller from './controller.js';
|
||||
import { AdminApi } from './admin-api/index.js';
|
||||
import ClientApi from './client-api/index.js';
|
||||
|
||||
import { ReadyCheckController } from './ready-check.js';
|
||||
import { HealthCheckController } from './health-check.js';
|
||||
import FrontendAPIController from '../features/frontend-api/frontend-api-controller.js';
|
||||
import EdgeController from './edge-api/index.js';
|
||||
@ -25,6 +26,10 @@ class IndexRouter extends Controller {
|
||||
) {
|
||||
super(config);
|
||||
|
||||
this.use(
|
||||
'/ready',
|
||||
new ReadyCheckController(config, services, db).router,
|
||||
);
|
||||
this.use('/health', new HealthCheckController(config, services).router);
|
||||
this.use(
|
||||
'/invite',
|
||||
|
||||
44
src/lib/routes/ready-check.test.ts
Normal file
44
src/lib/routes/ready-check.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import supertest, { type Test } from 'supertest';
|
||||
import { createServices } from '../services/index.js';
|
||||
import { createTestConfig } from '../../test/config/test-config.js';
|
||||
|
||||
import createStores from '../../test/fixtures/store.js';
|
||||
import getLogger from '../../test/fixtures/no-logger.js';
|
||||
import getApp from '../app.js';
|
||||
import type TestAgent from 'supertest/lib/agent.d.ts';
|
||||
|
||||
async function getSetup() {
|
||||
const stores = createStores();
|
||||
const config = createTestConfig();
|
||||
const services = createServices(stores, config);
|
||||
const app = await getApp(config, stores, services);
|
||||
|
||||
return {
|
||||
request: supertest(app),
|
||||
stores,
|
||||
};
|
||||
}
|
||||
let request: TestAgent<Test>;
|
||||
beforeEach(async () => {
|
||||
const setup = await getSetup();
|
||||
request = setup.request;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getLogger.setMuteError(false);
|
||||
});
|
||||
|
||||
test('should give 200 when ready', async () => {
|
||||
await request.get('/ready').expect(200);
|
||||
});
|
||||
|
||||
test('should give health=GOOD when ready', async () => {
|
||||
expect.assertions(2);
|
||||
await request
|
||||
.get('/ready')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.health).toBe('GOOD');
|
||||
});
|
||||
});
|
||||
62
src/lib/routes/ready-check.ts
Normal file
62
src/lib/routes/ready-check.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import type { IUnleashConfig } from '../types/option.js';
|
||||
import type { IUnleashServices } from '../services/index.js';
|
||||
import type { Db } from '../db/db.js';
|
||||
import type { Logger } from '../logger.js';
|
||||
|
||||
import Controller from './controller.js';
|
||||
import { NONE } from '../types/permissions.js';
|
||||
import { createResponseSchema } from '../openapi/util/create-response-schema.js';
|
||||
import type { ReadyCheckSchema } from '../openapi/spec/ready-check-schema.js';
|
||||
|
||||
export class ReadyCheckController extends Controller {
|
||||
private logger: Logger;
|
||||
|
||||
private db?: Db;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{ openApiService }: Pick<IUnleashServices, 'openApiService'>,
|
||||
db?: Db,
|
||||
) {
|
||||
super(config);
|
||||
this.logger = config.getLogger('ready-check.js');
|
||||
this.db = db;
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '',
|
||||
handler: this.getReady,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Operational'],
|
||||
operationId: 'getReady',
|
||||
summary: 'Get instance readiness status',
|
||||
description:
|
||||
'This operation returns information about whether this Unleash instance is ready to serve requests or not. Typically used by your deployment orchestrator (e.g. Kubernetes, Docker Swarm, Mesos, et al.).',
|
||||
responses: {
|
||||
200: createResponseSchema('readyCheckSchema'),
|
||||
500: createResponseSchema('readyCheckSchema'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getReady(_: Request, res: Response<ReadyCheckSchema>): Promise<void> {
|
||||
if (this.config.checkDbOnReady && this.db) {
|
||||
try {
|
||||
await this.db.raw('select 1');
|
||||
res.status(200).json({ health: 'GOOD' });
|
||||
return;
|
||||
} catch (err: any) {
|
||||
this.logger.warn('Database readiness check failed', err);
|
||||
res.status(500).json({ health: 'BAD' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ health: 'GOOD' });
|
||||
}
|
||||
}
|
||||
@ -175,6 +175,7 @@ export interface IUnleashOptions {
|
||||
resourceLimits?: Partial<ResourceLimits>;
|
||||
userInactivityThresholdInDays?: number;
|
||||
unleashFrontendToken?: string;
|
||||
checkDbOnReady?: boolean;
|
||||
}
|
||||
|
||||
export interface IEmailOption {
|
||||
@ -301,4 +302,6 @@ export interface IUnleashConfig {
|
||||
userInactivityThresholdInDays: number;
|
||||
buildDate?: string;
|
||||
unleashFrontendToken?: string;
|
||||
/** If true, the readiness endpoint will attempt a simple Postgres query to verify DB availability */
|
||||
checkDbOnReady?: boolean;
|
||||
}
|
||||
|
||||
27
src/test/e2e/ready.e2e.test.ts
Normal file
27
src/test/e2e/ready.e2e.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { setupApp } from './helpers/test-helper.js';
|
||||
import dbInit, { type ITestDb } from './helpers/database-init.js';
|
||||
import getLogger from '../fixtures/no-logger.js';
|
||||
import type { IUnleashStores } from '../../lib/types/index.js';
|
||||
|
||||
let stores: IUnleashStores;
|
||||
let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('ready_api', getLogger);
|
||||
stores = db.stores;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('returns readiness good', async () => {
|
||||
expect.assertions(0);
|
||||
const { request, destroy } = await setupApp(stores);
|
||||
await request
|
||||
.get('/ready')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect('{"health":"GOOD"}');
|
||||
await destroy();
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user