Browse Source

feat(core): Make all health checks configurable

Fixes #1494
Michael Bromley 3 years ago
parent
commit
f3d2d59bbd

+ 2 - 0
packages/core/src/config/config.module.ts

@@ -82,6 +82,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
         const { customPaymentProcess } = this.configService.paymentOptions;
         const { entityIdStrategy: entityIdStrategyDeprecated } = this.configService;
         const { entityIdStrategy } = this.configService.entityOptions;
+        const { healthChecks } = this.configService.systemOptions;
         return [
             ...adminAuthenticationStrategy,
             ...shopAuthenticationStrategy,
@@ -107,6 +108,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             ...customPaymentProcess,
             stockAllocationStrategy,
             stockDisplayStrategy,
+            ...healthChecks,
         ];
     }
 

+ 5 - 0
packages/core/src/config/config.service.ts

@@ -19,6 +19,7 @@ import {
     PromotionOptions,
     RuntimeVendureConfig,
     ShippingOptions,
+    SystemOptions,
     TaxOptions,
     VendureConfig,
 } from './vendure-config';
@@ -110,4 +111,8 @@ export class ConfigService implements VendureConfig {
     get jobQueueOptions(): Required<JobQueueOptions> {
         return this.activeConfig.jobQueueOptions;
     }
+
+    get systemOptions(): Required<SystemOptions> {
+        return this.activeConfig.systemOptions;
+    }
 }

+ 4 - 0
packages/core/src/config/default-config.ts

@@ -32,6 +32,7 @@ import { defaultPromotionActions, defaultPromotionConditions } from './promotion
 import { InMemorySessionCacheStrategy } from './session-cache/in-memory-session-cache-strategy';
 import { defaultShippingCalculator } from './shipping-method/default-shipping-calculator';
 import { defaultShippingEligibilityChecker } from './shipping-method/default-shipping-eligibility-checker';
+import { TypeORMHealthCheckStrategy } from '../health-check/typeorm-health-check-strategy';
 import { DefaultTaxLineCalculationStrategy } from './tax/default-tax-line-calculation-strategy';
 import { DefaultTaxZoneStrategy } from './tax/default-tax-zone-strategy';
 import { RuntimeVendureConfig } from './vendure-config';
@@ -183,4 +184,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         Zone: [],
     },
     plugins: [],
+    systemOptions: {
+        healthChecks: [new TypeORMHealthCheckStrategy()],
+    },
 };

+ 1 - 0
packages/core/src/config/index.ts

@@ -56,6 +56,7 @@ export * from './shipping-method/default-shipping-calculator';
 export * from './shipping-method/default-shipping-eligibility-checker';
 export * from './shipping-method/shipping-calculator';
 export * from './shipping-method/shipping-eligibility-checker';
+export * from './system/health-check-strategy';
 export * from './tax/default-tax-line-calculation-strategy';
 export * from './tax/default-tax-zone-strategy';
 export * from './tax/tax-line-calculation-strategy';

+ 49 - 0
packages/core/src/config/system/health-check-strategy.ts

@@ -0,0 +1,49 @@
+import { HealthIndicatorFunction } from '@nestjs/terminus';
+
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+
+/**
+ * @description
+ * This strategy defines health checks which are included as part of the
+ * `/health` endpoint. They should only be used to monitor _critical_ systems
+ * on which proper functioning of the Vendure server depends.
+ *
+ * For more information on the underlying mechanism, see the
+ * [NestJS Terminus module docs](https://docs.nestjs.com/recipes/terminus).
+ *
+ * Custom strategies should be added to the `systemOptions.healthChecks` array.
+ * By default, Vendure includes the `TypeORMHealthCheckStrategy`, so if you set the value of the `healthChecks`
+ * array, be sure to include it manually.
+ *
+ * Vendure also ships with the {@link HttpHealthCheckStrategy}, which is convenient
+ * for adding a health check dependent on an HTTP ping.
+ *
+ *
+ *
+ * @example
+ * ```TypeScript
+ * import { HttpHealthCheckStrategy, TypeORMHealthCheckStrategy } from '\@vendure/core';
+ * import { MyCustomHealthCheckStrategy } from './config/custom-health-check-strategy';
+ *
+ * export const config = {
+ *   // ...
+ *   systemOptions: {
+ *     healthChecks: [
+ *       new TypeORMHealthCheckStrategy(),
+ *       new HttpHealthCheckStrategy({ key: 'my-service', url: 'https://my-service.com' }),
+ *       new MyCustomHealthCheckStrategy(),
+ *     ],
+ *   },
+ * };
+ * ```
+ *
+ * @docsCategory health-check
+ */
+export interface HealthCheckStrategy extends InjectableStrategy {
+    /**
+     * @description
+     * Should return a `HealthIndicatorFunction`, as defined by the
+     * [NestJS Terminus module](https://docs.nestjs.com/recipes/terminus).
+     */
+    getHealthIndicator(): HealthIndicatorFunction;
+}

