삽집하는 개발들/AWS

[API Logger][Cloudwatch 연동]

악투 2023. 9. 7. 15:04
반응형

1. 현재 Logger 관련 구성 체크

  • common/middlewares/logger.middleware 존재
use(req: Request, res: Response, next: NextFunction) {
    res.on('finish', () => {
      this.logger.log(
        `${req.ip}, ${req.originalUrl}, ${req.method}, ${res.statusCode}`,
      );
    });
    next();
  }

2. Cloud Watch 와 API Logger 구성에 대한 고찰

  • 현재 미들웨어에서 구성을 하려고 작업 진행중 - 현재 시스템에서 많은 변화를 주는 건 부담이 될듯하여, 기존 미들웨어에서 cloudwatch로 Log 보내느 것을 구현하려고함.
  • custom Log 기준
    • debug
    • verbose
    • log
    • Warning
    • Error
  • 개발환경 로그와 라이브 환경의 로그 분리에 대한 고민

3. 개발 프로세스

1) Cloudwatch + log 테스트 연동

  • 로그 연동은 성공, 스트림을 설정하지 않고, 로그 보낼시 스트림 에러, 스트림 생성 후 작업 로직 추가 필요
constructor() {
		this.cloudWatchLogs = new CloudWatchLogs({
			region: 'ap-northeast-2',
			accessKeyId : process.env.AWS_CW_ACCESS_KEY,			
			secretAccessKey : process.env.AWS_CW_SECRET_KEY,			
		});
		this.logGroupName = "babayo-api-logs"
		this.logGroupName = "babayo-api-logs"
	}

const params = {
	logGroupName : "babayo-api-logs",
	logStreamName : "test",
	logEvents: [
		{
			message: "연결 성공",
			timestamp: Date.now(),
		},
	],
}
this.cloudWatchLogs.putLogEvents(params).promise();

2) 로그스트림과 로그 생성

  • 로그를 생성하기 위해선 로그스트림이라는 것이 먼저 생성되어야함.
// 로그 스트림 생성 기준
// 날짜 기준으로 하루에 하나에 로그스트림을 생성하고, 그 안에 로그를 쌓는 방식으로 진행하려고함.
// 처음 API가 구동 시 로그 스트림 체크를 위해 만들어져있는 로그 스트림을 최신 한개를 가져와 로그스트림의 값과 비교하고 그 이후 전역변수에 담아 체크는 하되, AWS에서 로그 스트림을 가져오는 행위는 하루에 한번 혹은 최초 구동시 한번만 사용가능하게 하기위한 로직을 추가

// 현재 날짜의 스트림이 있는지 체크
// 최초 서버가 실행될때만 체크하기 위한 로직 추가
async cloudWatchStreamCheck() {
		try {
			return await this.cloudWatchLogs.describeLogStreams({
				logGroupName : "babayo-api-logs",
				descending : true,
				orderBy: "LogStreamName",
				limit : 1
			}).promise().then((data) => {			
				let logStreams = data.logStreams.length != 0 ? data.logStreams[0].logStreamName : ""
				return logStreams;
			}).catch(err => {
				console.log(err)
			});
		} catch (exception) {
			console.log(exception);
		}
	}
  • 로그 스트림이 생성된 후 스트림 안에 로그를 생성해야함.
  • 여기서 문제는 로그 스트림을 어떤 기준으로 생성할 것인가?
  • 또한 로그 스트림을 만들고 최초 로그를 등록하면, 그 로그에 다시 넣으려고 하면 nextSequenceToken 에러가 나옴. 확인 결과 이미 만들어진 로그에 넣으려면 최초 로그를 만들면 생성되는 SequenceToken을 가지고 있다가 같은 로그에 넣게 될 때 파라미터로 보내주지 않으면 에러남.

3) 로그 생성 과정

  • 최초 서버 구동시 로그 스트림을 체크하여, 현재날짜와 비교했을때 다르면 생성 로직 타게 설정
