pino logger + aws cloudwatch (elk ) / 항해플러스 Chapter3

Nhahan·2023년 7월 16일
  1. 로그 레벨 라이브러리로 winston을 쓰라고 했지만 기존에 이미 pino로 세팅을 해놔서 pino로 진행
  2. 예시로 나온 winston cloudwatch 라이브러리를 사용하지 못하기에 pino cloudwatch 라이브러리를 찾아봤지만, 그냥 직접 구현하기로 결심하여 직접 구현
import { loggerConfig } from '@/config/logger.config';

import { LogBehavior } from './log-behavior.decorator';

class LoggerService {
  private static instance: LoggerService;
  private readonly logger;

  private constructor() {
    this.logger = loggerConfig;
  }

  static getInstance(): LoggerService {
    if (!LoggerService.instance) {
      LoggerService.instance = new LoggerService();
    }
    return LoggerService.instance;
  }

  @LogBehavior()
  error(message: string, context?: any, stack?: any) {
    this.logger.error({ message, context, stack });
  }

  @LogBehavior()
  log(message: string, context?: any) {
    this.logger.info({ message, context });
  }

  @LogBehavior()
  warn(message: string, context?: any) {
    this.logger.warn({ message, context });
  }

  debug(message: string, context?: any) {
    this.logger.debug({ message, context });
  }

  verbose(message: string, context?: any) {
    this.logger.trace({ message, context });
  }
}

export const logger = LoggerService.getInstance();
  1. loggerConfig에 pino 관련 설정이 있어서 여기에 관련 코드는 없고, 매번 로거서비스를 주입하기 싫어서 static으로 선언해 어디에서든 쓸 수 있게, 마치 console.log()처럼 편하게 쓸 수 있게 했음
  2. 로그를 찍는것과 저장(cloudwatch나 elk)하는 로직을 분리하기 위해 데코레이터로 분리하였다(나중에는 ELK를 붙일 생각. 현재는 시간이 없어서 구현이 쉬운 클라우드워치로 대체). 또한 원하는 로그레벨에만 적용하기 위해 데코레이터로 명시적으로 원하는 로그 레벨을 정할 수 있게 하기 위함도 있음
import { CloudWatchLogSender } from '../infra/cloud-watch';
import { LogSender } from '../interface/log-sender.interface';

const logSender: LogSender = CloudWatchLogSender.getInstance();

export function LogBehavior(): MethodDecorator {
  return function (target: any, propertyKey: LogLevel, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const message = args[0];
      const context = args[1];
      const stack = args[2];
      logAction(propertyKey, message, context, stack);
      return originalMethod.apply(this, args);
    };
  };
}

function logAction(level: LogLevel, message: string, context: any, stack: any) {
  if (process.env.NODE_ENV === 'test') {
    ['error', 'log', 'warn', 'debug', 'verbose'].includes(level) && console.log(message, context);
    return;
  }
  message && logSender?.sendLog(`[${level}] ${message}`, context, stack);
}

type LogLevel = 'error' | 'log' | 'warn' | 'debug' | 'verbose';

logSender라는 인터페이스를 만들어 언제든 클라우드워치와 ELK를 바꿔끼울 수 있게 만들었다.

export interface LogSender {
  sendLog(message: string, context: any, stack: any): void;
}

아래는 클라우드 워치 구현

import { ConfigService } from '@nestjs/config';
import AWS from 'aws-sdk';

import { LogSender } from '@shared/interface/log-sender.interface';

export class CloudWatchLogSender implements LogSender {
  private cloudWatchLogs: AWS.CloudWatchLogs;
  private configService: ConfigService;

  private static instance: CloudWatchLogSender;

  private constructor() {
    this.cloudWatchLogs = new AWS.CloudWatchLogs();
    this.configService = new ConfigService();
  }

  static getInstance(): CloudWatchLogSender {
    if (!CloudWatchLogSender.instance) {
      CloudWatchLogSender.instance = new CloudWatchLogSender();
    }
    return CloudWatchLogSender.instance;
  }

  async sendLog(message: string, context: any, stack: any): Promise<void> {
    const logGroupName = this.configService.get<string>('AWS_CLOUDWATCH_LOG_GROUP_NAME');
    const logStreamName = this.configService.get<string>('AWS_CLOUDWATCH_LOG_STREAM_NAME');

    const logEvent: AWS.CloudWatchLogs.Types.InputLogEvent = {
      message: JSON.stringify({
        message,
        ...(context && { context }),
        ...(stack && { stack }),
      }),
      timestamp: Date.now(),
    };

    const params = {
      logGroupName,
      logStreamName,
      logEvents: [logEvent],
    };

    try {
      await this.cloudWatchLogs.putLogEvents(params).promise();
    } catch (error) {
      console.error('[CloudWatchLogSender] putLogEvents fail. error:', error);
      throw error;
    }
  }
}

로그 앞에 로그레벨까지 붙여서 완성.

0개의 댓글