@nestjs/common
에서 제공하는 Logger
대신 winston
을 이용하면 로그 파일을 저장하거나 중요한 로그는 db에 처리하는 등을 할 수 있게된다.
express에서는 winston + morgan 조합으로 가능했는데 nest-morgan
은 더 이상 유지보수를 하지 않았다.
대체 라이브러리로 nestjs-pino를 제안했다.
하지만 로그의 포맷이나 날짜별 관리는 winston
이 더 편리하다고 느꼈고,
요청/응답에 대한 로그는 미들웨어로 가능했기에 winston을 사용하기로 결정 🙂
$ npm i nest-winston winston winston-daily-rotate-file
winston-daily-rotate-file
와 함께 사용하여 로그파일을 날짜별로 관리(압축, 삭제 등)할 수 있다.
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함수에 인스턴스를 전달해야 했다.
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 덕분에 로그 폴더들이 자동으로 생기는걸 확인할 수 있다.
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('*')
함수로 로거를 모든 라우트에 적용을 했다.
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();
}
}
완성 !
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