Skip to content

meshestra/nestjs-otel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm version License: MIT TypeScript NestJS OpenTelemetry


@meshestra/otel

@meshestra/otelNestJS 애플리케이션에 OpenTelemetry 기반의 관측 가능성(Observability)을 빠르게 통합하기 위한 패키지입니다.

  • Traces: OTLP HTTP로 분산 추적 데이터를 내보냅니다.
  • Metrics: OTLP HTTP로 주기적으로 메트릭을 내보냅니다.
  • Logs: Winston 로거를 통해 콘솔 및 TCP(Logstash 등)로 구조화된 로그를 전송합니다.

트레이스·메트릭·로그 세 신호를 단일 OtelModule.forRoot() 호출 하나로 초기화할 수 있으며, @Trace·@TraceEventHandler 데코레이터로 메서드 단위 계측을 선언적으로 추가할 수 있습니다.


Installation

# pnpm
pnpm add @meshestra/otel

# npm
npm install @meshestra/otel

# yarn
yarn add @meshestra/otel

Peer Dependencies

{
  "@nestjs/common": "^11.x",
  "@nestjs/core": "^11.x",
  "reflect-metadata": "^0.2.x",
  "rxjs": "^7.x",
  "winston": "^3.x"
}

Quick Start

1. SDK를 메인 진입점 최상단에서 초기화

OpenTelemetry SDK는 다른 모든 import보다 반드시 먼저 로드해야 자동 계측이 정상 동작합니다.

// src/instrument.ts -- 이 파일을 main.ts 보다 먼저 import
import { createOtelSDK } from '@meshestra/otel';

const { sdk } = createOtelSDK({
  serviceName: process.env.SERVICE_NAME ?? 'my-service',
  exporters: [
    { type: 'trace',  url: 'http://otel-collector:4318/v1/traces' },
    { type: 'metric', url: 'http://otel-collector:4318/v1/metrics' },
    { type: 'log',    url: 'tcp://logstash:5044', transport: 'tcp' },
  ],
});

sdk.start();
// src/main.ts
import './instrument'; // 반드시 가장 먼저

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

2. AppModule에 OtelModule 등록

// src/app.module.ts
import { Module } from '@nestjs/common';
import { OtelModule } from '@meshestra/otel';

@Module({
  imports: [
    OtelModule.forRoot({
      serviceName: process.env.SERVICE_NAME ?? 'my-service',
      exporters: [
        { type: 'trace',  url: process.env.OTEL_TRACE_URL! },
        { type: 'metric', url: process.env.OTEL_METRIC_URL! },
        { type: 'log',    url: process.env.LOG_URL!, transport: 'tcp' },
      ],
    }),
  ],
})
export class AppModule {}

3. 로거 주입

OtelModuleOTEL_LOGGER 토큰으로 Winston 로거를 제공합니다.

import { Inject, Injectable } from '@nestjs/common';
import { OTEL_LOGGER, OtelLogger } from '@meshestra/otel';

@Injectable()
export class OrderService {
  constructor(
    @Inject(OTEL_LOGGER) private readonly logger: OtelLogger,
  ) {}

  async createOrder() {
    this.logger.info('주문 생성 시작');
  }
}

Configuration

OtelConfig

interface OtelConfig {
  /** 서비스 식별자. OTLP resource attribute로 사용됩니다. */
  serviceName: string;

  /** 내보낼 신호(trace / metric / log)별 exporter 설정 목록 */
  exporters: ExporterConfig[];
}

ExporterConfig

interface ExporterConfig {
  /** 신호 유형 */
  type: 'trace' | 'metric' | 'log';

  /**
   * 엔드포인트 URL
   * - trace / metric: OTLP HTTP URL (예: http://collector:4318/v1/traces)
   * - log: TCP URL (예: tcp://logstash:5044) 또는 HTTP URL
   */
  url: string;

  /** log 타입일 때만 사용. 'tcp'로 지정하면 TCP 소켓 전송을 활성화합니다. */
  transport?: 'http' | 'tcp';
}

API Reference

OtelModule

NestJS 글로벌 Dynamic Module입니다. 한 번만 등록하면 모든 모듈에서 OTEL_LOGGER를 주입받을 수 있습니다.

OtelModule.forRoot(config: OtelConfig): DynamicModule
Provider 토큰 타입 설명
'OTEL_SDK' NodeSDK OpenTelemetry Node SDK 인스턴스
OTEL_LOGGER winston.Logger 구조화 로거 인스턴스

Lifecycle

  • onApplicationBootstrap: SDK .start() 호출
  • onApplicationShutdown: SDK .shutdown() 호출 (플러시 보장)

@Trace

메서드를 OpenTelemetry **스팬(Span)**으로 자동으로 감쌉니다.

@Trace(options?: TraceOptions): MethodDecorator
@Injectable()
export class PaymentService {
  @Trace()
  async processPayment(orderId: string) {
    // 클래스명.메서드명 → 'PaymentService.processPayment'
  }

