본문 바로가기
TypeScript

[Nest.js] DataDog 를 이용하여 trace id, span id 심기

by yonikim 2023. 10. 17.
728x90

로그는 중요하다. 로그는 애플리케이션, 네트워크 또는 서버에서 발생하는 모든 일에 대한 기록이기 때문이다.  즉, 애플리케이션 모니터링, 오류 추적 및 오류 보고를 위한 기반이 되어주므로 옵저버빌리티에 반드시 필요한 요소이다. 

그러나 불행히도 일반적으로 로그는 호출된 서비스와 같은 상황별 정보가 부족하기 때문에 코드 실행을 추적하는데는 그다지 유용하진 않다.

* 옵저버빌리티(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();
728x90

'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