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로 전달하는 매우 중요한 기능이므로 꼭 구현해보고 싶은 욕심이 생겼다. 몇시간 동안의 고생 끝에 결국은 성공했다. 아래에서는 시도하고, 실패하고, 성공한 내용을 정리해보고자 한다.
동작하는 코드부터 살펴보자.
문제의 핵심은 winstonModule
의 transport
리스트에 전달되는 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;
}
}
구체적인 트러블슈팅과정은 나중에 추가해야지..
typeorm 모듈을 동기적으로 로드
Connection
getRepository
log.repository.ts
autoLoadEntities: true
를 활용하자.typeormModuleOptions
에서 autoLoadEntities: true
를 활용 하면 entity 로드시 경로설정 관련 에러가 해결되는 경우가 많으므로 상당부분 시간을 절약할 수 있다. 절대경로로 정확하게 지정해주는 방법이 있지만, 프로덕션레벨이 아니라면 굳이 편리한 도구 사용을 마다할 필요는 없다.
엇, 질문자인데 우연히 검색하다 글을 발견했네요! 뭔가 글로 포스팅까지 해주시니 더욱 감사합니다 😀