+ 28 - 0
packages/core/src/config/vendure-config.ts

@@ -41,6 +41,7 @@ import { PromotionCondition } from './promotion/promotion-condition';
 import { SessionCacheStrategy } from './session-cache/session-cache-strategy';
 import { ShippingCalculator } from './shipping-method/shipping-calculator';
 import { ShippingEligibilityChecker } from './shipping-method/shipping-eligibility-checker';
+import { HealthCheckStrategy } from './system/health-check-strategy';
 import { TaxLineCalculationStrategy } from './tax/tax-line-calculation-strategy';
 import { TaxZoneStrategy } from './tax/tax-zone-strategy';
 
@@ -880,6 +881,25 @@ export interface EntityOptions {
     metadataModifiers?: EntityMetadataModifier[];
 }
 
+/**
+ * @description
+ * Options relating to system functions.
+ *
+ * @since 1.6.0
+ * @docsCategory configuration
+ */
+export interface SystemOptions {
+    /**
+     * @description
+     * Defines an array of {@link HealthCheckStrategy} instances which are used by the `/health` endpoint to verify
+     * that any critical systems which the Vendure server depends on are also healthy.
+     *
+     * @default [TypeORMHealthCheckStrategy]
+     * @since 1.6.0
+     */
+    healthChecks?: HealthCheckStrategy[];
+}
+
 /**
  * @description
  * All possible configuration options are defined by the
@@ -1001,6 +1021,13 @@ export interface VendureConfig {
      * Configures how the job queue is persisted and processed.
      */
     jobQueueOptions?: JobQueueOptions;
