Health checks (Terminus)

Health checks (Terminus)

terminus๋Š” ์ •์ƒ์ ์ธ ์ข…๋ฃŒ์— ๋ฐ˜์‘ํ•˜๊ธฐ์œ„ํ•œ ํ›„ํฌ๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ ๋ชจ๋“  HTTP ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์— ๋Œ€ํ•œ ์ ์ ˆํ•œ Kubernetes readiness/liveness ํ™•์ธ์„ ์ƒ์„ฑํ•˜๋„๋ก ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. @nestjs/terminus ๋ชจ๋“ˆ์€ ํ„ฐ๋ฏธ๋„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ Nest ์—์ฝ” ์‹œ์Šคํ…œ๊ณผ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค.

Getting started

@nestjs/terminus๋ฅผ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ํ•„์š”ํ•œ ์˜์กด์„ฑ์„ ์„ค์น˜ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

$ npm install --save @nestjs/terminus @godaddy/terminus

Setting up a health check

์ƒํƒœ ํ™•์ธ์€ ์ƒํƒœ ํ‘œ์‹œ๊ธฐ์˜ ์š”์•ฝ์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ์ƒํƒœ ํ‘œ์‹œ๊ธฐ๋Š” ์„œ๋น„์Šค ์ƒํƒœ์— ๊ด€๊ณ„์—†์ด ์„œ๋น„์Šค ๊ฒ€์‚ฌ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ํ• ๋‹น๋œ ๋ชจ๋“  ์ƒํƒœ ํ‘œ์‹œ๊ธฐ๊ฐ€ ์ž‘๋™๋˜์–ด ์‹คํ–‰์ค‘์ธ ๊ฒฝ์šฐ ์ƒํƒœ ํ™•์ธ์€ ๊ธ์ •์ ์ž…๋‹ˆ๋‹ค. ๋งŽ์€ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์ด ์œ ์‚ฌํ•œ ์ƒํƒœ ํ‘œ์‹œ๊ธฐ๋ฅผ ํ•„์š”๋กœ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— @nestjs/terminus๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฏธ๋ฆฌ ์ •์˜๋œ ์ƒํƒœ ํ‘œ์‹œ๊ธฐ ์ง‘ํ•ฉ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • DNSHealthIndicator

  • TypeOrmHealthIndicator

  • MongooseHealthIndicator

  • MicroserviceHealthIndicator

  • MemoryHealthIndicator

  • DiskHealthIndicator

DNS Health Check

์ฒซ๋ฒˆ์งธ ์ƒํƒœ ํ™•์ธ์„ ์‹œ์ž‘ํ•˜๋Š” ์ฒซ๋ฒˆ์งธ ๋‹จ๊ณ„๋Š” ์ƒํƒœ ํ‘œ์‹œ๊ธฐ๋ฅผ ์—”๋“œ ํฌ์ธํŠธ์— ์—ฐ๊ฒฐํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

@@filename(terminus-options.service)
import {
  TerminusEndpoint,
  TerminusOptionsFactory,
  DNSHealthIndicator,
  TerminusModuleOptions
} from '@nestjs/terminus';
import { Injectable } from '@nestjs/common';

@Injectable()
export class TerminusOptionsService implements TerminusOptionsFactory {
  constructor(
    private readonly dns: DNSHealthIndicator,
  ) {}

  createTerminusOptions(): TerminusModuleOptions {
    const healthEndpoint: TerminusEndpoint = {
      url: '/health',
      healthIndicators: [
        async () => this.dns.pingCheck('google', 'https://google.com'),
      ],
    };
    return {
      endpoints: [healthEndpoint],
    };
  }
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { DNSHealthIndicator } from '@nestjs/terminus';

@Injectable()
@Dependencies(DNSHealthIndicator)
export class TerminusOptionsService {
  constructor(dns) {
    this.dns = dns;
  }

