[Nest.js]log stream을 db로 전송하기

Donghun Seol·2023년 5월 4일
0

0. nest.js 디스코드 채널

nest.js 학습중에 도움을 얻고자 디스코드 채널에 참여하고 있었다.
어제 어떤 유저분이 아래와 같이 로그관련 질문을 하셨다.

질문

winston-transport의 TransportStream을 상속받는 ApiLoggerStream을 활용해 WinstonModule을 구성할때, Typeorm의 Repository를 생성자 파라미터로 넣을 수 있는 방법이 뭐가 있을까요?

Typeorm은 별도의 모듈을 생성해서 그 안에 DataSource와 Repository Provider를 선언해 두었고 해당 모듈도 AppModule에 import 시켜두었습니다.
위와 같이 구성할 경우 logRepository 에 undefined가 넘어가게 됩니다. 뭘 놓친건지 잘 모르겠습니다. 도와주세요 🙏

// AppModule
WinstonModule.forRootAsync({
  useFactory: (logRepository: Repository<Log>) => {
    return {
      transports: [
        new ApiLoggerStream({
          level: STAGE === "prod" ? "info" : "silly",
          format: winston.format.combine(
            winston.format.timestamp, utilities.format.nestLike("PLATFORM", { prettyPrint: true }),
          ),
        }, logRepository),
      ]
    }
  }
)}

나는 언뜻 나이브하게 생각하고 다음과 같이 답변을 했었는데 역시나 이대론 작동하지 않았다.


// AppModule
WinstonModule.forRootAsync({

  // 이렇게 주입하면 되지 않을까요?
  imports: [TypeOrmModule],
  inject: [logRepository],
  // 이렇게

  useFactory: (logRepository: Repository<Log>) => {
    return {
      transports: [
        new ApiLoggerStream({
          level: STAGE === "prod" ? "info" : "silly",
          format: winston.format.combine(
            winston.format.timestamp, utilities.format.nestLike("PLATFORM", { prettyPrint: true }),
          ),
        }, logRepository),
      ]
    }
  }
)

유저께서 질문하신 내용은 로그스트림을 db로 전달하는 매우 중요한 기능이므로 꼭 구현해보고 싶은 욕심이 생겼다. 몇시간 동안의 고생 끝에 결국은 성공했다. 아래에서는 시도하고, 실패하고, 성공한 내용을 정리해보고자 한다.

1. 성공

동작하는 코드부터 살펴보자.
문제의 핵심은 winstonModuletransport리스트에 전달되는 ApiLoggerTransport의 생성자가 제대로된 DB respository를 인자로 전달받지 못하는 것이었다.

내 생각엔 다른 모듈에서 해당 repository를 주입받아 사용하려면 해당 repository는 네스트런타임의 IoC컨테이너에서 미리 인스턴스화된 상태로 존재해야 한다. 따라서 네스트가 인스턴스화 할 수 있도록 provider로 작성해 주어야 한다. 그렇지 않으면 오류가 발생한다.(뇌피셜)

결국은 해당 repository를 커스텀 프로바이더로 선언하고, 이를 winston Module에 주입해줌으로써 해결했다.

아래에서 LogRepositoryProvider가 커스텀 프로바이더다. 얘는 typeorm의 Repository를 주입받아서 Repository<Log>타입의 respository를 반환한다. 또한 winston.module.ts 파일에 인라인으로 커스텀 프로바이더 형식으로 작성되어 있으므로 자동으로 네스트 런타임이 인스턴스화 해준다.

따라서 다음과 같이'LogRepository'라는 식별자로 inject해줄 수 있다.
inject: ['LogRepository'],

이제 useFactory에 정의된 함수에서 네스트 런타임은 inject를 참조하여 logRepository 파라미터에 올바른 프로바이더를 주입할 수 있다. 이후 ApiLoggerTransport에 제대로 인스턴스화된 logRepository가 전달되서 제대로 동작하게 된다.

아래의 파일이 winston.module.ts 전체 파일이다.

// winston.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WinstonModule } from 'nest-winston';
import { Log } from './entity/Log.entity';
import { LogRepository } from './log.repository';
import * as winston from 'winston';
import { ApiLoggerTransport } from './api-logger.transport';
import { Repository } from 'typeorm';

const LogRepositoryProvider = {
  provide: 'LogRepository',
  useFactory: (repository: Repository<Log>) => repository,
  inject: [Repository],
};

@Module({
  imports: [
    WinstonModule.forRootAsync({
      imports: [TypeOrmModule.forFeature([Log])],
      inject: ['LogRepository'],
      useFactory: async (logRepository: Repository<Log>) => {
        const transportOptions = {
          level: 'silly',
          format: winston.format.combine(winston.format.timestamp()),
        };
        return {
          transports: [
            new ApiLoggerTransport(transportOptions, logRepository),
            new winston.transports.Console(),
          ],
        };
      },
    }),
  ],
})
export class MyWinstonModule {}

이제 잘 세팅된 로거를 사용하고 싶은 서비스에서 MyWinstonModule을 주입받아서 사용하면된다. 먼저 모듈 레벨에서 MyWinstonModule을 import 해주고, 서비스에서는 아래와 같이 주입받아서 사용하면된다. 주입받은 로거는 로그를 콘솔에 출력하고, 또 DB에 저장한다.

// myCron.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { Logger as WinstonLogger } from 'winston';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
@Injectable()
export class TaskService {
  // private readonly logger = new Logger(TaskService.name);
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: WinstonLogger){}
}

