본문 바로가기

Backend/NestJS

[NestJS] Logger(Custom, Winston)

728x90
NestJS 공식문서와 서적을 참고한 logging 공부 기록

개요

서비스에 기능이 늘어나고 사이즈가 커지면 동작 과정을 남기고 추적하는 일이 중요하게 된다.

이슈 발생 시에 이슈 현상만으로 원인을 파악하는 데에 시간과 노력이 많이 들고, 코드를 역추적해야 하기 때문에 이해하는데 어려움도 따르기 때문이다.

 

때문에 이슈가 발생한 지점과 콜 스택이 함께 제공이 되어야 빠른 이슈 해결이 가능하다.

또 어떤 기능이 많이 사용되는지 등과 같은 사용 패턴 분석하는데 로그가 활용된다.

 

NestJS는 위의 요구사항을 만족시켜 줄 Logger 클래스를 제공한다.

NestJS 공식문서에 기재된 Logger 클래스가 제공하는 기능은 아래와 같다.

  • 로깅 비활성화
  • 로그 레벨 지정: log, error, warn, debug, verbose
  • 로거의 타임스탬프 재정의
  • 기본 로거를 재정의(오버라이딩)
  • 기본 로거를 확장해서 커스텀 로거를 작성
  • 의존성 주입을 통해 손쉽게 로거를 주입하거나 테스트 모듈로 제공

기본 커스텀

먼저 logger를 main.ts에서 지정할 수 있다.

아래처럼 logger를 비활성화하거나

const app = await NestFactory.create(AppModule, {
  logger: false,
});
await app.listen(3000);

아래처럼 logger의 레벨을 지정할 수 있다.

const app = await NestFactory.create(AppModule, {
  logger: ['error', 'warn'],
});
await app.listen(3000);

로거의 레벨은 위에서 언급한 대로 아래와 같다.

log, error, warn, debug, verbose

커스텀 로거

Nest가 제공하는 LoggerService를 implements 하는 커스텀 logger를 만들 수도 있다.

import { LoggerService } from '@nestjs/common';

export class MyLogger implements LoggerService {
  /**
   * Write a 'log' level log.
   */
  log(message: any, ...optionalParams: any[]) {}

