서비스를 실행하면 서버 콘솔에는 아래 그림과 같은 로그가 출력됩니다. 이미 각 컴포넌트에서는 내장 로거를 이용하여 로그를 출력하고 있다.
내장 Logger
클래스는 @nest/common
패키지로 제공됩니다. 로깅 옵션을 조절하면 다음과 같이 로깅 시스템의 동작을 제어할 수 있다.
log
, error
, warn
, debug
, verbose
import {Injectable,Logger }from '@nestjs/common';
@Injectable()
exportclassAppService {
private readonly logger =newLogger(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!';
}
}
일반적으로 프로덕션 환경에서는 debug 로그가 남지 않도록 하는 게 좋다.
실행환경에 따라 로그 레벨을 지정하는 경우가 보통이다.
일반적으로 프로덕션 환경에서는 debug 로그가 남지 않도록 하는 게 좋다. 디버그 로그는 테스트 과정에서 디버깅용으로 객체가 가지고 있는 세부 데이터까지 남기는 경우가 많은데 상용환경에서는 사용자의 민감 정보가 포함될 수 있기 때문에 제외한다. 디버깅 로그는 로그의 크기 자체도 큰 경우가 대부분이므로 로그 파일의 사이즈를 줄이기 위한 목적도 있다.
const app = await NestFactory.create(AppModule, {
logger: process.env.NODE_ENV === 'production'
? ['error', 'warn', 'log']
: ['error', 'warn', 'log', 'verbose', 'debug']
});
💡 만약 로그레벨을 하나만 설정한다면 해당 로그레벨보다 레벨이 낮은 레벨의 로그도 모두 함께 출력됩니다.따라서 debug로만 설정한다면 모든 로그가 출력된다.
const LOG_LEVEL_VALUES: Record<LogLevel, number> = {
debug: 0,
verbose: 1,
log: 2,
warn: 3,
error: 4,
};
export interface LoggerService {
log(message: any, ...optionalParams: any[]): any;
error(message: any, ...optionalParams: any[]): any;
warn(message: any, ...optionalParams: any[]): any;
debug?(message: any, ...optionalParams: any[]): any;
verbose?(message: any, ...optionalParams: any[]): any;
setLogLevels?(levels: LogLevel[]): any;
}
export class MyLogger extends ConsoleLogger {
error(message: any, stack?: string, context?: string) {
super.error.apply(this, arguments);
this.doSomething();
}
private doSomething() {
// 여기에 로깅에 관련된 부가 로직을 추가합니다.
// ex. DB에 저장
}
...
}
import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service';
@Module({
providers: [MyLogger],
exports: [MyLogger],
})
export class LoggerModule { }
import { LoggerModule } from './logging/logger.module';
@Module({
imports: [LoggerModule],
...
})
export class AppModule { }
import { MyLogger } from './logging/my-logger.service';
@Injectable()
export class AppService {
constructor(private myLogger: MyLogger) { }
getHello(): string {
this.myLogger.error('level: error');
this.myLogger.warn('level: warn');
this.myLogger.log('level: log');
this.myLogger.verbose('level: verbose');
this.myLogger.debug('level: debug');
return 'Hello World!';
}
}
@nestjs/common
패키지에서 제공하는 Logger
클래스를 이용하여 로깅을 구현하는 것도 가능하지만, 서비스를 상용 수준으로 운용하기 위해서는 로그를 콘솔에만 출력하는 게 아니라 파일에 저장을 하거나, 중요한 로그는 데이터베이스에 저장을 해서 쉽게 검색할 수 있도록 해야 한다.
$ npm i nest-winston winston
AppModule에 WinstonModule
을 import합니다.
import * as winston from 'winston';
import {
utilities as nestWinstonModuleUtilities,
WinstonModule,
} from 'nest-winston';
@Module({
imports: [
...
WinstonModule.forRoot({
transports: [
new winston.transports.Console({
level: process.env.NODE_ENV === 'production' ? 'info' : 'silly',
format: winston.format.combine(
winston.format.timestamp(),
nestWinstonModuleUtilities.format.nestLike('MyApp', { prettyPrint: true }),
),
}),
],
}),
],
})
export class AppModule { }
{
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
}
import { Logger as WinstonLogger } from 'winston';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
...
export class UsersController {
constructor(
@Inject(WINSTON_MODULE_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);
}
...
}
[MyApp] Error 5/21/2022, 4:20:00 AM error: - {}
[MyApp] Warn 5/21/2022, 4:20:00 AM warn: - {}
[MyApp] Info 5/21/2022, 4:20:00 AM info: - {}
[MyApp] Http 5/21/2022, 4:20:00 AM http: - {}
[MyApp] Verbose 5/21/2022, 4:20:00 AM verbose: - {}
[MyApp] Debug 5/21/2022, 4:20:00 AM debug: - {}
[MyApp] Silly 5/21/2022, 4:20:00 AM silly: - {}
먼저 main.ts에 전역 로거로 설정합니다.
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
await app.listen(3000);
}
bootstrap();
그 다음 로깅을 하고자 하는 곳에서 LoggerService
를 WINSTON_MODULE_NEST_PROVIDER
토큰으로 주입받습니다.
import { LoggerService } from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
...
export class UsersController {
constructor(
@Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService,
) { }
...
@Post()
async createUser(@Body() dto: CreateUserDto): Promise<void> {
this.printLoggerServiceLog(dto);
...
}
private printLoggerServiceLog(dto) {
try {
throw new InternalServerErrorException('test');
} catch (e) {
this.logger.error('error: ' + JSON.stringify(dto), e.stack);
}
this.logger.warn('warn: ' + JSON.stringify(dto));
this.logger.log('log: ' + JSON.stringify(dto));
this.logger.verbose('verbose: ' + JSON.stringify(dto));
this.logger.debug('debug: ' + JSON.stringify(dto));
}
}
[MyApp] Info 2021-11-19 9:32:07 ├F10: PM┤ [RoutesResolver] UsersController {/users}: - {}
[MyApp] Info 2021-11-19 9:32:07 ├F10: PM┤ [RouterExplorer] Mapped {/users, POST} route - {}
[MyApp] Info 2021-11-19 9:32:07 ├F10: PM┤ [RouterExplorer] Mapped {/users/email-verify, POST} route - {}
[MyApp] Info 2021-11-19 9:32:07 ├F10: PM┤ [RouterExplorer] Mapped {/users/login, POST} route - {}
[MyApp] Info 2021-11-19 9:32:07 ├F10: PM┤ [RouterExplorer] Mapped {/users/:id, GET} route - {}
[MyApp] Info 2021-11-19 9:32:07 ├F10: PM┤ [NestApplication] Nest application successfully started - {}
Nest의 의존성 주입은 한가지 단점이 있는데, 부트스트래핑 과정(모듈, 프로바이더, 의존성 주입 등을 초기화)에서 WinstonLogger
는 아직 사용이 불가하다.
[Nest] 88819 - 2021-11-19 9:39:28 ├F10: PM┤ LOG [InstanceLoader] ConfigHostModule dependencies initialized +2ms
[Nest] 88819 - 2021-11-19 9:39:28 ├F10: PM┤ LOG [InstanceLoader] WinstonModule dependencies initialized +0ms
[Nest] 88819 - 2021-11-19 9:39:28 ├F10: PM┤ LOG [InstanceLoader] EmailModule dependencies initialized +0ms
[Nest] 88819 - 2021-11-19 9:39:28 ├F10: PM┤ LOG [InstanceLoader] AuthModule dependencies initialized +1ms
[Nest] 88819 - 2021-11-19 9:39:28 ├F10: PM┤ LOG [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 88819 - 2021-11-19 9:39:28 ├F10: PM┤ LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +83ms
[Nest] 88819 - 2021-11-19 9:39:28 ├F10: PM┤ LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 88819 - 2021-11-19 9:39:28 ├F10: PM┤ LOG [InstanceLoader] UsersModule dependencies initialized +2ms
import { 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 === 'production' ? 'info' : 'silly',
format: winston.format.combine(
winston.format.timestamp(),
nestWinstonModuleUtilities.format.nestLike('MyApp', { prettyPrint: true }),
),
}),
],
})
});
await app.listen(3000);
}
bootstrap();
import { Logger } from '@nestjs/common';
...
@Module({
...
providers: [Logger]
})
export class UsersModule { }
import { Logger } from '@nestjs/common';
...
export class UsersController {
constructor(
@Inject(Logger) private readonly logger: LoggerService,
) { }
...
}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [NestFactory] Starting Nest application... - {}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [InstanceLoader] AppModule dependencies initialized - {}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [InstanceLoader] TypeOrmModule dependencies initialized - {}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [InstanceLoader] ConfigHostModule dependencies initialized - {}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [InstanceLoader] EmailModule dependencies initialized - {}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [InstanceLoader] AuthModule dependencies initialized - {}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [InstanceLoader] ConfigModule dependencies initialized - {}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [InstanceLoader] TypeOrmCoreModule dependencies initialized - {}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [InstanceLoader] TypeOrmModule dependencies initialized - {}
[MyApp] Info 2021-11-19 9:55:16 ├F10: PM┤ [InstanceLoader] UsersModule dependencies initialized - {}
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { v4 as uuid } from 'uuid';
import { PrivacyReplacer } from './PrivacyReplacer';
import { clone } from 'ramda';
@Injectable()
export class LogInterceptor implements NestInterceptor {
private requestLogger = new Logger('HTTP_REQUEST');
private responseLogger = new Logger('HTTP_RESPONSE');
constructor(
@Inject('LOGGING_IGNORE_PATH') private readonly loggingIgnorePath: string[],
private readonly privacyReplacer: PrivacyReplacer,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const executionId = uuid();
this.logRequest(context, executionId);
return next.handle().pipe(
catchError((error: any) => {
console.log(error);
this.logResponse(context, executionId, error);
throw error;
}),
tap((data) => {
this.logResponse(context, executionId, data);
}),
);
}
private logResponse(
context: ExecutionContext,
executionId: string,
data: any,
) {
if (this.isIgnorePath(context)) return;
const response = context.switchToHttp().getResponse<Response>();
const loggingParams: Record<string, any> = {
executionId,
user: this.getUser(context),
headers: this.privacyReplacer.replaceResponseHeader(
clone(response.header),
),
};
if (data instanceof Error) {
loggingParams.error = { message: data.message };
} else {
loggingParams.body = this.privacyReplacer.replaceResponseBody(
clone(data),
);
}
this.responseLogger.log(JSON.stringify(loggingParams));
}
private logRequest(context: ExecutionContext, executionId: string) {
if (this.isIgnorePath(context)) return;
const request = context.switchToHttp().getRequest<Request>();
this.requestLogger.log(
JSON.stringify({
executionId,
user: this.getUser(context),
path: request.path,
body: this.privacyReplacer.replaceRequestBody(clone(request.body)),
headers: this.privacyReplacer.replaceRequestHeader(
clone(request.headers),
),
}),
);
}
private getUser(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
if (request.user == null || typeof request.user !== 'object') {
return null;
}
const user = { ...request.user };
delete user.password;
return user;
}
private isIgnorePath(context: ExecutionContext): boolean {
return this.loggingIgnorePath.includes(
context.switchToHttp().getRequest<Request>().path,
);
}
}