import type { OnModuleInit } from '@nestjs/common'; import { Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import type { Constructor } from '@rsdk/common'; import { ILogger } from '@rsdk/logging'; import { RsdkMetadataProvider } from '@rsdk/metadata'; import { InjectLogger } from '../logging'; import { HEALTH_INDICATOR_RSDK_METADATA_SCOPE } from './metadata/constants'; import type { HealthIndicatorRsdkMetadata } from './metadata/types'; import { HealthCheckException } from './exceptions'; import { DEFAULT_HEALTH_CHECK_SCOPE } from './health.const'; import type { CheckSummary, HealthIndicator, IndicatorMetadata, IndicatorWithMetadata, ResolvedIndicators, } from './types'; @Injectable() export class HealthChecksService implements OnModuleInit { private readonly indicators: ResolvedIndicators = { common: [] }; private readonly indicatorNames = new Map(); constructor( @InjectLogger(HealthChecksService) private readonly logger: ILogger, private readonly moduleRef: ModuleRef, private readonly rsdkMetadataProvider: RsdkMetadataProvider, ) {} async check(filter?: '*' | string): Promise { this.logger.trace('Checking health', { filter }); const indicators = this.filterIndicators(filter); let success = true; const summary: CheckSummary = { details: {}, status: 'up' }; await Promise.all( indicators.map(async ({ indicator, meta: { isOptional } }) => { const name = this.indicatorNames.get(indicator); if (!name) { throw new Error( `Indicator [${indicator.constructor.name}] - name not found`, ); } try { const result = await indicator.check(); if (!isOptional && result.isError) { success = false; } result.details.optional = isOptional; summary.details[name] = result; } catch (error) { if (!isOptional) { success = false; } const err = error as any; summary.details[name] = { ...err, details: { ...err.details, optional: isOptional }, }; } }), ); if (!success) { throw new HealthCheckException(summary.details); } return summary; } iterateByIndicators(): [Constructor, IndicatorMetadata][] { const resources = this.rsdkMetadataProvider.get( HEALTH_INDICATOR_RSDK_METADATA_SCOPE, ); return resources.map((value) => [value.value.target, value.value.options]); } async onModuleInit(): Promise { this.logger.debug('Initializing indicators...'); const indicators = this.iterateByIndicators(); for (const [ctor, meta] of indicators) { const { key, scope } = meta; if (!this.indicators[scope]) { this.indicators[scope] = []; } const [instance] = this.resolve([ctor]); this.indicatorNames.set(instance, key); this.indicators[scope].push({ indicator: instance, meta }); } this.logger.info(`Indicators initialized!`); } private filterIndicators(filter: '*' | string = ''): IndicatorWithMetadata[] { switch (filter) { /** * This means readiness healthcheck which means that server itself is up, * but doesn't guarantee that it is ready to serve. */ case '': return []; /** * This means - "perform all checks you have" */ case '*': return [...this.indicators[DEFAULT_HEALTH_CHECK_SCOPE]]; /** * Matching exact filter. If no specific checks * found - common checks will be performed */ default: { const specific = this.indicators[filter]; if (!specific || specific.length === 0) { this.logger.warn(`Unsupported healthcheck filter: ${filter}`); } return [ ...this.indicators[DEFAULT_HEALTH_CHECK_SCOPE], ...(specific || []), ]; } } } private resolve( indicators: Readonly[]>, ): HealthIndicator[] { return indicators.map((x) => { if (typeof x !== 'function') { return x; } return this.moduleRef.get(x, { strict: false }); }); } }