  /**
   * Write an 'error' level log.
   */
  error(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'warn' level log.
   */
  warn(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'debug' level log.
   */
  debug?(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'verbose' level log.
   */
  verbose?(message: any, ...optionalParams: any[]) {}
}

위처럼 레벨마다 메세지와 옵션을 지정할 수 있다.

main.ts에서 커스텀 logger를 지정해 보자.

const app = await NestFactory.create(AppModule, {
  logger: new MyLogger(),
});
await app.listen(3000);

위와 같이 MyLogger는 의존성 주입 없이 인스턴스로 사용되고 있다.

이렇게 의존성 주입 없이 logger를 사용하게 되면 문제가 생길 수 있다.

이에 대한 해결 방법을 다뤄보자.

로거 모듈

다른 프로바이더와 마찬가지로 MyLogger 클래스를 프로바이더로 등록하는

LoggerModule을 만들면 위의 문제를 해결할 수 있다.

import { Module } from '@nestjs/common';
import { MyLogger } from './logger.service';

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

위와 같이 logger module을 만들어주고, MyLogger를 프로바이더로 지정하면 의존성 주입을 할 수 있게 된다.

 

main.ts에서 NestFactory.create()를 이용한 어플리케이션 인스턴스화는 모든 모듈의 컨텍스트 외부에서 발생하므로

초기화의 일반적인 종속성 주입 단계에 참여하지 않는다.

 

때문에 위 LoggerModule을 가져와 인스턴스화해 줄 모듈이 필요한데

아래와 같이 해결할 수 있다.

const app = await NestFactory.create(AppModule, {
  bufferLogs: true,
});
app.useLogger(app.get(MyLogger));
await app.listen(3000);
  • bufferLogs : 사용자 지정 로거가 연결되고 어플리케이션이 완료되거나 실패할 때까지 모든 로그가 버퍼링 되도록 bufferLogs를 true로 설정한다.
  • get() : MyLogger 객체의 싱글톤 인스턴스를 검색한다.

MyLogger 프로바이더는 feature class에도 주입될 수 있어서

일관된 로깅 동작을 보장할 수 있다.


외부 로거 사용하기

Nest에서는 외부 로거로 winston 라이브러리가 사용된다.
winston을 Nest의 모듈로 만들어놓은 nest-winston 패키지 또한 존재한다.

winston을 사용해 보자.

$npm i nest-winston winston
$yarn add nest-winston winston

Nest에서 제공하는 Logger 클래스를 이용해서 로깅을 구현하는 것도 물론 가능하지만,

상용 수준으로 운영하기 위해서는 로그를 파일이나 DB에 저장해서 쉽게 검색할 수 있도록 해야 한다.

 

winston 공식문서에 따르면 winston은 다중 전송을 지원하도록 설계되었다.

로깅 프로세스의 과정들을 분리시켜 좀 더 유연하고 확장 가능한 로깅 시스템을 작성하게 해 준다.

 

app module에서 WinstonModule을 import 해보자

import * as winston from 'winston';
import {
  WinstonModule,
  utilities as nestWinstonModuleUtilities,
} from 'nest-winston';

@Module({
  imports: [
  	...
    WinstonModule.forRoot({
      transports: [
        new winston.transports.Console({
          level: process.env.NODE_ENV === 'production' ? 'info' : 'silly',
          format: winston.format.combine(
            winston.format.timestamp(),
            nestWinstonModuleUtilities.format.nestLike('MyApp',{prettyPrint:true}),
          ),
        }),
      ],
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
  • transports : winston 옵션에서 transport 옵션을 설정한다.
  • level : 로그 레벨을 개발 환경에 따라 다르도록 지정한다.
  • winston.format.timestamp() : 로그를 남긴 시각을 함께 표시하도록 한다.
  • nestWinstonModuleUtilities.format.nestLike() : 어디에서 로그를 남겼는지를 구분하는 appName('MyApp')과 로그를 읽기 쉽도록 하는 prettyPrint 옵션을 설정한다.

winston이 지원하는 로그 레벨은 아래와 같다.

  • error : 0
  • warn : 1
  • info : 2
  • http : 3
  • verbose : 4
  • debug : 5
  • silly : 6

위의 level을 이용해 로그 레벨을 설정해 줬는데

프로덕션 환경이 아닐 때 silly로 설정했으므로 모든 레벨의 로그가 출력된다.

 

이제 user controller에 로그를 적용해 보자.

import { WINSTON_MODULE_PROVIDER } from 'nest-winston/dist/winston.constants';
import { Logger as WinstonLogger } from 'winston';

@Controller('users')
export class UsersController {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: WinstonLogger,
    private readonly usersService: UsersService,
  ) {}

  private printWinstonLog(dto) {
    this.logger.error('error: ', dto);
    this.logger.warn('warn: ', dto);
    this.logger.info('info: ', dto);
    this.logger.http('http: ', dto);
    this.logger.verbose('verbose: ', dto);
    this.logger.debug('debug: ', dto);
    this.logger.silly('silly: ', dto);
  }

  @Post()
  async create(@Body() dto: CreateUserDTO): Promise<void> {
    this.printWinstonLog(dto);

    const { name, email, password } = dto;
    await this.usersService.createUser(name, email, password);
  }
}

위와 같이 컨트롤러에 주입하고 로그를 지정해 주면

정상적으로 로그가 출력된 것을 볼 수 있다.

내장 로거 대체하기

위에서 사용한 것처럼 nest-winston은 LoggerService를 구현한 WinstonLogger 클래스를 제공한다.

Nest가 시스템 로깅을 할 때 이 클래스를 이용할 수 있기 때문에 Nest 시스템에서 출력하는 로그와 직접 출력하고자 하는 로깅의 형식을 동일하게 할 수 있다.

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
  );
  await app.listen(3000);
}
bootstrap();

위와 같이 앞서 봤던 get 메서드를 이용해 winston logger의 토큰을 주입하면,

라우터 엔드포인트를 winston logger를 적용해서 보여줄 수 있다.

 

하지만 아직 부트스트래핑 과정(모듈, 프로바이더, 의존성 주입 등의 초기화)에서는 Winston Logger 사용이 불가하다.

때문에 내장 로거가 사용되고 있는 것을 알 수 있다.

이는 winston module을 App module에서 import 하는 것이 아닌

NestFactory.create의 인수로 전달해야 한다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as winston from 'winston';
import {
  WinstonModule,
  utilities as nestWinstonModuleUtilities,
} from 'nest-winston';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: WinstonModule.createLogger({
      transports: [
        new winston.transports.Console({
          level: process.env.NODE_ENV === 'production' ? 'info' : 'silly',
          format: winston.format.combine(
            winston.format.timestamp(),
            nestWinstonModuleUtilities.format.nestLike('MyApp', {
              prettyPrint: true,
            }),
          ),
        }),
      ],
    }),
  });
  await app.listen(3000);
}
bootstrap();

위처럼 main.ts에서 로그를 설정해 주면,

로그 형식이 모두 바뀐 것을 알 수 있다.


참고

 

11장 로깅 - 애플리케이션의 동작을 기록한다

서비스에 기능이 늘어나고 사이즈가 커지게 되면 동작 과정을 남기고 추적하는 일이 중요하게 됩니다. 이슈가 발생했을 경우 이슈 증상만으로 원인을 파악하는 데에는 시간과 노력이 많이…

wikidocs.net

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

 

GitHub - winstonjs/winston: A logger for just about everything.

A logger for just about everything. Contribute to winstonjs/winston development by creating an account on GitHub.

github.com

마치며

로그를 공부하면서 Nest 동작 과정이나 기본이 부족하다는 것을 많이 체감했다.

Nest 공식문서를 정독하는 것이 최우선이 되어야 할 것 같다.

 

잘못된 정보에 대한 피드백은 환영입니다.

감사합니다.

728x90