BackEnd/Nest.js

[Nest.js] 헬스 체크

Grace 2023. 6. 1. 16:34

서비스를 운영하다 보면 트래픽이 늘어나거나 DB에 부하가 생기기도 하고 기간 통신망이 끊기게 되는 경우도 발생합니다. 장애는 어느 레이어에서든 발생할 수 있고 사용자의 불편을 줄이기 위해 신속하게 장애에 대응하는 게 필요합니다. 그러려면 현재 서비스가 건강한 상태인지 항상 체크하고 있어야 할 장치가 필요합니다. 이를 헬스 체크라고 부릅니다.

서버는 HTTP, DB, 메모리, 디스크 상태 등을 체크하는 헬스 체크 장치가 있어야 합니다. 만약 서버가 건강하지 않은 상태가된다면 즉시 이를 사내 메신저 등을 통해 담당자에게 알려야 합니다. 헬스 체크와 함께 쌓아둔 에러 로그를 기반으로 종합적으로 모니터링 전략을 세워야 합니다.

Nest는 Terminus(@nestjs/terminus) 헬스 체크 라이브러리를 제공합니다. Terminus는 상태 표시기를 제공하며 필요하다면 직접 만들어서 사용할 수도 있습니다. @nestjs/terminus 패키지에서 제공하는 상태 표시기는 다음과 같습니다.

  • HttpHealthIndicator
  • MongooseHealthIndicator
  • TypeOrmHealthIndicator
  • SequelizeHealthIndicator
  • MicroserviceHealthIndicator
  • MemoryHealthIndicator
  • CRPCHealthIndicator
  • DiskHealthIndicator

Terminus 적용

$ npm i @nestjs/terminus

상태확인은 특정 라우터 엔드포인트로 요청을 보내고 응답을 확인하는 방법을 사용합니다. 이를 위한 HealthChechController 컨트롤러를 생성합니다. TerminusModule과 생성한 컨트롤러를 실행할 수 있도록 준비합니다.

$ nest g controller health-check
import { TerminusModule } from '@nestjs/terminus';
import { HealthCheckController } from './health-check/health-check.controller'
...

@Module({
  imports: [TerminusModule],
  providers: [HealthCheckController],
  ...
})
export class AppModule {}

헬스 체크

HttpHealthIndicator는 동작 과정에서 @nestjs/axios 에서 제공하는 HttpModule을 필요로 합니다.

$ npm i @nestjs/axios
import { HttpModule } from '@nestjs/axios';
import { TerminusModule } from '@nestjs/terminus';
import { HealthCheckController } from './health-check/health-check.controller'
...

@Module({
  imports: [TerminusModule, HttpModule],
  providers: [HealthCheckController],
  ...
})
export class AppModule {}
import { Controller, Get } from '@nestjs/common';
import { HealthCheckService, HttpHealthIndicator, HealthCheck } from '@nest/terminus';

@Controller('health-check')
export class HealthCheckController {
  constructor(
    private health: HealthCheckService,
    private http: HttpHealthIndicator
  ) { }
  
  @Get()
  @HttpCheck()
  check() {
    return this.health.check([
      () => this.http.pingCheck('nestjs-docs', 'https://docs.nestjs.com')
    ])
  }
}

HttpHealthIndicator가 제공하는 pingCheck 함수를 이용하여 서비스가 제공하는 다른 서버가 잘 동작하고 있는지 확인합니다.

헬스 체크 요청에 대한 응답은 HealthCheckResult 타입을 가지고 있습니다.

export interface HealthCheckResult {
  // 헬스 체크를 수행한 전반적인 상태. 'error' | 'ok' | 'shutting_down' 값을 가짐
  status: HEalthCheckStatue;
  
  // 상태가 "up"일 때의 상태 정보
  info?: HealthIndicatorResult;
  
  // 상태가 "down"일 때의 상태 정보
  error?: HealthIndicatorResult;
  
  // 모든 상태 표시기의 정보
  details: HealthIndicatorResult;
}

TypeOrm 헬스 체크

TypeOrmHealthIndicator는 단순히 DB가 잘 살아 있는지 확인합니다.

import { Controller, Get } from '@nestjs/common';
import { HealthCheckService, HttpHealthIndicator, HealthCheck, TypeOrmHealthIndicator } 
from '@nestjs/terminus';

@Controller('health-check')
export class HealthCheckController {
  constructor(
    private health: HealthCheckService,
    private http: HttpHealthIndicator,
    private db: TypeOrmHealthIndicator, // 컨트롤러에 TypeOrmHealthIndicator 주입
  ) { }
  
  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.http.pingCheck('nestjs-docs', 'https://docs.nestjs.com')
      () => this.db.pingCheck('database') // 헬스 체크 리스트에 DB 헬스 체크 추가
    ])
  }
}

커스텀 상태 표시기

@nestjs/terminus에서 제공하지 않는 상태 표시기가 필요하다면 HealthIndicator를 상속받는 상태 표시기를 직접 만들 수 있습니다.

export declare abstract class HealthIndicator {
  protected getStatus(key: string, isHealthy: boolean, data?: {
    [key: string]: any
  }): HealthIndicatorResult;
}

HealthIndicatorHealthIndicatorResult를 리턴하는 getStatus 메서드를 가지고 있습니다. 이 메서드에서 상태를 나타내는 key, 상태 표시기가 상태를 측정한 결과인 isHealthy, 그리고 결과에 포함시킬 데이터를 인수로 넘깁니다.

import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from 
'@nestjs/terminus';

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

@Injectable()
export class DogHealthIndicator extends HealthIndicator {
  private dogs: Dog[] = [ // 강아지들의 상태 하드 코딩
    { name: 'Fido', type: 'goodboy'},
    { name: 'Rex', type: 'badboy'},
  ]
  
  async isHealthy(key: string): Promise<HealthIndicatorResult> {
    // 강아지의 상태가 badboy인 강아지가 있으면 HealthCheckError
    const badboys  = this.dogs.filter(dog => dog.type === 'bodboy')
    const isHealthy = badboys.legnth === 0;
    
    const result = this.getStatus(key, isHealthy, { badboys: badboys.length })
    
    if(isHealthy) return result;
    throw new HealthCheckError('Dogcheck failed', result )
  }
}
import { Controller, Get } from '@nestjs/common';
import { HealthCheckService, HttpHealthIndicator, HealthCheck, TypeOrmHealthIndicator }
from '@nestjs/terminus';
import { DogHealthIndicator } from './dog.health';

@Controller('health-check')
export class HealthCheckController {
  constructor (
    private health: HealthCHeckService,
    private http: HttpHealthIndicator,
    private db: TypeOrmHealthIndicator,
    private dogHealthIndicator: DogHealthIndicator,
  ) { }
  
  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.http.pingCheck('nestjs-docs', 'https://docs.nestjs.com')
      () => this.db.pingCheck('database')
      () => this.dogHealthIndicator,isHealthy('dog')
    ])
  }
}

'BackEnd > Nest.js' 카테고리의 다른 글

[Nest.js] 태스크 스케줄링  (0) 2023.05.31
[Nest.js] 인터셉터  (0) 2023.05.31
[Nest.js] 예외 필터  (0) 2023.05.30
[Nest.js] 로깅  (0) 2023.05.30
[Nest.js] JWT 인증/인가  (0) 2023.05.26