Logger

장현욱(Artlogy)·2022년 11월 17일
0

Nest.js

목록 보기
11/18
post-thumbnail
post-custom-banner

Logger


서비스가 커지면 커질수록 동작 과정을 남기고 추적하는 일이 중요해진다.
이슈가 발생한 지점과 콜스택이 함께 제공된다면 디버깅이 매우 수월해질것이다.

내장 로거

내장 Logger는 @nest/common패키지로 제공되며, 로깅옵션을 조절하면 다음과 같이 로깅 시스템의 동작을 제어할 수 있다.

  • 로깅 비활성화
  • 로그 레벨 지정 : log,error,warn,debug,verbose
  • 로거의 타임스탬프 재정의
  • 기본 로거를 오버라이딩
  • 기본 로거 확장 및 커스텀 로거 작성
  • DI 로그 주입, 테스트 모듈로 제공

내장 로거의 인스턴스는 로그를 남기고자 하는 부분에서 직접 생성해서 사용할 수 있다.

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

@Injectable()
export class AppService {
  //AppService.name으로 로그가 나오는 위치를 지정했다. (물론 다른 문자열로 써도됌)
  private readonly logger = new Logger(AppService.name);

  getHello(): string {
    this.logger.error('level: error');
    this.logger.warn('level: warn');
    this.logger.log('level: log');
    this.logger.verbose('level: verbose');
    this.logger.debug('level: debug');

    return 'Hello World!';
  }
}

로깅 비활성화

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

추천은 안하지만 로그가 아에 뜨지 않게 만들 수 있다.

로그 레벨 지정

  const app = await NestFactory.create(AppModule, {
    logger:
      process.env.NODE_ENV === 'prod'
        ? ['error', 'warn', 'log']
        : ['debug', 'error', 'log', 'verbose', 'warn'],
  });

일반적으로 빌드환경에선 debug 로그가 남지 않도록 하는게 좋다.
민감한 정보를 포함되고, 디버그 로그는 그 자체로 사이즈가 크기때문에 상용환경에선 좋은 퍼포먼스를 기대하기 힘들다.

애초에 상용환경에서 디버그를 켜놓는게 아무 의미가 없다.

커스텀 로그

로그가 몇개 안나오면 그냥 좀 살펴보면 되지만, 서비스를 하면 수 많은 로그가 쌓이고 거기에서 내가 원하는 로그를 찾기란 하고싶지도 않고 힘든 일이다. 때문에 로그를 데이터베이스에 저장해 검색을 하는 커스텀로거를 만들어 볼 수 있다. 내 포스팅에선 커스텀 로그를 만들기 위한 기본 재료만 알려줄것이다.

기본 형태

import { ConsoleLogger, Injectable, Module } from '@nestjs/common';

@Injectable()
export class MyLogger extends ConsoleLogger {
  error(message: any, stack?: string, context?: string) {
    super.error.apply(this, arguments);
  }

  private doSomething() {
    // log에 대한 부가 로직을 넣어준다.
    // ex. DB에 저장
  }
}

이렇게 해주고 루트모듈이나, 글로벌 모듈에 등록만 해주면 전역적으로 종속성을 주입하여 사용할 수 있다.

Winston

Nestjs에서 공식적으로 패키징하고있는 로그패키지이다.
커스텀로그로 DB에 저장, 추적, 검색을 구현할 수도 있지만 많은 수고가 필요하다 Winston을 이용하면 간단하게 구현이 가능하다.

근데 난 내장 로거를 커스텀해서 사용하는게 더 편하더라..

패키지 설치

#npm
$ npm i -s nest-winston winston

#yarn
$ yarn add nest-winston winston