//최초 서버 구동시
let logStreams = "";
let nextSequenceToken = "";
let cloudWatchLogs = new CloudWatchLogs({
  region: 'ap-northeast-2',
  accessKeyId : process.env.AWS_CW_ACCESS_KEY,      
  secretAccessKey : process.env.AWS_CW_SECRET_KEY,
});

// 최초 로그 스트림 체크 하여 전역변수에 저장
cloudWatchLogs.describeLogStreams({
  logGroupName : "babayo-api-logs",
  descending : true,
  orderBy: "LogStreamName",
  limit : 1
}).promise().then((data) => {     
  logStreams = data.logStreams.length != 0 ? data.logStreams[0].logStreamName : ""  
  return logStreams;
}).catch(err => {
  console.log(err)
});

async logIntegrate(params: any){
    try {
      let { logType, message } = params;
      const now: string = moment().format('YYYY-MM-DD');
      if(logStreams == "" || now != logStreams) {
        //해당 스트림명으로 생성
        await this.cloudWatchStreamCreate({
          now
        });
      }
      await this.cloudWatchLogsInit({
        logType, message
      });
    } catch (exception) {
			console.log("logIntegrate() Error : ", exception);
    }   
  }

4. 개발

1) app 실행시 현재 날짜에 로그 스트림이 있는지 확인 후, 글로벌 변수에 저장.(로그 종류는 차차 늘릴 예정)

  • DEV : LOG, WARNING, ERROR
  • PROD : WARNING, ERROR
// log Stream 체크
	// 모듈의 종속성 처리 후 호출
async onModuleInit() {
    let logStreamName: string = process.env.MODE == "dev" ? "babayo-api-logs" : "babayo-api-logs-live";
    global.logStreams = "";

    let cloudWatchLogs = new CloudWatchLogs({
      region: 'ap-northeast-2',
      accessKeyId : process.env.AWS_CW_ACCESS_KEY,      
      secretAccessKey : process.env.AWS_CW_SECRET_KEY,
    });
    global.cloudWatchLogs = cloudWatchLogs;
    
    // 최초 로그 스트림 체크 하여 전역변수에 저장
    cloudWatchLogs.describeLogStreams({
      logGroupName : logStreamName,
      descending : true,
      orderBy: "LogStreamName",
      limit : 1
    }).promise().then((data) => {     
      global.logStreams = data.logStreams.length != 0 ? data.logStreams[0].logStreamName : ""       
    }).catch(err => {
      console.log(err)
    });   
  }

2) Logger Service 구현(하루 기준 로그)

// 기본 로그
  log(message: any, stack?:string, context?: string) {
    let logType = "LOG";
    //super.log(message, context);
		if (process.env.MODE == 'dev') {
			this.logIntegrate({
				logType, message, stack
			});
		}
  };

  // warning 로그
  warn(message: any, stack?:string, context?: string) {
    let logType = "WARNING";
    //super.warn(message, context);
    this.logIntegrate({
      logType, message, stack
    });
  };

  // Error 로그
  error(message: any, stack?: string, context?: string) {
    let logType = "ERROR";
    //super.error(message, stack, context);
    this.logIntegrate({
      logType, message, stack
    });
  };  

