로그는 중요하다. 로그는 애플리케이션, 네트워크 또는 서버에서 발생하는 모든 일에 대한 기록이기 때문이다. 즉, 애플리케이션 모니터링, 오류 추적 및 오류 보고를 위한 기반이 되어주므로 옵저버빌리티에 반드시 필요한 요소이다.
그러나 불행히도 일반적으로 로그는 호출된 서비스와 같은 상황별 정보가 부족하기 때문에 코드 실행을 추적하는데는 그다지 유용하진 않다.
* 옵저버빌리티(Observability) 란?: 로그, 메트릭 및 추적 등 시스템에서 생성하는 데이터를 기반으로 시스템의 현재 상태를 측정하는 기능
그렇다면 코드 실행까지 추적하기 위해선 어떻게 하는게 좋을까?
보통 OpenTelemetry 과 같은 추적 관련 서비스를 심는듯 하나, 우린 이전에 DataDog 를 우리의 애플리케이션에 심었었다. (참고: https://yonikim.tistory.com/128)
DataDog 에서도 trace 기능을 제공하는데 traceId, spanId 를 추출할 수도 있다.
추출해낸 traceId, spanId 를 로그에 추가해주면 request 부터 response 까지 하나의 사이클 내에서 실행되는 모든 코드를 추적 가능하다는 말씀!
* span 은 작업 단위를 나타낸다. 요청이 수행하는 특정 작업을 추적하여 해당 작업이 실행되는 동안 발생한 일을 보여준다.
* trace 는 마이크로서비스 및 서버리스 애플리케이션과 같은 다중 서비스 아키텍처를 통해 전파되는 요청이 취한 경로를 기록한다.
먼저 Nest 에서 기본적으로 제공해주는 LoggerService 를 implement 받아서 새로운 로그서비스를 구축한다.
▷ trace-logger.service.ts
import { Injectable, LoggerService } from "@nestjs/common";
import { ExecutionContext } from "@nestjs/common";
import tracer from "dd-trace";
import { formats } from "dd-trace/ext";
import { isString } from "lodash";
import { TraceLoggerType } from "../enums/trace-logger.enum";
@Injectable()
export class TraceLoggerService implements LoggerService {
trace(type: TraceLoggerType, message: any, context?: ExecutionContext) {
const now = new Date();
const className = isString(context)? context : context?.getClass().name;
const methodName = isString(context)? undefined : context?.getHandler().name;
const traceLog = {
type,
createdDate: now,
className,
methodName,
...message,
};
const span = tracer.scope().active();
if (span) {
tracer.inject(span.context(), formats.LOG, traceLog);
}
console.log(JSON.stringify(traceLog));
}
/**
* Write a 'log' level log.
*/
log(message: any, context?: any, ...rest: any[]): void {
this.trace(
TraceLoggerType.Debugging,
{ log: { message, level: "log" } },
context,
);
}
/**
* Write an 'error' level log.
*/
error(message: any, context?: any, ...rest: any[]) {
this.trace(
TraceLoggerType.Debugging,
{ log: { message, level: "error" } },
context,
);
}
/**
* Write a 'warn' level log.
*/
warn(message: any, context?: any, ...rest: any[]) {
this.trace(
TraceLoggerType.Debugging,
{ log: { message, level: "warn" } },
context,
);
}
/**
* Write a 'debug' level log.
*/
debug?(message: any, context?: any, ...rest: any[]) {
this.trace(
TraceLoggerType.Debugging,
{ log: { message, level: "debug" } },
context,
);
}
/**
* Write a 'verbose' level log.
*/
verbose(message: any, context?: any) {
this.trace(
TraceLoggerType.Debugging,
{ log: { message, level: "verbose" } },
context,
);
}
}
▷ trace-logger.enum.ts
export enum TraceLoggerType {
Debugging = "DEBUGGING",
Request = "REQUEST",
Response = "RESPONSE",
}
request, response 로깅도 필요하기 때문에 interceptor 도 함께 구현해줬다. (다른 더 좋은 방법이 있다면 댓글로 좀...💜)
health 체크 요청과 GET 요청의 response 는 굳이 로깅이 필요하지 않다고 생각하여 로깅 패쓰
▷ trace-logger.interceptor.ts
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
Logger,
NestInterceptor,
} from "@nestjs/common";
import { Observable, catchError, map, throwError } from "rxjs";
import { TraceLoggerService } from "../services/trace-logger.service";
import { TraceLoggerType } from "../enums/trace-logger.enum";
@Injectable()
export class TraceLoggerInterceptor implements NestInterceptor {
private readonly traceLoggerService = new TraceLoggerService();
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const { method, headers, url, params, query, body } = request;
if (url.includes("/health-check")) return next.handle();
const { statusCode } = response;
const record = {
headers,
request: { method, url, params, query, body },
};
this.traceLoggerService.trace(TraceLoggerType.Request, record, context);
return next
.handle()
.pipe(
map((response) => {
if (method == "GET") return response;
const log = { statusCode, response };
this.traceLoggerService.trace(TraceLoggerType.Response, log, context);
return response;
}),
)
.pipe(
catchError((err) =>
throwError(() => {
const log = { statusCode, response: err };
this.traceLoggerService.trace(
TraceLoggerType.Response,
log,
context,
);
throw err;
}),
),
);
}
}
위에서 작성한 TraceLoggerService 와 TraceLoggerInterceptor 를 전역에서 사용할 수 있도록 설정해준다.
▷ main.ts
import { HttpStatus, ValidationPipe, VersioningType } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import tracer from "dd-trace";
import { AppModule } from "./app.module";
import { TraceLoggerService } from "@libs/common/services/trace-logger.service";
import { TraceLoggerInterceptor } from "@libs/common/interceptors/trace-logger.interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
app.useLogger(new TraceLoggerService());
app.useGlobalInterceptors(new TraceLoggerInterceptor());
tracer.init({
logInjection: true,
});
}
bootstrap();
'TypeScript' 카테고리의 다른 글
[TypeScript] as const (0) | 2024.10.16 |
---|---|
[Nest.js] 카프카(Kafka) 세팅하기 (0) | 2023.12.07 |
[Nest.js] 버전 별로 스웨거 관리 (0) | 2023.01.11 |
[TypeORM] 데코레이터 - Entity (1) | 2022.09.19 |
[Nest.js] Custom Decorator (0) | 2022.04.12 |