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
수준의 로그를 찍어보겠습니다.
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
수준의 로그를 찍어볼까요?
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)를 함께 출력되도록 로거를 확장해보겠습니다.
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
옵션으로 확장한 로거 클래스의 인스턴스를 넘겨주면 되겠습니다.
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
전역 객체를 사용하여 로거를 구현해보겠습니다.
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);
}
}
이 로거 서비스 클래스는 외부 라이브러리에 의존하지 하지 않기 때문에 굳이 그럴 필요는 없지만 완전한 예제를 위해서 별도의 모듈에 담아서 제공하도록 하겠습니다.
import { Module } from "@nestjs/common";
import { LoggerService } from "./logger.service";
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}
이렇게 로거를 별도의 모듈로 제공하는 경우에는 로거의 인스턴스를 직접 생성하는 대신에 다른 일반 서비스처럼 NestJS의 DI(의존성 주입)을 사용해야합니다.
즉, AppModule
모듈에서 LoggerModule
모듈을 불러와야겠습니다.
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 앱이 구동되는 초반에 잠시동안 내장 로거가 사용될 수 있기 때문입니다.
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에 관련된 다른 포스팅은 관련 태그를 참고 바라겠습니다.