NodeJS

nestjs-pino 로깅 처리

lahuman 2023. 5. 30. 13:58
728x90

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에서 사용되는 여러 모듈의 사용법을 정리해 두었습니다.

참고자료

728x90