[GCP/Cloud Logging] NestJs에서 winston을 사용해서 google cloud logging sink router 설정하기.

resilient

·

2022. 12. 2. 16:32

728x90
반응형
③ 개인정보처리자는 제1항 및 제2항에 의한 권한 부여, 변경 또는 말소에 대한 내역을 기록하고, 그 기록을 최소 3년간 보관하여야 한다.

 

 

제가 속해있는 SecOps팀에서는 admin project내에서의 권한관리를 어떻게 할 것인지, 그리고 권한관리를 효율적으로 하기 위해서 많은 노력을 기울이고 있습니다.

 

지금까지는 슬랙 쓰레드를 이용해서 권한 요청을 하고, 쓰레드를 이용해서 log를 남기곤 했었는데요. 이제는 개발적으로 확실하게 audit log를 관리해보고자 gcp cloud logging을 도입했고, 그 과정을 정리해보고자 합니다.

 

0. 왜 winston인가?

 

많은 Node.js logging module중(morgan, bunyan등등..) winston을 사용한 이유는 multiple transport가 가능하다는 점입니다.

multiple transport가 가능해짐으로써 cloud logging랑 같이 쓰기에 좋다고 판단했죠.

 

또한 gcp에서 제공하는 sdk중 @google-cloud/logging-winston 이라는 라이브러리가 있다는 점도 굉장히 큰 장점으로 다가왔습니다.

 

NestJs에서 winston을 사용하는 방법을 실습을 통해서 알아보겠습니다.

 

1. NestJs에서 winston사용하기

 

먼저 아래 두개의 패키지를 설치해줍니다. 각각 사용법은 아래에서 설명해드리도록 하죠.

yarn add winston @google-cloud/logging-winston

 

winston은 말그대로 winston 라이브러리이고 @google-cloud/logging-winston 은 gcp log에 찍힐때의 log들을 custom해서 사용할 수 있는 객체를 제공하는 라이브러리 입니다.

 

아래 코드를 보겠습니다.

 

import { LoggingWinston } from '@google-cloud/logging-winston';
import { Injectable } from '@nestjs/common';
import * as winston from 'winston';

@Injectable()
export class LoggerService {
  constructor() {}

  async logger(actionType: string) {
    const loggingWinston = new LoggingWinston({
      logName: `${actionType}`,
    });

    const {
      combine,
      json: jsonFormat,
      timestamp,
    } = winston.format;

    const logger = winston.createLogger({ //winston logger 생성
      level: 'info',
      transports: [loggingWinston],
      format: combine( // format 옵션 여러개를 적용할 시, combine을 사용해야 합니다.
        jsonFormat(),
        timestamp({ format: 'isoDateTime' }),
      ),
    });

    return logger;
  }
}

 

위 코드는 LoggerService Class 코드입니다. logger 라는 async 함수를 만들어서 다른 Service에서도 DI를 받아서 사용할 수 있게 설계했습니다. 

 

아래코드는 @google-cloud/logging-winston에서 불러와서 사용하는 부분인데요. logName을 default값이 아닌 logger라는 함수의 파라미터로 받는 값으로 지정해줍니다. 이렇게 되면 사용자가 원하는 logName을 사용할 수 있고, log router를 통해 원하는 log bucket안에 쌓을 수 있죠. 

 

(logName은 사용자가 원하는 custom logName을 사용할 경우 projects/[PROJECT_ID]/logs/[LOG_ID] 와 같은 포맷으로 남게됩니다.)

const loggingWinston = new LoggingWinston({
      logName: `${actionType}`,
    });

 

위와 같이 module을 생성하고 , service 레이어에서 위에서 만들었던 logger를 사용하는 코드를 보겠습니다.

 

const log = await this.loggerService.logger('admin-user-update-log');

    log.info('admin-user-update-log', {
      uid: `${user.uid}`, // 바뀌는 사람
      email: `${user.email}`,
      editor: `${editor.email}`, // 권한을 바꾸는 사람
      previousRoles: user.roles,
      newRoles: updateAppUserDto.roles,
    });

 

log.info를 사용해서 json타입으로 원하는 logger type을 만들어서 넘길 수 있습니다.

 

위 로직을 api logic에 추가하고, api call을 해서 cloud log에서 확인을 해보면,

 

Log example

 

위 로그 처럼 제대로 들어오는 것을 확인할 수 있습니다.

 

2. 그렇다면 log router + sink를 통해 log bucket에 넣어보겠습니다.

 

admin-user-update-log라는 logName을 가진 로그들만 따로 관리를 해야만 합니다. default bucket에는 log를 붙여놓은 모든 부분이 쌓여있고, 해당 로그를 관리하려면 꽤나 귀찮기 때문이죠.

 

GCP log 제공 서비스중 Logs Storage라는 GCS가 아닌 Log만 관리할 수 있는 Storage가 있다는 걸 알게 되었고 사용하게 되었습니다.

Logs Storage

CREATE LOG BUCKET을 이용해서 admin-user-update-logs라는 bucket을 만들어주고

 

admin-user-update-logs bucket 생성

admin-user-update-logs에 logName이 admin-user-update-log인 로그들만 routing 될 수 있도록 Logs Router에서 아래와 같은 방식으로 sink를 생성해줍니다. 

 

router sink 추가.

 

모든 준비가 끝났습니다.

 

다시 api call을 날려서 방금 만든 log bucket에 log가 제대로 들어가는지 확인해보겠습니다.

 

admin-user-update-logs bucket안에 들어있는 하나의 로그

 

위와 같이 admin-user-update-log의 logName을 가진 log만 logbucket에 들어오는걸 확인할 수 있습니다.

 

이렇게 logName을 이용해서 log를 관리하면 나중에 아래와 같이 쿼리를 이용해서 log를 쉽게 검색할 수 있습니다.

 

log를 검색할때의 query

 

3. 정리

 

이번 시간에는 log를 cloud logging + winston + logs Router(w. sink) 를 이용해서 쉽게 관리할 수 있는 프로세스를 만들어 봤습니다.

물론 이제 최선이고 완벽한 로깅 시스템이냐? 라고 물었을 때 부족한 부분이 많지만, 현재 저희 팀에서 사용하기에는 아주 충분하고 적합한 로깅 시스템이라고 생각이드네요.

 

읽어주셔서 감사합니다.

반응형