DB에 로그를 직접 저장하는 transport설정인 api-logger.transport.ts의 세부 구현은 아래와 같다.

// api-logger.transport.ts
import TransportStream = require('winston-transport');
import { Log } from './entity/Log.entity';
import { LogRepository } from './log.repository';

export class ApiLoggerTransport extends TransportStream {
  constructor(
    options?: TransportStream.TransportStreamOptions,
    private readonly logRepository?: LogRepository,
  ) {
    super(options);
  }

  log(info: any, callback: () => void) {
    setImmediate(() => {
      this.emit('logged', info);
    });

    if (this.logRepository) {
      const logEntry = this.transformLogEntry(info); // Customize this method to transform the log data to match your Log entity

      this.logRepository
        .save(logEntry)
        .then(() => {
          callback();
        })
        .catch((error) => {
          console.error(error);
          this.emit('error', error);
          callback();
        });
    } else {
      callback();
    }
  }

  // Implement the necessary methods and logic to transform the log data to match your Log entity
  private transformLogEntry(info: any): Log {
    // Customize this method to transform the log data to match your Log entity
    const logEntry = new Log();
    logEntry.message = info.message;
    logEntry.level = info.level;
    // Set other properties of the Log
    return logEntry;
  }
}

2. 실패

구체적인 트러블슈팅과정은 나중에 추가해야지..

typeorm 모듈을 동기적으로 로드

  • 별 관계 없었음. 인스턴스화를 안해준게 문제

Connection

getRepository

log.repository.ts

3. 느낀 점

  1. 기본적으로 typeorm 설정에서 autoLoadEntities: true를 활용하자.

    위와 같은 에러 발생시 네스트 런타임이 entity파일을 제대로 로드하지 못한 경우가 많았다. 이 경우에는 typeormModuleOptions에서 autoLoadEntities: true를 활용 하면 entity 로드시 경로설정 관련 에러가 해결되는 경우가 많으므로 상당부분 시간을 절약할 수 있다. 절대경로로 정확하게 지정해주는 방법이 있지만, 프로덕션레벨이 아니라면 굳이 편리한 도구 사용을 마다할 필요는 없다.
  2. 내부동작을 잘 이해하고 있어야 디버깅이 쉽다.
    커스텀 프로바이더의 작동원리를 탄탄히 이해하고 넘어가자.
    커스텀 프로바이더를 자유롭게 구현하고 테스팅에 사용할 수 있어야 한다.
    해당 내용은 꼭 학습하고, 별도의 포스팅으로 정리하자!
profile
I'm going from failure to failure without losing enthusiasm

5개의 댓글

comment-user-thumbnail
2023년 5월 4일

엇, 질문자인데 우연히 검색하다 글을 발견했네요! 뭔가 글로 포스팅까지 해주시니 더욱 감사합니다 😀

1개의 답글
comment-user-thumbnail
2023년 5월 12일

sample mocking comment for api test

답글 달기
comment-user-thumbnail
2023년 5월 13일

sample mocking comment for api test

답글 달기
comment-user-thumbnail
2023년 5월 13일

sample mocking comment for api test

답글 달기