  createTerminusOptions() {
    const healthEndpoint = {
      url: '/health',
      healthIndicators: [
        async () => this.dns.pingCheck('google', 'https://google.com'),
      ],
    };
    return {
      endpoints: [healthEndpoint],
    };
  }
}

์ผ๋‹จ TerminusOptionsService๋ฅผ ์„ค์ •ํ•˜๋ฉด, TerminusModule์„ ๋ฃจํŠธ ApplicationModule๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. TerminusOptionsService๋Š” ์„ค์ •์„ ์ œ๊ณตํ•˜๋ฉฐ,์ด ์„ค์ •์€ TerminusModule์— ์˜ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

@@filename(app.module)
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { TerminusOptionsService } from './terminus-options.service';

@Module({
  imports: [
    TerminusModule.forRootAsync({
      useClass: TerminusOptionsService,
    }),
  ],
})
export class ApplicationModule { }

info ํžŒํŠธ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜ํ–‰๋˜๋ฉด Nest๋Š” GET ์š”์ฒญ์„ ํ†ตํ•ด ์ •์˜ ๋œ ๊ฒฝ๋กœ์— ๋„๋‹ฌ ํ•  ์ˆ˜์žˆ๋Š” ์ •์˜ ๋œ ์ƒํƒœ ์ ๊ฒ€์„ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด curl -X GET 'http://localhost:3000/health'

Custom health indicator

๊ฒฝ์šฐ์— ๋”ฐ๋ผ @nestjs/terminus์—์„œ ์ œ๊ณตํ•˜๋Š” ์‚ฌ์ „ ์ •์˜๋œ ์ƒํƒœ ํ‘œ์‹œ๊ธฐ๊ฐ€ ๋ชจ๋“  ์ƒํƒœ ํ™•์ธ ์š”๊ตฌ ์‚ฌํ•ญ์„ ๋‹ค๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ํ•„์š”์— ๋”ฐ๋ผ ์‚ฌ์šฉ์ž ์ •์˜ ์ƒํƒœ ํ‘œ์‹œ๊ธฐ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งž์ถคํ˜• ์ƒํƒœ ํ™•์ธ์„ ๋‚˜ํƒ€๋‚ด๋Š” ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ƒํƒœ ํ™•์ธ์ด ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋Š”์ง€์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ์ง€์‹์„ ์–ป๊ธฐ ์œ„ํ•ด DogHealthIndicator ์˜ˆ์ œ๋ฅผ ๋งŒ๋“ค ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  Dog ์˜ค๋ธŒ์ ํŠธ์— goodboy ์œ ํ˜•์ด ์žˆ๋Š” ๊ฒฝ์šฐ์ด Health ํ‘œ์‹œ๊ธฐ์˜ ์ƒํƒœ๋Š” "up"์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  Health ํ‘œ์‹œ๊ธฐ๋Š” "down"์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

@@filename(dog.health)
import { Injectable } from '@nestjs/common';
import { HealthCheckError } from '@godaddy/terminus';
import { HealthIndicatorResult } from '@nestjs/terminus';

export interface Dog {
  name: string;
  type: string;
}

@Injectable()
export class DogHealthIndicator extends HealthIndicator {
  private readonly dogs: Dog[] = [
    { name: 'Fido', type: 'goodboy' },
    { name: 'Rex', type: 'badboy' },
  ];

  async isHealthy(key: string): Promise<HealthIndicatorResult> {
    const badboys = this.dogs.filter(dog => dog.type === 'badboy');
    const isHealthy = badboys.length === 0;
    const result = this.getStatus(key, isHealthy, { badboys: badboys.length });

    if (isHealthy) {
      return result;
    }
    throw new HealthCheckError('Dogcheck failed', result);
  }
}
@@switch
import { Injectable } from '@nestjs/common';
import { HealthCheckError } from '@godaddy/terminus';

@Injectable()
export class DogHealthIndicator extends HealthIndicator {
  dogs = [
    { name: 'Fido', type: 'goodboy' },
    { name: 'Rex', type: 'badboy' },
  ];

