모든 로그를 남겨보자!

HanSH·2024년 2월 19일

NestJS

목록 보기
17/29

모든 것을 로그로 남겨보자!
Nest 자체에서 로깅을 지원해주기는 하지만 몇 가지 제한 사항이 있어 Winston + winston-daily-rotate-file 을 이용하였다.
해당 제한 사항은

  1. 로그를 파일로 저장할 수 있는지 여부 확인 불가

    공식 문서에 콘솔로 출력하는 예제는 있는데 파일로 저장하는 예제는 보이지 않는다.

  2. 날짜별로 데이터 저장 - winston

    여러 모듈들을 찾아보는 중 winston이 가장 쓰기 편해보였다. 덤으로 콘솔에 보이는 로그 포맷도 정할 수 있다는 점이 강점!

  3. 파일 크기가 일정크기 이상이 되면 파일 나누기 - winston-daily-rotate-file

    winston의 문제점이 저장하는 파일 크기를 조정하지 못하는 것. 물론 지금으로서는 처리하는 데이터가 많지 않아 일별로 하나의 파일에 저장을 해도 큰 문제는 없지만 추후 처리하는 데이터가 많아질 것을 고려하여 일정 크기 이상으로 로그파일이 커지면 분리된 파일로 저장되게 한다.

모듈 설치

yarn add winston winston-daily-rotate-file nest-winston

winston 모듈과 nest에서 사용하기 위한 nest-winston, 로그 파일 최대 크기를 정하기 위한 winston-daily-rotate-file 모듈을 다운받는다.

설정

  1. 로그 파일 config 내용 설정
import * as winston from 'winston';
import {
  utilities as nestWinstonModuleUtilities,
} from 'nest-winston';
import * as DailyRotateFile from 'winston-daily-rotate-file';

const loggerLevel = 'silly';

const loggerFormatter = winston.format.combine(
  winston.format.timestamp(),
  nestWinstonModuleUtilities.format.nestLike('MyApp', {	// `MyApp`은 로그 맨 앞에 뜨는 문구. 맨 위의 사진에서 보면 된다.
    prettyPrint: true, // json 데이터의 경우에 예쁘게 저장해줌. 로그 분석에 어려움을 겪을 수 있으니 false로 하자
  }),
);

const loggerSetting = {
  level: loggerLevel,
  format: loggerFormatter,
};

export const loggerConfig = {
  transports: [
    new winston.transports.Console(	// winston에서 console에 띄우는 로그 세팅
      loggerSetting
    ),
    new DailyRotateFile({
      filename: 'logs/%DATE%.log',	// 로그 파일 저장 위치와 이름. 디렉터리가 없으면 새로 생성한다.
      datePattern: 'YYYY-MM-DD', 	// 날짜 저장 형식. filename의 매개변수로 이용된다.
      maxSize: '20m', 				// 로그 파일의 최대 크기
      // maxFiles: '14d', 			// 로그 파일의 최대 저장 기간 설정. 14일이 지나면 삭제된다.
      ...loggerSetting
    }),
  ],
};
/*
	loggerSetting 		: 해당 딕셔너리 자체를 사용
    ...loggerSetting 	: 해당 딕셔너리에 있는 데이터를 뽑아서 사용
    
    즉, { loggerSetting }의 경우
    	{
        	{
            	level: loggerLevel,
                format: loggerFormat
            }
        }
    { ...loggerSetting }의 경우
        {
            level: loggerLevel,
            format: loggerFormat
        }
    로 나타나게 된다.
*/
  1. 전역 로그로 사용
import { WinstonModule } from 'nest-winston';

@Module({
  imports: [
    ...
    WinstonModule.forRoot(
      loggerConfig 			// 실제로는 transport에 여러가지를 다 넣어야하지만 유지,보수를 위해 다른 파일로 빼놓았다.
    ),
  ],
  controllers: [],
  providers: [],
})
export class AppModule implements NestModule {}
  1. 시스템 로그로 사용
    전역 로그를 만들어놓았어도 서버가 실행될때의 로그는 저장되지 않는다. bootstraping 중에는 기본 로그를 사용하는 것으로 확인된다.
    자세한 내용은 이곳으로.
import { WinstonModule } from 'nest-winston';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: WinstonModule.createLogger(				// 시스템 로그를 Winston으로 설정하자!
      loggerConfig
    ),
  });

  await app.listen(3000);
}
bootstrap();

로그 사용

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

@Controller('boards')
export class BoardsController {
  constructor(
    ...
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: WinstonLogger	// 로거를 사용하기 위해서는 의존성 주입이 필요
  ) {};

  private printWinstonLog(dto) {
    this.logger.error('error: ', dto);		// error. 앱 실행이 중단될 수 있는 막대한 오류
    this.logger.warn('warn: ', dto);		// warining. 잠재적으로 error가 될 수 있는 간단한 오류
    this.logger.info('info: ', dto);		// info. 일반적인 메시지.
    this.logger.debug('debug: ', dto);		// 디버깅용 로그 출력
    this.logger.verbose('verbose: ', dto);	// 가장 낮은 레벨의 로그.
    this.logger.http('http: ', dto);		// http 요청 및 응답 로그를 남기기 위해 사용. 일반적인 로그 레벨은 아님.
    this.logger.silly('silly: ', dto); 
  }

  @Get()
  async getAllBoards(@Query('page') page: number = 1) {
    const res = await this.boardsService.findAll(page);
    this.printWinstonLog(res);
    return res;
  }
}


없는 데이터를 반환하게 했지만, 로그가 정상적으로 출력되는 것을 볼 수 있다.

파일에 저장하는 것도 성공적으로 이루어졌다!

로그 추출

정규표현식을 이용하여 원하는 타입의 로그만 뽑을 수 있게 하였다!

import os
import re

# 파일 이름 패턴 정규 표현식
file_pattern = r"^2024-02-19"
log_dir = "./nest/logs"
log_pattern = r"\[.*?\]\s+(.*?)\s+(\d{4}\.\s\d{1,2}\.\s\d{1,2}\.\s오후\s\d{1,2}:\d{1,2}:\d{1,2})\s+(.*?)$"

# 현재 디렉토리의 파일 목록 가져오기
files = os.listdir(log_dir)

# 정규 표현식에 따라 파일 필터링
filtered_files = [file for file in files if re.match(file_pattern, file)]

log_messages = []
for file in filtered_files:
  with open(os.path.join(log_dir, file), 'r', encoding='utf-8') as file:
    lines = file.readlines()
    log_messages += [ line.strip().replace('\t', ' ') for line in lines ]

for log_message in log_messages:
    match = re.match(log_pattern, log_message)
    if match:
      level = match.group(1)
      time = match.group(2)
      message = match.group(3)

      if level == 'Warn':
        print("Level:", level)
        print("Time:", time)
        print("Message:", message)
        print()
profile
저는 말하는 싹 난 감자입니다

0개의 댓글