[NestJS] winston으로 로그 관리

aryang·2022년 6월 8일
5

Node.js

목록 보기
1/1
post-custom-banner

@nestjs/common에서 제공하는 Logger 대신 winston을 이용하면 로그 파일을 저장하거나 중요한 로그는 db에 처리하는 등을 할 수 있게된다.

express에서는 winston + morgan 조합으로 가능했는데 nest-morgan은 더 이상 유지보수를 하지 않았다.

대체 라이브러리로 nestjs-pino를 제안했다.
하지만 로그의 포맷이나 날짜별 관리는 winston이 더 편리하다고 느꼈고,
요청/응답에 대한 로그는 미들웨어로 가능했기에 winston을 사용하기로 결정 🙂

1. 설치

$ npm i nest-winston winston winston-daily-rotate-file

winston-daily-rotate-file와 함께 사용하여 로그파일을 날짜별로 관리(압축, 삭제 등)할 수 있다.

2. 적용


main.ts

import { NestFactory } from '@nestjs/core';
import { winstonLogger } from './utils/winston.util';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: winstonLogger,
  });

  await app.listen(process.env.PORT || 3000);
}
bootstrap();

부트스트래핑 과정(모듈, 프로바이더, 의존성 주입 등을 초기화)에 winston을 적용하려면 NestFactory.create함수에 인스턴스를 전달해야 했다.

winston.util.ts

import { utilities, WinstonModule } from 'nest-winston';
import * as winstonDaily from 'winston-daily-rotate-file';
import * as winston from 'winston';

const env = process.env.NODE_ENV;
const logDir = __dirname + '/../../logs'; // log 파일을 관리할 폴더

// rfc5424를 따르는 winston만의 log level
// error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
export const winstonLogger = WinstonModule.createLogger({
  transports: [
    new winston.transports.Console({
      level: env === 'production' ? 'http' : 'silly',
      // production 환경이라면 http, 개발환경이라면 모든 단계를 로그
      format:
        env === 'production'
      // production 환경은 자원을 아끼기 위해 simple 포맷 사용
          ? winston.format.simple() 
          : winston.format.combine(
              winston.format.timestamp(),
              utilities.format.nestLike('프로젝트이름', {
                prettyPrint: true, // nest에서 제공하는 옵션. 로그 가독성을 높여줌
              }),
            ),
    }),

    // info, warn, error 로그는 파일로 관리
    new winstonDaily(dailyOptions('info')),
    new winstonDaily(dailyOptions('warn')),
    new winstonDaily(dailyOptions('error')),
  ],
});

dailyOptions은 중복되서 따로 함수로 뺐다.

const dailyOptions = (level: string) => {
  return {
    level,
    datePattern: 'YYYY-MM-DD',
    dirname: logDir + `/${level}`,
    filename: `%DATE%.${level}.log`,
    maxFiles: 30, //30일치 로그파일 저장
    zippedArchive: true, // 로그가 쌓이면 압축하여 관리
  };
};

다음과 같이 winstonDaily 덕분에 로그 폴더들이 자동으로 생기는걸 확인할 수 있다.

app.module.ts

import { Logger, MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { EnvModule } from './env.module';

import { LoggerMiddleware } from './middlewares/logger.middleware';

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
  }
}

인터셉터 방식은 라우트 핸들러를 지나야해서 잘못된 요청(404 등)은 로그가 찍히지 않았다.
aws에 서버를 올리면 악성 공격을 하는 ip는 밴을 해야할 필요가 있을 수 있으니
모든 요청에 대한 기록이 필요해서 미들웨어 방식을 선택했다.

forRoutes('*') 함수로 로거를 모든 라우트에 적용을 했다.

logger.middleware.ts

import {
  Inject,
  Injectable,
  Logger,
  LoggerService,
  NestMiddleware,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  constructor(@Inject(Logger) private readonly logger: LoggerService) {}

  use(req: Request, res: Response, next: NextFunction) {
    // 요청 객체로부터 ip, http method, url, user agent를 받아온 후
    const { ip, method, originalUrl } = req;
    const userAgent = req.get('user-agent');

    // 응답이 끝나는 이벤트가 발생하면 로그를 찍는다.
    res.on('finish', () => {
      const { statusCode } = res;
      this.logger.log(
        `${method} ${originalUrl} ${statusCode} ${ip} ${userAgent}`,
      );
    });

    next();
  }
}

완성 !

3. 참고

https://www.npmjs.com/package/winston
https://www.npmjs.com/package/nest-winston
https://choseongho93.tistory.com/317
https://wikidocs.net/book/7059
https://velog.io/@gwon713/Express-winston-morgan-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC
https://blog.naver.com/kjmj13/222685440259

post-custom-banner

0개의 댓글