nestjs-pino 로깅 처리
상황 정리
위와 같은 WEB 환경에서 Some logging
의 경우 Request(요청)
정보가 없기 때문에 동시 다발적인 이벤트의 로그들을 추적하기가 불가능 합니다.
이를 nestjs-pino 이용해서 각 로그별로 동일한 요청의 경우 연결 처리 할 수 있도록 작성합니다
nestjs-pino에서도 적혀 있지만, pino-http모듈을
nestjs
에 녹인 프로젝트입니다.
필요 모듈 정보
- nestjs-pino : nestjs와 연동 처리된 모듈
- pino-http : pino 로그에 request, response 정보를 bind 처리한 모듈
- file-stream-rotator : 파일 스트림을 기반으로 파일의 생명주기를 관리하는 모듈
- pino-pretty : pino 로그의 결과를 이쁘게 정렬하여 표기하는 모듈
기본 모듈 설치 및 설정
nestjs-pino 는 기본적으로 로그 파일 저장시 파일을 나눠주거나, 관리를 지원하지 않습니다.
이를 file-stream-rotator 모듈을 이용해서 로그 파일에 대한 생명주기 및 적재 관리를 합니다.
다음 명령어로 기본적인 모듈을 설치합니다.
$ npm i nestjs-pino pino-http file-stream-rotator
nestjs 설정하기
먼저, nestjs의 main.ts
에서 Logging을 설정 합니다.
import { Logger } from "nestjs-pino";
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(app.get(Logger));
이후 app.module.ts
에 아래와 같이 설정 합니다.
import { LoggerModule } from "nestjs-pino";
@Module({
imports: [LoggerModule.forRoot()],
})
class AppModule {}
위의 예제는 기본 설정입니다.
위와 같이 설정 후 아래와 같이 로깅을 하면,
// NestJS standard built-in logger.
// Logs will be produced by pino internally
import { Logger } from '@nestjs/common';
export class MyService {
private readonly logger = new Logger(MyService.name);
foo() {
// All logger methods have args format the same as pino, but pino methods
// `trace` and `info` are mapped to `verbose` and `log` to satisfy
// `LoggerService` interface of NestJS:
this.logger.verbose({ foo: 'bar' }, 'baz %s', 'qux');
this.logger.debug('foo %s %o', 'bar', { baz: 'qux' });
this.logger.log('foo');
}
}
아래와 같이 표출 됩니다.
// Logs by injected Logger and PinoLogger in Services/Controllers. Every log
// has it's request data and unique `req.id` (by default id is unique per
// process, but you can set function to generate it from request context and
// for example pass here incoming `X-Request-ID` header or generate UUID)
{"level":10,"time":1629823792023,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"context":"MyService","foo":"bar","msg":"baz qux"}
{"level":20,"time":1629823792023,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"context":"MyService","msg":"foo bar {\"baz\":\"qux\"}"}
{"level":30,"time":1629823792023,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"context":"MyService","msg":"foo"}
로깅 결과 이쁘게 표시 하기
pino-pretty는 stdout 리디렉션을 사용하기 때문에 경우에 따라 셸 제한으로 인해 명령이 오류와 함께 종료될 수 있습니다. 따라서 운영에서는 사용하지 말고, 개발시에만 사용하세요.
로깅을 이쁘게 표기하기 위해서는 pino-pretty모듈을 설치하면 됩니다.
$ npm install pino-pretty
설정은 아래와 같이 할 수 있습니다.
import { LoggerModule } from 'nestjs-pino';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: [
{
name: 'add some name to every JSON line',
level: process.env.NODE_ENV !== 'production' ? 'debug' : 'info',
// install 'pino-pretty' package in order to use the following option
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
useLevelLabels: true,
// and all the others...
},
],
})
],
...
})
class MyModule {}
처리를 하면 다음과 같이 정렬된 로그를 확인 가능합니다.
[17:05:49.172] INFO (20249): GET / 200 168 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 ::1
{
"level": 10,
"time": 1629823792023,
"pid": 15067,
"hostname": "my-host",
"req": {
"id": 1,
"method": "GET",
"url": "/",
"query": {
},
"params": {
"0": ""
},
"headers": {
"host": "localhost:3000",
"user-agent": "curl/7.64.1",
"accept": "*/*"
},
"remoteAddress": "::1",
"remotePort": 63822
},
"context": "MyService",
"foo": "bar",
"msg": "baz qux"
}
로그 설정 파일 관리
다음은 pinoLogging.ts
로 로깅 설정에 대한 정보입니다.
import pino from 'pino';
import * as FileStreamRotator from 'file-stream-rotator';
import { v4 } from 'uuid';
type Level = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
export default () => {
const LOG_LEVEL = process.env.LOGGING_DEBUG ? 'debug' : 'info';
// 로그 파일 관리 스트림 생성
const rotatingLogStream = FileStreamRotator.getStream({
filename: `${process.env.LOGGING_PATH}/${LOG_LEVEL}/${LOG_LEVEL}-%DATE%`, // 파일 위치 & 이름
frequency: '1h', // 주기 설정
date_format: 'YYYY-MM-DD-HH', // 데이터 포멧 설정
size: process.env.LOGGING_MAXSIZE, // 최대 파일 크기 설정
max_logs: process.env.LOGGING_MAXFILES, // 파일 로깅
audit_file: `${process.env.LOGGING_PATH}/audit.json`, // 정보 파일
extension: '.log', // 로그 확장자
create_symlink: true, // 링크 파일 여부
symlink_name: 'tail-current.log', //링크 파일 명
});
return {
logginConfig: {
pinoHttp: {
genReqId: function (req, res) { // req - id 를 uuid로 생성
const uuid = v4();
res.header('X-Request-Id', uuid);
return uuid;
},
transport: LOG_LEVEL === 'debug' // debug일 경우 pretty 처리
? { target: 'pino-pretty' }
: undefined,
level: LOG_LEVEL, // 여기에도 있고, stream 상세에도 있어야 정상 동작 한다
stream: pino.multistream([ // multistream으로 여러군데 동시 출력
{
stream: rotatingLogStream,
level: LOG_LEVEL as Level,
},
{
stream: process.stdout, // 콘솔에 출력
level: process.env.LOGGING_CONSOLE_LEVEL as Level,
},
]),
formatters: { // 로그 표시시 포멧
level(level) {
return { level };
},
},
redact: { // 로그 표기시 제외 처리
remove: true,
paths: [
'email',
'password',
'req.query',
'req.params',
'req.query',
'res.headers',
'req.headers.host',
'req.headers.connection',
'req.headers.accept',
'req.headers.origin',
'req.headers.referer',
'req.headers["content-type"]',
'req.headers["sec-ch-ua"]',
'req.headers["sec-ch-ua-mobile"]',
'req.headers["user-agent"]',
'req.headers["sec-ch-ua-platform"]',
'req.headers["sec-fetch-site"]',
'req.headers["sec-fetch-mode"]',
'req.headers["sec-fetch-dest"]',
'req.headers["accept-encoding"]',
'req.headers["accept-language"]',
'req.headers["if-none-match"]',
],
},
timestamp: pino.stdTimeFunctions.isoTime,
},
},
};
};
전체 예제 코드 바로 가기
전체 예제 코드를 보면, nestjs-pino 외에도 nestjs에서 사용되는 여러 모듈의 사용법을 정리해 두었습니다.