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