### 설정
```ts
WinstonModule.forRoot({
      transports: [
        new winston.transports.Console({
          //로그 레벨 지정
          level: process.env.NODE_ENV === 'prod' ? 'info' : 'silly',
          format: winston.format.combine(
            //로그 남긴 시간 추가
            //로그에 appName('ArtlogyApp')남기며, 로그에 색조합을 주는 prettyPrint true
            winston.format.timestamp(),
 
            nestWinstonModuleUtilities.format.nestLike('ArtlogyApp', {
              prettyPrint: true,
            }),
          ),
        }),
      ],
    }),

레벨

{ 
  error: 0, 
  warn: 1, 
  info: 2, 
  http: 3,
  verbose: 4, 
  debug: 5, 
  silly: 6 
}

숫자가 낮을 수록 레벨이 높으며 설정된 레벨보다 낮은 로그가 출력된다.

사용

import { Logger as WinstonLogger } from 'winston';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
...

export class UsersController {

  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: WinstonLogger,
  ) { }

  @Post()
  async createUser(@Body() dto: CreateUserDto): Promise<void> {
        this.printWinstonLog(dto);
        ...
  }

    private printWinstonLog(dto) {
    console.log(this.logger.name);

    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);
  }
    ...
}

WINSTON_MODULE_NEST_PROVIDER토큰으로 winston에 제공하는 Logger객체를 주입 받을 수 있다.

Nest Logger 대체하기

forRoot나 forRootAsync으로 모듈을 만들면 부트스트랩까지 대체할 수 없기 때문에
다음과 같이 만들어준다.

main.ts
import { AuthGuard } from '@config/guard/jwt.guard';
import { swaggerConfig } from '@config/swagger.config';
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app/app.module';
import * as winston from 'winston';
import {
  WINSTON_MODULE_NEST_PROVIDER,
  utilities as nestWinstonModuleUtilities,
  WinstonModule,
} 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 === 'prod' ? 'info' : 'silly',
          format: winston.format.combine(
            winston.format.timestamp(),
            winston.format.ms(),
            nestWinstonModuleUtilities.format.nestLike('ArtlogyApp', {
              prettyPrint: true,
            }),
          ),
        }),
      ],
    }),
  });
  app.useGlobalPipes(
    new ValidationPipe({
      // 변환 허용
      transform: true,
      // 유효성 검사 데코레이터가 없는 필드를 요청 할 경우 건너뜀
      whitelist: true,
      // 유효성 검사 데코레이터가 없는 필드가 있을 경우 에러를 반환
      forbidNonWhitelisted: true,
      // null, undefined값을 가진 객체는 유효성 검사를 건너뜀
      skipMissingProperties: true,
    }),
  );
  const authGuard: AuthGuard = app.get<AuthGuard>(AuthGuard);
  app.useGlobalGuards(authGuard);
  const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
  SwaggerModule.setup('api', app, swaggerDocument);
  await app.listen(8084, () => {
    console.log('server running');
  });
}
bootstrap();

로그를 남길 모듈에 Logger 서비스를 프로바이더로 선언하면 된다.

import { Logger } from '@nestjs/common';
...
@Module({
    ...
  providers: [Logger]
})
export class UsersModule { }

이제 @nestjs/common 패키지의 Logger 또는 LoggerService를 다음처럼 주입받을 수 있다.

import { Logger } from '@nestjs/common';
...
export class UsersController {
  constructor(
    @Inject(Logger) private readonly logger: LoggerService,
  ) { }
    ...
}

로그 활용

winston을 이용하거나 커스텀을 이용하는 가장 큰 이유는 파일이나 DB에 저장해서 로그를 활용하고자 함이다. 최근에는 New Relic, DataDog같은 외부 유로 서비스에 전송해서 분석과 시각화 툴을 활용하기도 한다.

winston-transport라이브러리로 지속적인 로그 전달이 가능하다.
난 돈이 없기 때문에 이 부분은 그냥 넘어 갈 것인데 관심있음 레퍼런스 많으니 찾아보면 좋다.
(물론 DB에 넣고 웹으로 내가 만들어도 된다. )

💡 상용 서비스에서 에러 정보, 호출 스택 등의 정보와 사용자가 추가한 로그를 파일에 기록하고 기록된 파일을 외부 서비스에 다시 전달하여 검색과 시각화를 한다. 구현시 웹프레임워크나 서비스 업체에서 제공하는 라이브러리를 활용한다.

post-custom-banner

0개의 댓글