서비스가 커지면 커질수록 동작 과정을 남기고 추적하는 일이 중요해진다.
이슈가 발생한 지점과 콜스택이 함께 제공된다면 디버깅이 매우 수월해질것이다.
내장 Logger는 @nest/common
패키지로 제공되며, 로깅옵션을 조절하면 다음과 같이 로깅 시스템의 동작을 제어할 수 있다.
log
,error
,warn
,debug
,verbose
내장 로거의 인스턴스는 로그를 남기고자 하는 부분에서 직접 생성해서 사용할 수 있다.
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class AppService {
//AppService.name으로 로그가 나오는 위치를 지정했다. (물론 다른 문자열로 써도됌)
private readonly logger = new Logger(AppService.name);
getHello(): string {
this.logger.error('level: error');
this.logger.warn('level: warn');
this.logger.log('level: log');
this.logger.verbose('level: verbose');
this.logger.debug('level: debug');
return 'Hello World!';
}
}
const app = await NestFactory.create(AppModule, {
logger: false,
});
추천은 안하지만 로그가 아에 뜨지 않게 만들 수 있다.
const app = await NestFactory.create(AppModule, {
logger:
process.env.NODE_ENV === 'prod'
? ['error', 'warn', 'log']
: ['debug', 'error', 'log', 'verbose', 'warn'],
});
일반적으로 빌드환경에선 debug 로그가 남지 않도록 하는게 좋다.
민감한 정보를 포함되고, 디버그 로그는 그 자체로 사이즈가 크기때문에 상용환경에선 좋은 퍼포먼스를 기대하기 힘들다.
애초에 상용환경에서 디버그를 켜놓는게 아무 의미가 없다.
로그가 몇개 안나오면 그냥 좀 살펴보면 되지만, 서비스를 하면 수 많은 로그가 쌓이고 거기에서 내가 원하는 로그를 찾기란 하고싶지도 않고 힘든 일이다. 때문에 로그를 데이터베이스에 저장해 검색을 하는 커스텀로거를 만들어 볼 수 있다. 내 포스팅에선 커스텀 로그를 만들기 위한 기본 재료만 알려줄것이다.
import { ConsoleLogger, Injectable, Module } from '@nestjs/common';
@Injectable()
export class MyLogger extends ConsoleLogger {
error(message: any, stack?: string, context?: string) {
super.error.apply(this, arguments);
}
private doSomething() {
// log에 대한 부가 로직을 넣어준다.
// ex. DB에 저장
}
}
이렇게 해주고 루트모듈이나, 글로벌 모듈에 등록만 해주면 전역적으로 종속성을 주입하여 사용할 수 있다.
Nestjs에서 공식적으로 패키징하고있는 로그패키지이다.
커스텀로그로 DB에 저장, 추적, 검색을 구현할 수도 있지만 많은 수고가 필요하다 Winston을 이용하면 간단하게 구현이 가능하다.
근데 난 내장 로거를 커스텀해서 사용하는게 더 편하더라..
패키지 설치
#npm $ npm i -s nest-winston winston
#yarn
$ yarn add nest-winston winston
### 설정
```ts
WinstonModule.forRoot({
transports: [
new winston.transports.Console({
//로그 레벨 지정
level: process.env.NODE_ENV === 'prod' ? 'info' : 'silly',
format: winston.format.combine(
//로그 남긴 시간 추가
//로그에 appName('ArtlogyApp')남기며, 로그에 색조합을 주는 prettyPrint true
winston.format.timestamp(),
nestWinstonModuleUtilities.format.nestLike('ArtlogyApp', {
prettyPrint: true,
}),
),
}),
],
}),
{
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
}
숫자가 낮을 수록 레벨이 높으며 설정된 레벨보다 낮은 로그가 출력된다.
import { Logger as WinstonLogger } from 'winston';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
...
export class UsersController {
constructor(
@Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: WinstonLogger,
) { }
@Post()
async createUser(@Body() dto: CreateUserDto): Promise<void> {
this.printWinstonLog(dto);
...
}
private printWinstonLog(dto) {
console.log(this.logger.name);
this.logger.error('error: ', dto);
this.logger.warn('warn: ', dto);
this.logger.info('info: ', dto);
this.logger.http('http: ', dto);
this.logger.verbose('verbose: ', dto);
this.logger.debug('debug: ', dto);
this.logger.silly('silly: ', dto);
}
...
}
WINSTON_MODULE_NEST_PROVIDER
토큰으로 winston에 제공하는 Logger객체를 주입 받을 수 있다.
forRoot나 forRootAsync으로 모듈을 만들면 부트스트랩까지 대체할 수 없기 때문에
다음과 같이 만들어준다.
import { AuthGuard } from '@config/guard/jwt.guard';
import { swaggerConfig } from '@config/swagger.config';
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app/app.module';
import * as winston from 'winston';
import {
WINSTON_MODULE_NEST_PROVIDER,
utilities as nestWinstonModuleUtilities,
WinstonModule,
} from 'nest-winston';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
transports: [
new winston.transports.Console({
level: process.env.NODE_ENV === 'prod' ? 'info' : 'silly',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.ms(),
nestWinstonModuleUtilities.format.nestLike('ArtlogyApp', {
prettyPrint: true,
}),
),
}),
],
}),
});
app.useGlobalPipes(
new ValidationPipe({
// 변환 허용
transform: true,
// 유효성 검사 데코레이터가 없는 필드를 요청 할 경우 건너뜀
whitelist: true,
// 유효성 검사 데코레이터가 없는 필드가 있을 경우 에러를 반환
forbidNonWhitelisted: true,
// null, undefined값을 가진 객체는 유효성 검사를 건너뜀
skipMissingProperties: true,
}),
);
const authGuard: AuthGuard = app.get<AuthGuard>(AuthGuard);
app.useGlobalGuards(authGuard);
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api', app, swaggerDocument);
await app.listen(8084, () => {
console.log('server running');
});
}
bootstrap();
로그를 남길 모듈에 Logger 서비스를 프로바이더로 선언하면 된다.
import { Logger } from '@nestjs/common';
...
@Module({
...
providers: [Logger]
})
export class UsersModule { }
이제 @nestjs/common 패키지의 Logger 또는 LoggerService를 다음처럼 주입받을 수 있다.
import { Logger } from '@nestjs/common';
...
export class UsersController {
constructor(
@Inject(Logger) private readonly logger: LoggerService,
) { }
...
}
winston을 이용하거나 커스텀을 이용하는 가장 큰 이유는 파일이나 DB에 저장해서 로그를 활용하고자 함이다. 최근에는 New Relic, DataDog같은 외부 유로 서비스에 전송해서 분석과 시각화 툴을 활용하기도 한다.
winston-transport
라이브러리로 지속적인 로그 전달이 가능하다.
난 돈이 없기 때문에 이 부분은 그냥 넘어 갈 것인데 관심있음 레퍼런스 많으니 찾아보면 좋다.
(물론 DB에 넣고 웹으로 내가 만들어도 된다. )
💡 상용 서비스에서 에러 정보, 호출 스택 등의 정보와 사용자가 추가한 로그를 파일에 기록하고 기록된 파일을 외부 서비스에 다시 전달하여 검색과 시각화를 한다. 구현시 웹프레임워크나 서비스 업체에서 제공하는 라이브러리를 활용한다.