+    /**
+     * @description
+     * Configures system options
+     *
+     * @since 1.6.0
+     */
+    systemOptions?: SystemOptions;
 }
 
 /**
@@ -1023,6 +1050,7 @@ export interface RuntimeVendureConfig extends Required<VendureConfig> {
     promotionOptions: Required<PromotionOptions>;
     shippingOptions: Required<ShippingOptions>;
     taxOptions: Required<TaxOptions>;
+    systemOptions: Required<SystemOptions>;
 }
 
 type DeepPartialSimple<T> = {

+ 7 - 1
packages/core/src/health-check/health-check-registry.service.ts

@@ -14,7 +14,13 @@ import { HealthIndicatorFunction } from '@nestjs/terminus';
  * Plugins which rely on external services (web services, databases etc.) can make use of this
  * service to add a check for that dependency to the Vendure health check.
  *
- * To use it in your plugin, you'll need to import the {@link PluginCommonModule}:
+ *
+ * Since v1.6.0, the preferred way to implement a custom health check is by creating a new
+ * {@link HealthCheckStrategy} and then passing it to the `systemOptions.healthChecks` array.
+ * See the {@link HealthCheckStrategy} docs for an example configuration.
+ *
+ * The alternative way to register a health check is by injecting this service directly into your
+ * plugin module. To use it in your plugin, you'll need to import the {@link PluginCommonModule}:
  *
  * @example
  * ```TypeScript

+ 7 - 4
packages/core/src/health-check/health-check.module.ts

@@ -1,5 +1,5 @@
 import { Module } from '@nestjs/common';
-import { TerminusModule, TypeOrmHealthIndicator } from '@nestjs/terminus';
+import { TerminusModule } from '@nestjs/terminus';
 
 import { ConfigModule } from '../config/config.module';
 import { ConfigService } from '../config/config.service';
@@ -20,11 +20,14 @@ export class HealthCheckModule {
     constructor(
         private configService: ConfigService,
         private healthCheckRegistryService: HealthCheckRegistryService,
-        private typeOrm: TypeOrmHealthIndicator,
         private worker: WorkerHealthIndicator,
     ) {
-        // Register the default health checks for database and worker
-        this.healthCheckRegistryService.registerIndicatorFunction([() => this.typeOrm.pingCheck('database')]);
+        // Register all configured health checks
+        for (const strategy of this.configService.systemOptions.healthChecks) {
+            this.healthCheckRegistryService.registerIndicatorFunction(strategy.getHealthIndicator());
+        }
+
+        // TODO: Remove in v2
         const { enableWorkerHealthCheck, jobQueueStrategy } = this.configService.jobQueueOptions;
         if (enableWorkerHealthCheck && isInspectableJobQueueStrategy(jobQueueStrategy)) {
             this.healthCheckRegistryService.registerIndicatorFunction([() => this.worker.isHealthy()]);

+ 47 - 0
packages/core/src/health-check/http-health-check-strategy.ts

@@ -0,0 +1,47 @@
+import { HealthIndicatorFunction, HttpHealthIndicator } from '@nestjs/terminus';
+
+import { Injector } from '../common/index';
+import { HealthCheckStrategy } from '../config/system/health-check-strategy';
+
+let indicator: HttpHealthIndicator;
+
+export interface HttpHealthCheckOptions {
+    key: string;
+    url: string;
+    timeout?: number;
+}
+
+/**
+ * @description
+ * A {@link HealthCheckStrategy} used to check health by pinging a url. Internally it uses
+ * the [NestJS HttpHealthIndicator](https://docs.nestjs.com/recipes/terminus#http-healthcheck).
+ *
+ * @example
+ * ```TypeScript
+ * import { HttpHealthCheckStrategy, TypeORMHealthCheckStrategy } from '\@vendure/core';
+ *
+ * export const config = {
+ *   // ...
+ *   systemOptions: {
+ *     healthChecks: [
+ *       new TypeORMHealthCheckStrategy(),
+ *       new HttpHealthCheckStrategy({ key: 'my-service', url: 'https://my-service.com' }),
+ *     ]
+ *   },
+ * };
+ * ```
+ *
+ * @docsCategory health-check
+ */
+export class HttpHealthCheckStrategy implements HealthCheckStrategy {
+    constructor(private options: HttpHealthCheckOptions) {}
+
+    async init(injector: Injector) {
+        indicator = await injector.get(HttpHealthIndicator);
+    }
+
+    getHealthIndicator(): HealthIndicatorFunction {
+        const { key, url, timeout } = this.options;
+        return () => indicator.pingCheck(key, url, { timeout });
+    }
+}

+ 2 - 0
packages/core/src/health-check/index.ts

@@ -1,2 +1,4 @@
 export * from './constants';
 export * from './health-check-registry.service';
+export * from './typeorm-health-check-strategy';
+export * from './http-health-check-strategy';

+ 48 - 0
packages/core/src/health-check/typeorm-health-check-strategy.ts

@@ -0,0 +1,48 @@
+import { HealthIndicatorFunction, TypeOrmHealthIndicator } from '@nestjs/terminus';
+
+import { Injector } from '../common/index';
+import { HealthCheckStrategy } from '../config/system/health-check-strategy';
+
+let indicator: TypeOrmHealthIndicator;
+
+export interface TypeORMHealthCheckOptions {
+    key?: string;
+    timeout?: number;
+}
+
+/**
+ * @description
+ * A {@link HealthCheckStrategy} used to check the health of the database. This health
+ * check is included by default, but can be customized by explicitly adding it to the
+ * `systemOptions.healthChecks` array:
+ *
+ * @example
+ * ```TypeScript
+ * import { TypeORMHealthCheckStrategy } from '\@vendure/core';
+ *
+ * export const config = {
+ *   // ...
+ *   systemOptions: [
+ *     // The default key is "database" and the default timeout is 1000ms
+ *     // Sometimes this is too short and leads to false negatives in the
+ *     // /health endpoint.
+ *     new TypeORMHealthCheckStrategy({ key: 'postgres-db', timeout: 5000 }),
+ *   ]
+ * }
+ * ```
+ *
+ * @docsCategory health-check
+ */
+export class TypeORMHealthCheckStrategy implements HealthCheckStrategy {
+    constructor(private options?: TypeORMHealthCheckOptions) {}
+
+    async init(injector: Injector) {
+        indicator = await injector.resolve(TypeOrmHealthIndicator);
+    }
+
+    getHealthIndicator(): HealthIndicatorFunction {
+        const key = this.options?.key || 'database';
+        const timeout = this.options?.timeout ?? 1000;
+        return () => indicator.pingCheck(key, { timeout });
+    }
+}