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?