Logo

NestJS에서 로깅(logging)하기

로깅(logging)은 애플리케이션에서 발생하는 각종 이벤트에 대한 기록을 남기고 문제 발생 시 원인을 파악하는데 핵심적인 역할을 하는데요. 이번 포스팅에서는 NestJS 앱에서 어떻게 로거(logger)를 사용하고 커스터마이징(customizing)할 수 있는지 알아보겠습니다.

실습 프로젝트 구성

먼저 간단한 실습을 위해서 NestJS 프로젝트가 하나 필요할 것 같은데요. 터미널에서 NestJS CLI 도구의 nest new 명령어를 실행하여 새로운 프로젝트를 구성하도록 하겠습니다.

$ nest new our-nestjs
⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? (Use arrow keys)npm
  yarn
  pnpm

NestJS CLI를 설치하고 NestJS 프로젝트를 구성하는 기본적인 방법은 관련 포스팅을 참고 바랍니다.

로거 사용하기

NestJS는 기본적으로 로거가 내장되어 있으며 서비스 클래스나 컨트롤러 클래스에서 로거를 매우 쉽게 사용할 수 있는데요. 단순히 @nestjs/common 패키지에서 Logger 클래스를 불러온 후 인스턴스를 생성한 다음에 log()warn(), debug()와 같은 메서드를 호출하면 됩니다.

이 때 Logger() 생성자의 인자로 <클래스명>.name을 넘기주면 로거의 컨텍스트(context)가 클래스 이름으로 설정됩니다. 이렇게 해주면 나중에 로그를 확인할 때 어느 클래스에서 찍히는 로그인지를 파악할 수 있어서 도움이 됩니다.

보통 Logger 클래스의 인스턴스는 해당 클래스의 logger 속성에 할당해놓고 여러 메서드에서 접근하는 것이 관례인데요. 예를 들어, AppService 클래스의 getHello() 메서드 안에서 debug 수준의 로그를 찍어보겠습니다.

src/app.service.ts
import { Injectable, Logger } from "@nestjs/common";

@Injectable()
export class AppService {
  private readonly logger = new Logger(AppService.name);

  getHello(): string {
    this.logger.debug("Logging...");
    return "Hello World!";
  }
}

이번에는 내부적으로 AppService 클래스의 getHello() 메서드를 호출하는 AppController 클래스의 getHello() 메서드 안에서 warn 수준의 로그를 찍어볼까요?

src/app.controller.ts
import { Controller, Get, Logger } from "@nestjs/common";
import { AppService } from "./app.service";

@Controller()
export class AppController {
  private readonly logger = new Logger(AppController.name);

  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    this.logger.warn("Logging...");
    return this.appService.getHello();
  }
}

이제 NestJS 앱을 구동하고 GET / 엔드포인트를 호출해보면 다음과 같은 로그가 찍히는 것을 확인할 수 있을 것입니다.

[Nest] 14  - 01/11/2023, 8:23:41 PM     LOG [NestFactory] Starting Nest application...
[Nest] 14  - 01/11/2023, 8:23:41 PM     LOG [InstanceLoader] LoggerModule dependencies initialized +1ms
[Nest] 14  - 01/11/2023, 8:23:41 PM     LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 14  - 01/11/2023, 8:23:41 PM     LOG [RoutesResolver] AppController {/}: +0ms
[Nest] 14  - 01/11/2023, 8:23:41 PM     LOG [RouterExplorer] Mapped {/, GET} route +0ms
[Nest] 14  - 01/11/2023, 8:23:41 PM     LOG [NestApplication] Nest application successfully started +0ms
[Nest] 14  - 01/11/2023, 8:23:41 PM    WARN [AppController] Logging...[Nest] 14  - 01/11/2023, 8:23:41 PM   DEBUG [AppService] Logging...

Logger 클래스의 인스턴스를 생성할 때 클래스 이름을 컨텍스트로 설정해줬기 때문에 로그 메시지 앞에 클래스 이름이 함께 출력이 되는데요. 이를 통해 로그 메시지가 동일하더라도 AppController 클래스에서 찍히는 건지 AppService 클래스에서 찍히는 건지 쉽게 알아낼 수 있습니다.

로거 확장하기

만약에 로그를 남기는 방식에 변화를 주고 싶다면 NestJS에 내장된 기본 로거를 어렵지 않게 확장할 수 있습니다. @nestjs/common 패키지의 ConsoleLogger 클래스를 확장(extend)하여 원하는 메서드만 오버라이드(override)해주면 됩니다.

예를 들어, debug 수준과 warn 수준에서 로그가 찍힐 때 메시지 앞에 특정 이모지(emoji)를 함께 출력되도록 로거를 확장해보겠습니다.

src/my-logger.ts
import { ConsoleLogger, Injectable } from "@nestjs/common";

@Injectable()
export class MyLogger extends ConsoleLogger {
  debug(message: any, ...optionalParams: any[]) {
    super.debug(`🐛 ${message}`, ...optionalParams);
  }

  warn(message: any, ...optionalParams: any[]) {
    super.warn(`🚨 ${message}`, ...optionalParams);
  }
}

이렇게 확장한 로거는 NestJS 앱에 설정을 해줘야 기본 로거 대신에 사용이 되는데요. 이 부분은 애플리케이션의 진입 지점(entry point)인 main.ts 파일에서 해줄 수 있습니다.

NestFactory.create()를 호출할 때 logger 옵션으로 확장한 로거 클래스의 인스턴스를 넘겨주면 되겠습니다.

