NestJS 기본 Logger의 한계
NestJS 내장 Logger는 개발 환경에서는 충분하지만, 프로덕션에서는 구조화된 JSON 로깅, 요청별 상관 ID(Correlation ID), 로그 레벨 동적 제어, 외부 수집 시스템 연동이 필수입니다.
이 글에서는 Pino 기반 구조화 로깅 구축, AsyncLocalStorage를 활용한 요청 컨텍스트 전파, 커스텀 LoggerService 구현, 그리고 ELK/Datadog 연동까지 실전 패턴을 다룹니다.
nestjs-pino: 구조화 로깅의 표준
Pino는 Node.js에서 가장 빠른 JSON 로거입니다. nestjs-pino 패키지가 NestJS와의 통합을 제공합니다.
npm install nestjs-pino pino-http pino-pretty
// app.module.ts
import { LoggerModule } from 'nestjs-pino';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
// 프로덕션: JSON, 개발: pretty print
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
level: process.env.LOG_LEVEL || 'info',
// 요청/응답 자동 로깅 설정
autoLogging: true,
// 민감 정보 마스킹
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'req.body.password',
'req.body.token',
],
censor: '[REDACTED]',
},
// 커스텀 직렬화
serializers: {
req(req) {
return {
method: req.method,
url: req.url,
query: req.query,
params: req.params,
// body는 POST/PUT만
...((['POST', 'PUT', 'PATCH'].includes(req.method))
&& { body: req.raw.body }),
};
},
res(res) {
return { statusCode: res.statusCode };
},
},
// 커스텀 속성 추가
customProps: (req) => ({
context: 'HTTP',
}),
// 요청 ID 자동 생성
genReqId: (req) =>
req.headers['x-request-id'] || crypto.randomUUID(),
},
}),
],
})
export class AppModule {}
// main.ts — NestJS 내장 Logger를 Pino로 교체
import { Logger } from 'nestjs-pino';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(app.get(Logger));
await app.listen(3000);
}
bootstrap();
이제 모든 로그가 구조화된 JSON으로 출력됩니다:
{"level":30,"time":1709060000000,"pid":1234,"hostname":"api-pod-xyz",
"reqId":"550e8400-e29b-41d4-a716","req":{"method":"POST","url":"/api/users"},
"msg":"request completed","responseTime":45,"res":{"statusCode":201}}
AsyncLocalStorage: 요청 컨텍스트 전파
서비스 레이어, 리포지토리, 유틸 함수 어디서든 현재 요청의 상관 ID, 유저 정보 등을 접근해야 합니다. AsyncLocalStorage가 이를 해결합니다.
npm install nestjs-cls
// app.module.ts
import { ClsModule } from 'nestjs-cls';
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
generateId: true,
idGenerator: (req) =>
req.headers['x-correlation-id'] as string || crypto.randomUUID(),
},
}),
],
})
export class AppModule {}
// 요청 컨텍스트에 유저 정보 세팅 (Guard 또는 Middleware에서)
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly cls: ClsService,
private readonly jwtService: JwtService,
) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = this.jwtService.verify(request.headers.authorization);
// CLS에 유저 정보 저장 → 어디서든 접근 가능
this.cls.set('userId', user.id);
this.cls.set('userEmail', user.email);
this.cls.set('correlationId', this.cls.getId());
return true;
}
}
// 서비스 어디서든 컨텍스트 접근
@Injectable()
export class OrderService {
private readonly logger = new Logger(OrderService.name);
constructor(
private readonly cls: ClsService,
private readonly orderRepo: OrderRepository,
) {}
async createOrder(dto: CreateOrderDto) {
const userId = this.cls.get('userId');
const correlationId = this.cls.getId();
this.logger.log({
msg: '주문 생성 시작',
correlationId,
userId,
productIds: dto.items.map(i => i.productId),
});
const order = await this.orderRepo.save({ ...dto, userId });
this.logger.log({
msg: '주문 생성 완료',
correlationId,
orderId: order.id,
totalAmount: order.totalAmount,
});
return order;
}
}
커스텀 LoggerService: Pino + CLS 통합
@Injectable()
export class AppLoggerService {
private readonly logger: PinoLogger;
constructor(
pinoLogger: PinoLogger,
private readonly cls: ClsService,
) {
this.logger = pinoLogger;
}
private enrichLog(obj: Record<string, any> = {}) {
return {
...obj,
correlationId: this.cls.getId(),
userId: this.cls.get('userId'),
traceId: this.cls.get('traceId'),
};
}
info(msg: string, obj?: Record<string, any>) {
this.logger.info(this.enrichLog(obj), msg);
}
warn(msg: string, obj?: Record<string, any>) {
this.logger.warn(this.enrichLog(obj), msg);
}
error(msg: string, error?: Error, obj?: Record<string, any>) {
this.logger.error({
...this.enrichLog(obj),
err: error ? {
message: error.message,
stack: error.stack,
name: error.name,
} : undefined,
}, msg);
}
debug(msg: string, obj?: Record<string, any>) {
this.logger.debug(this.enrichLog(obj), msg);
}
}
로그 레벨 동적 제어
// 런타임에 로그 레벨 변경 엔드포인트
@Controller('admin/logging')
@UseGuards(AdminGuard)
export class LoggingController {
constructor(@Inject('PinoLogger') private readonly pino: any) {}
@Put('level')
setLevel(@Body('level') level: string) {
// pino 인스턴스의 레벨 동적 변경
this.pino.logger.level = level; // 'debug', 'info', 'warn', 'error'
return { level: this.pino.logger.level };
}
@Get('level')
getLevel() {
return { level: this.pino.logger.level };
}
}
프로덕션에서 디버깅이 필요할 때 재배포 없이 로그 레벨을 변경할 수 있습니다. DI Scope를 활용하면 특정 요청에만 debug 레벨을 적용하는 것도 가능합니다.
ELK Stack 연동
# docker-compose.yml (로그 수집 파이프라인)
services:
app:
image: my-nestjs-app
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Filebeat → Elasticsearch 직접 전송
filebeat:
image: elastic/filebeat:8.12.0
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
# filebeat.yml
filebeat.inputs:
- type: container
paths: ['/var/lib/docker/containers/*/*.log']
processors:
- decode_json_fields:
fields: ["message"]
target: ""
overwrite_keys: true
output.elasticsearch:
hosts: ["elasticsearch:9200"]
index: "nestjs-logs-%{+yyyy.MM.dd}"
Pino의 JSON 출력은 ELK, Datadog, CloudWatch 등 모든 로그 수집 시스템과 파싱 없이 바로 연동됩니다.
성능 비교
- Pino: ~30,000 ops/sec — Node.js 최고 성능. 비동기 직렬화, 워커 스레드 지원
- Winston: ~10,000 ops/sec — 유연한 Transport, 다양한 플러그인
- NestJS 내장: console.log 래퍼 — 프로덕션 부적합
고트래픽 API에서는 Pino가 압도적으로 유리합니다. pino.destination()으로 파일 직접 쓰기 시 비동기 I/O로 이벤트 루프 블로킹을 최소화합니다.
실전 체크리스트
- JSON 구조화: 프로덕션은 반드시 JSON 포맷 (grep → jq로 검색)
- Correlation ID: 모든 로그에 요청 추적 ID 포함
- 민감 정보 마스킹: password, token, authorization 헤더 redact
- 로그 레벨: dev=debug, staging=info, prod=warn (동적 변경 가능하게)
- 에러 로깅: stack trace 포함, 구조화된 err 객체로
- 요청/응답: method, url, statusCode, responseTime 기본 포함
마무리
NestJS 프로덕션 로깅의 핵심은 Pino + AsyncLocalStorage(CLS) 조합입니다. 구조화된 JSON 로그에 요청별 Correlation ID가 자동 포함되면, 마이크로서비스 환경에서도 요청 흐름을 완전히 추적할 수 있습니다. Interceptor로 요청/응답 로깅을 자동화하고, Pipe 검증 실패도 구조화된 에러 로그로 남기면 운영 가시성이 크게 향상됩니다.