//현재 날짜의 스트림이 없을 시 스트림 생성
  async cloudWatchStreamCreate(params: any) {
    try {
      let { now, logStreamName } = params
      return await global.cloudWatchLogs.createLogStream({
        logGroupName : logStreamName,
        logStreamName : now,
      }).promise().then((data) => {     
        global.logStreams = now;
      }).catch(exception => {
        global.logStreams = now;        
      });
    } catch (exception) {
      console.log("cloudWatchStreamCreate() Error : ", exception);
    }
  }

  // 로그 생성, 현재 날짜로 로그 생성한 후 같은 로그에 넣으려면 nextSequenceToken 필요하기 때문에 전역변수로 선언해놓음.
  async cloudWatchLogsInit(params: any) {
    try {
      let { logType, message, stack, logStreamName } = params;
			stack = stack ? stack : "";
      let initLogParams: any;
      if (nextSequenceToken == "") {   
        initLogParams = {
          logGroupName : logStreamName,
          logStreamName : logStreams,
          logEvents: [
            {
              message: `[${logType}] ${message} ${stack}`,
              timestamp: Date.now(),
            },
          ],
        }
      } else {
        initLogParams = {
          logGroupName : logStreamName,
          logStreamName : logStreams,
          logEvents: [
            {
              message: `[${logType}] ${message} ${stack}`,
              timestamp: Date.now(),
            },
          ],
          sequenceToken : nextSequenceToken
        }
      }
      return await cloudWatchLogs.putLogEvents(initLogParams).promise().then((data) => {
      nextSequenceToken = data.nextSequenceToken;
      }).catch(err => {
        console.log(err)
        const now: string = moment().format('YYYY-MM-DD');
        this.cloudWatchStreamCreate({
          now
        });
      });
    } catch (exception) {
      console.log("cloudWatchLogsInit() Error : ", exception);
    }
  }

  async logIntegrate(params: any){
    try {
			let logStreamName: string = process.env.MODE == "dev" ? "babayo-api-logs" : "babayo-api-logs-live";
			logStreams = global.logStreams
      let { logType, message, stack } = params;
      const now: string = moment().format('YYYY-MM-DD');
      if(logStreams == "" || now != logStreams) {
        //해당 스트림명으로 생성
        await this.cloudWatchStreamCreate({
          now, logStreamName
        });
      }
      await this.cloudWatchLogsInit({
        logType, message, stack, logStreamName
      });
    } catch (exception) {
			console.log("logIntegrate() Error : ", exception);
    }   
  }

3) Exception 처리 로직

import { ApiLogger } from "@common/logger/logger.service";
import { ArgumentsHost, Catch, ExceptionFilter } from "@nestjs/common";
import { Request, Response } from 'express';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  constructor(private logger:ApiLogger){}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();
    const req = ctx.getRequest<Request>();
    const status = exception.status == undefined ? "" : exception.response.status;
    const userAgent = req.headers["user-agent"] == "" ? "" : req.headers["user-agent"];
    const params = req.params;
    
    if(status) {
      this.logger.error(`${req.ip} ${req.originalUrl} ${req.method} ${status} ${userAgent} {
        "params": ${JSON.stringify(params)},
        "exception" : "${exception.stack}"
      }`);
      res.json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: req.url,
        exception: exception.message,
      });
    } else {
      this.logger.error(`${req.ip} ${req.originalUrl} ${req.method} ${userAgent} {
        "params": ${JSON.stringify(params)},
        "exception": "${exception.stack}"
      }`);
      res.json({
        statusCode: 40001,
        timestamp: new Date().toISOString(),
        path: req.url,
        exception: exception.message,
      });
    }
  }
}

4) Controller

  • HTTP Exception 적용
@ApiOperation({
		summary: '바바요 전체 회원수'
	})
	@Get('userAllCount')
	async getUserAllCount(@Request() req: any){
		try {
			return await this.userService.getUserAllCount();
		} catch (exception) {
			throw new HttpException(`[${UserController.name}] ${req.originalUrl} - ${exception}`, 40001);
		}
	}

5) Service

  • Service Unavailable Exception 적용
throw new ServiceUnavailableException(`${AdminService.name}`, err);

5. 추가사항

  • CLI 명령어 사용하여, 로그 실시간으로 로컬에서 볼 수 있게 셋팅
  • Logs format (콤마 ,) 사용관련 체크

 

6. AWS CLI

  • aws logs tail "babayo-api-logs" --follow
  • aws logs tail "babayo-api-logs" --follow --format short --filter-pattern "ERROR”
반응형