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

Nhahan·2023년 7월 16일
1
  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개의 댓글