  @Trace({
    name: 'payment.process',
    attributes: { 'payment.gateway': 'stripe' },
    skipIf: (args) => args[0]?.isSystemEvent === true,
  })
  async processWithOptions(orderId: string) { ... }
}

TraceOptions

옵션 타입 설명
name string 스팬 이름. 미지정 시 클래스명.메서드명 자동 사용
skipIf (...args) => boolean true를 반환하면 스팬을 생성하지 않음
attributes Record<string, string> 스팬에 추가할 정적 어트리뷰트

예외 발생 시 스팬 상태를 ERROR로 설정하고 recordException()을 자동 호출합니다. 예외는 그대로 다시 throw됩니다.


@TraceEventHandler

이벤트 핸들러(소비자) 메서드에 사용합니다. 이벤트 객체의 headers에서 W3C Trace Context를 추출해 상위 스팬과 분산 추적 체인을 연결합니다.

@Injectable()
export class OrderEventHandler {
  @TraceEventHandler({ name: 'Handle.OrderCreated' })
  async handle(event: OrderCreatedEvent) {
    // event.headers에서 traceparent를 자동으로 추출하여
    // 발행 측의 스팬과 연결된 새 스팬을 시작합니다.
  }
}

이벤트 객체의 첫 번째 인자(args[0])에 headers 프로퍼티가 있어야 합니다. 발행 측에서 TraceContextManager.inject()로 헤더를 채워야 체인이 연결됩니다.


TraceContextManager

이벤트 기반 아키텍처에서 트레이스 컨텍스트를 수동으로 전파할 때 사용하는 정적 유틸리티 클래스입니다.

// 발행 측 — 현재 컨텍스트를 헤더에 주입
const headers = TraceContextManager.inject({});
await eventBus.publish({ payload: event, headers });

// 소비 측 — 헤더에서 컨텍스트 복원
const parentContext = TraceContextManager.extract(event.headers);

// 복원된 컨텍스트 안에서 실행
TraceContextManager.runWith(parentContext, () => { ... });

createLogger

NestJS 없이 순수하게 Winston 로거만 생성할 때 사용합니다.

const logger = createLogger('auth-service', {
  url: 'tcp://logstash:5044',
  transport: 'tcp',
});

logger.info('서버 시작', { port: 3000 });
  • 콘솔: 타임스탬프 [서비스명] 레벨: 메시지 {메타} (컬러 출력)
  • TCP: JSON 구조화 포맷 (timestamp, level, message, service, ...meta)

Distributed Tracing

마이크로서비스 환경에서 이벤트 발행·소비 간 트레이스 체인을 연결하는 패턴입니다.

[Service A]                         [Service B]
  @Trace                              @TraceEventHandler
  handleCommand()                     handleOrderCreated()
       │                                     │
       │  TraceContextManager.inject()       │
       │──────────── headers ──────────────►│
       │           (traceparent)             │  TraceContextManager.extract()
       │                                     │  → parentContext 복원
       └─────────── Span A ─────────────────┴──── Child Span B (연결됨)
// 발행 측
@Injectable()
export class OrderCommandHandler {
  @Trace({ name: 'OrderCommand.handle' })
  async execute(command: CreateOrderCommand) {
    const order = await Order.create(command);
    const headers = TraceContextManager.inject({});
    await this.eventBus.publish({ payload: new OrderCreatedEvent(order.id), headers });
  }
}

// 소비 측
@Injectable()
export class OrderProjection {
  @TraceEventHandler({ name: 'Handle.OrderCreated' })
  async handle(event: OrderCreatedEvent) {
    await this.repository.save(event.payload);
  }
}

Architecture

@meshestra/otel
├── OtelModule                  # NestJS 글로벌 모듈 (SDK 라이프사이클 관리)
│   └── createOtelSDK()         # NodeSDK 팩토리 (trace / metric / log exporter 구성)
│
├── Decorators
│   ├── @Trace                  # 메서드 → Span 래핑 (일반 서비스 계층)
│   └── @TraceEventHandler      # 이벤트 헤더에서 컨텍스트 추출 후 Span 생성
│
├── TraceContextManager         # 이벤트 헤더 inject / extract / runWith 유틸리티
│
└── createLogger()              # Winston 로거 팩토리 (Console + TCP transport)

Environment Variables

SERVICE_NAME=order-service

# OpenTelemetry Collector (OTLP HTTP)
OTEL_TRACE_URL=http://otel-collector:4318/v1/traces
OTEL_METRIC_URL=http://otel-collector:4318/v1/metrics

# Logstash / Loki (TCP)
LOG_URL=tcp://logstash:5044

특정 신호가 필요 없다면 해당 ExporterConfigexporters 배열에서 제거하면 됩니다.


Contributing

  1. Fork this repository
  2. Create a branch: git checkout -b feat/my-feature
  3. Commit your changes: git commit -m "feat: add some feature"
  4. Push the branch: git push origin feat/my-feature
  5. Open a Pull Request

License

MIT © meshestra

About

Integration of open-telemetry module for Nest.js

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors