[Nest.js]주기적으로 헬스체크 하기

Donghun Seol·2023년 4월 18일
0

나중에 알았지만 아래의 1 ~ 4번처럼 할 필요는 없었다.😂

베스트 프랙티스는 아래의 코드처럼 컨트롤러만 구현하고, terminus에서 제공해주는 HealthCheckService만 주입받은 다음 @Cron()만 붙이면 된다. 내가 아래에서 만들었던 서비스랑 내부 구현을 @nestjs/terminus에서 이미 제공해주고 있었다. (컨트롤러 인터페이스를 제공할 필요가 없으면 HealthCheckService를 주입받은 커스텀 서비스로 구현하는 방법도 가능하다.)


@Controller('healthCheck')
export class HealthCheckController {
  constructor(
    private health: HealthCheckService,
    private http: HttpHealthIndicator,
    private db: TypeOrmHealthIndicator,
    private dogs: DogHealthIndicator,
  ) {}

  @Cron('*/5 * * * * *')
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.http.pingCheck('nestjs-docs', 'https://docs.nestjs.rcom'),
      () => this.http.pingCheck('localhost', 'http://localhost:3000'),
      () => this.db.pingCheck('database'),
      () => this.dogs.isHealthy('dogs'),
    ]);
  }
}

컨트롤러 없이 서비스단에서 @Cron으로 호출하는 코드

import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import {
  HealthCheck,
  HealthCheckService,
  HttpHealthIndicator,
  TypeOrmHealthIndicator,
} from '@nestjs/terminus';
import { DogHealthIndicator } from './dog.health';

@Injectable()
export class MyHealthCheckService {
  constructor(
    private health: HealthCheckService,
    private http: HttpHealthIndicator,
    private db: TypeOrmHealthIndicator,
    private dogs: DogHealthIndicator,
  ) {}

  @Cron('*/5 * * * * *')
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.http.pingCheck('nestjs-docs', 'https://docs.nestjs.rcom'),
      () => this.http.pingCheck('localhost', 'http://localhost:3000'),
      () => this.db.pingCheck('database'), // 'database'는 헬스체크 응답객체의 키로 지정하는 부분.
      () => this.dogs.isHealthy('dogs'),
    ]);
  }
}

이전의 삽질과정

0. 목표

교재에서 배운 스케쥴러와 헬스체크를 조합해서 10초 간격으로 DB에 대한 헬스체크를 수행하는 기능을 만들어보자.

완성되면 아래와 같이 작동한다.
서버는 10초마다 DB에 대한 헬스체크를 수행하고,
DB가 죽으면 자동으로 빨간색 에러로그를 출력한다.
(물론 로그 출력 이외에 추가적인 로직도 구현가능하다.)

도커에서 DB 컨테이너를 중지시키면 에러로그가 콘솔에 출력된다

1. batch.module.ts

배치작업은 이 모듈에서 수행하므로 배치 모듈에 프로바이더를 다음과 같이 지정해준다.
Connection은 typeorm에서 제공해주는 객체인데, 클래스가 아니므로 커스텀 프로바이더로 선언해줘 주입가능하게 만들었다.

  providers: [
    HealthCheckService,
    HealthCheckScheduler,
    { provide: 'Connection', useValue: Connection }
  ]

2. healthCheck.service.ts

헬스체크 로직이 작성된 파일이다. Connection을 주입받아서 사용하는 것을 볼 수 있다.
이 서비스는 scheduler에서 @Cron() 데커레이터를 통해 주기적으로 호출된다.

import { Injectable, Logger } from '@nestjs/common';
import { Connection } from 'typeorm';

@Injectable()
export class HealthCheckService {
  constructor(
    private readonly connection: Connection,
    private logger: Logger,
  ) {}

  async check(): Promise<boolean> {
    try {
      await this.connection.query('SELECT 1');
      this.logger.log('DB OK');
      return true;
    } catch (error) {
      this.logger.log('DB DOWN,Call 911 !!!!', error.stack);
      return false;
    }
  }
}

3. healthCheck.scheduler.ts

실제로 서비스를 호출하는 스케쥴러 파일이다.
해당 클래스는 batch.module.ts에 프로바이더로 지정되었다.
프로바이더로 지정된 클래스는 네스트 런타임이 자동으로 인스턴스화 해준다.
그 과정에서 @Cron()으로 수식된 check()메서드가 주기적으로 호출된다.

import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class HealthCheckScheduler {
  constructor(private healthCheckService: HealthCheckService) {}

  @Cron(CronExpression.EVERY_10_SECONDS)
  async check(): Promise<boolean> {
    return this.healthCheckService.check();
  }
}

4. 응용

추가 헬스 체크

@nest/axios를 활용해서 http 요청을 주기적으로 날려 헬스체크를 하는 것도 가능하다.

먼저 service에 checkGoogle()이라는 메서드를 구현하고
scheduler에서 이를 주기적으로 호출하는 메서드를 추가한 뒤

@Cron(CronExpression.EVERY_HOUR)를 사용하면 매 시간마다 구글 서버의 상태를 주기적으로 체크할 수 있다.

import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Connection } from 'typeorm';

@Injectable()
export class HealthCheckScheduler {
  constructor(private healthCheckService: HealthCheckService) {}

  @Cron(CronExpression.EVERY_10_SECONDS)
  async check(): Promise<boolean> {
    return this.healthCheckService.checkDB();
  }
  // 한 시간마다 구글 서버가 죽었는지 확인하는 요청
  @Cron(CronExpression.EVERY_HOUR)
  async check(): Promise<boolean> {
    return this.healthCheckService.checkGoogle(); // 상세 로직은 서비스에다 구현하기
  }
}

헬스체크 서비스의 재사용

healthCheckService를 다른 서비스나 인터셉터에 주입해 활용하면 코드의 중복을 줄이면서도 재미있는 로직을 작성 가능하다. DB가 죽었을때는 정적으로 캐시된 응답만을 준다거나, 상세한 오류 메시지를 내보낼 수 있고, 어떤 DB가 죽었는지 체크하면 관리자 페이지의 UX를 향상 시킬 수 있을 것 같다.

profile
I'm going from failure to failure without losing enthusiasm

0개의 댓글