  async isHealthy(key) {
    const badboys = this.dogs.filter(dog => dog.type === 'badboy');
    const isHealthy = badboys.length === 0;
    const result = this.getStatus(key, isHealthy, { badboys: badboys.length });

    if (isHealthy) {
      return result;
    }
    throw new HealthCheckError('Dogcheck failed', result);
  }
}

๋‹ค์Œ์œผ๋กœ ํ•ด์•ผ ํ•  ์ผ์€ ์ƒํƒœ ํ‘œ์‹œ๊ธฐ๋ฅผ ๊ณต๊ธ‰์ž๋กœ ๋“ฑ๋กํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

@@filename(app.module)
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { TerminusOptions } from './terminus-options.service';
import { DogHealthIndicator } from './dog.health.ts';

@Module({
  imports: [
    TerminusModule.forRootAsync({
      imports: [ApplicationModule],
      useClass: TerminusOptionsService,
    }),
  ],
  providers: [DogHealthIndicator],
  exports: [DogHealthIndicator],
})
export class ApplicationModule { }

info ํžŒํŠธ ์‹ค์ œ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์—์„œ DogHealthIndicator๋Š” ๋ณ„๋„์˜ ๋ชจ๋“ˆ (์˜ˆ: DogsModule)๋กœ ์ œ๊ณต๋˜์–ด์•ผํ•˜๋ฉฐ, ๊ทธ๋Ÿฐ ๋‹ค์Œ ApplicationModule์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ DogHealthIndicator๋ฅผ DogModule์˜ exports ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•˜๊ณ  TerminusModule.forRootAsync()ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ์ฒด์˜ imports ๋ฐฐ์—ด์— DogModule์„ ์ถ”๊ฐ€ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

๋งˆ์ง€๋ง‰์œผ๋กœ ํ•ด์•ผ ํ•  ์ผ์€ ํ•„์š”ํ•œ ์ƒํƒœ ์ ๊ฒ€ ์—”๋“œ ํฌ์ธํŠธ์— ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ํ‘œ์‹œ๊ธฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ์šฐ๋ฆฌ๋Š” TerminusOptionsService๋กœ ๋Œ์•„๊ฐ€์„œ/health ์—”๋“œ ํฌ์ธํŠธ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

@@filename(terminus-options.service)
import {
  TerminusEndpoint,
  TerminusOptionsFactory,
  DNSHealthIndicator,
  TerminusModuleOptions
} from '@nestjs/terminus';
import { Injectable } from '@nestjs/common';

@Injectable()
export class TerminusOptionsService implements TerminusOptionsFactory {
  constructor(
    private readonly dogHealthIndicator: DogHealthIndicator
  ) {}

  createTerminusOptions(): TerminusModuleOptions {
    const healthEndpoint: TerminusEndpoint = {
      url: '/health',
      healthIndicators: [
        async () => this.dogHealthIndicator.isHealthy('dog'),
      ],
    };
    return {
      endpoints: [healthEndpoint],
    };
  }
}
@@switch
import { DogHealthIndicator } from '../dog/dog.health';
import { Injectable, Dependencies } from '@nestjs/common';

@Injectable()
@Dependencies(DogHealthIndicator)
export class TerminusOptionsService {
  constructor(dogHealthIndicator) {
    this.dogHealthIndicator = dogHealthIndicator;
  }

  createTerminusOptions() {
    const healthEndpoint = {
      url: '/health',
      healthIndicators: [
        async () => this.dogHealthIndicator.isHealthy('dog'),
      ],
    };
    return {
      endpoints: [healthEndpoint],
    };
  }
}

๋ชจ๋“  ๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์™„๋ฃŒ ๋˜์—ˆ๋‹ค๋ฉด/health ์—”๋“œ ํฌ์ธํŠธ๋Š” 503 ์‘๋‹ต ์ฝ”๋“œ์™€ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋กœ ์‘๋‹ตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

{
  "status": "error",
  "error": {
    "dog": {
      "status": "down",
      "badboys": 1
    }
  }
}

@nestjs/terminus ์ €์žฅ์†Œ์—์„œ ์‹ค์ œ ์˜ˆ์ œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Custom Logger

Terminus ๋ชจ๋“ˆ์€ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ ๋™์•ˆ ๋ชจ๋“  ์˜ค๋ฅ˜๋ฅผ ์ž๋™์œผ๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ์ „์—ญ ์ ์œผ๋กœ ์ •์˜ ๋œ Nest ๋กœ๊ฑฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ธ€๋กœ๋ฒŒ ๋กœ๊ฑฐ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ ๋กœ๊ฑฐ ์ฑ•ํ„ฐ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฒฝ์šฐ์— ๋”ฐ๋ผTerminus์˜ ๋กœ๊ทธ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ TerminusModule.forRoot[Async]ํ•จ์ˆ˜๋Š” ์ปค์Šคํ…€ ๋กœ๊ฑฐ๋ฅผ์œ„ํ•œ ์˜ต์…˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

TerminusModule.forRootAsync({
  logger: (message: string, error: Error) => console.error(message, error),
  endpoints: [
    ...
  ]
})

๋กœ๊ฑฐ ์˜ต์…˜์„ null๋กœ ์„ค์ •ํ•˜์—ฌ ๋กœ๊ฑฐ๋ฅผ ๋น„ํ™œ์„ฑํ™” ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

TerminusModule.forRootAsync({
  logger: null,
  endpoints: [
    ...
  ]
})

Last updated

Was this helpful?