src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { MyLogger } from "./my-logger";
async function bootstrap() {
  const app = await NestFactory.create(AppModule, {    logger: new MyLogger(),  });  await app.listen(3000);
}
bootstrap();

NestJS 앱을 재구동 후에 GET / 엔드포인트를 호출해보면 로그가 이모지와 함께 찍히는 것을 확인할 수 있을 것입니다.

[Nest] 40  - 01/11/2023, 8:41:15 PM     LOG [NestFactory] Starting Nest application...
[Nest] 40  - 01/11/2023, 8:41:15 PM     LOG [InstanceLoader] AppModule dependencies initialized
[Nest] 40  - 01/11/2023, 8:41:15 PM     LOG [RoutesResolver] AppController {/}:
[Nest] 40  - 01/11/2023, 8:41:15 PM     LOG [RouterExplorer] Mapped {/, GET} route
[Nest] 40  - 01/11/2023, 8:41:15 PM     LOG [NestApplication] Nest application successfully started
[Nest] 40  - 01/11/2023, 8:41:17 PM    WARN [AppController] 🚨 Logging...[Nest] 40  - 01/11/2023, 8:41:17 PM   DEBUG [AppService] 🐛 Logging...

로거 구현하기

어느 정도 규모가 있는 프로젝트에서는 로거를 완전히 입맛에 맞게 밑바닥부터 새롭게 구현해야하는 경우가 생기기 마련인데요. 이럴 때는 @nestjs/common 패키지의 LoggerService 인터페이스를 구현(implement)하는 경우가 많습니다.

이 인테페이스는 총 6개의 메서드가 있는데요. 이 중에서 log, warn, error는 반드시 구현해줘야 하고, debug, verbose는 선택적으로 구현이 가능합니다.

상용 애플리케이션이라면 Winston과 같은 외부 라이브러리를 도입하는 것을 고려하겠지만, 본 실습에서는 간단한 코드를 위해서 자바스크립트의 console 전역 객체를 사용하여 로거를 구현해보겠습니다.

src/logger/logger.service.ts
import { Injectable, LoggerService as NestLoggerService } from "@nestjs/common";

@Injectable()
export class LoggerService implements NestLoggerService {
  debug(message: any, ...optionalParams: any[]) {
    console.debug(`🐛 ${message}`, ...optionalParams);
  }

  warn(message: any, ...optionalParams: any[]) {
    console.warn(`🚨 ${message}`, ...optionalParams);
  }

  log(message: any, ...optionalParams: any[]) {
    console.log(`🪵 ${message}`, ...optionalParams);
  }

  error(message: any, ...optionalParams: any[]) {
    console.error(`💥 ${message}`, ...optionalParams);
  }
}

이 로거 서비스 클래스는 외부 라이브러리에 의존하지 하지 않기 때문에 굳이 그럴 필요는 없지만 완전한 예제를 위해서 별도의 모듈에 담아서 제공하도록 하겠습니다.

src/logger/logger.module.ts
import { Module } from "@nestjs/common";
import { LoggerService } from "./logger.service";

@Module({
  providers: [LoggerService],
  exports: [LoggerService],
})
export class LoggerModule {}

이렇게 로거를 별도의 모듈로 제공하는 경우에는 로거의 인스턴스를 직접 생성하는 대신에 다른 일반 서비스처럼 NestJS의 DI(의존성 주입)을 사용해야합니다. 즉, AppModule 모듈에서 LoggerModule 모듈을 불러와야겠습니다.

src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LoggerModule } from './logger/logger.module';

@Module({
  controllers: [AppController],
  providers: [AppService],
  imports: [LoggerModule],
})
export class AppModule {}

그리고 NestFactory.create() 함수를 호출할 때 bufferLogs 옵션을 반드시 true로 설정해주는 것이 중요한데요. 이렇게 해주지 않으면 NestJS 앱이 구동되는 초반에 잠시동안 내장 로거가 사용될 수 있기 때문입니다.

src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { LoggerService } from "./logger/logger.service";
async function bootstrap() {
  const app = await NestFactory.create(AppModule, {    bufferLogs: true,  });  app.useLogger(app.get(LoggerService));  await app.listen(3000);
}
bootstrap();

이제 다시 NestJS 앱을 구동하고 GET / 엔드포인트를 호출해보면 로그가 다음과 같이 찍히는 것을 볼 수 있습니다.

🪵 Starting Nest application... NestFactory
🪵 LoggerModule dependencies initialized InstanceLoader
🪵 AppModule dependencies initialized InstanceLoader
🪵 AppController {/}: RoutesResolver
🪵 Mapped {/, GET} route RouterExplorer
🪵 Nest application successfully started NestApplication
🚨 Logging... AppController🐛 Logging... AppService

기존에 출력이 되던 로깅 레벨이나 날짜/시간, 컨텍스트 정보가 모두 생략되고 단순히 이모지와 로그 메시지만 출력되는 것을 볼 수 있습니다.

전체 코드

실습 프로젝트의 코드는 아래에서 직접 확인하고 실행해볼 수 있습니다.

마치면서

이상으로 간단한 실습을 통해서 NestJS에서 어떻게 로깅을 할 수 있는지에 대해서 살펴보았습니다. 확장이 용이한 NestJS의 로깅 매커니즘을 활용하시는데 도움이 되었으면 좋겠습니다.

NestJS에 관련된 다른 포스팅은 관련 태그를 참고 바라겠습니다.