@meshestra/otel은 NestJS 애플리케이션에 OpenTelemetry 기반의 관측 가능성(Observability)을 빠르게 통합하기 위한 패키지입니다.
- Traces: OTLP HTTP로 분산 추적 데이터를 내보냅니다.
- Metrics: OTLP HTTP로 주기적으로 메트릭을 내보냅니다.
- Logs: Winston 로거를 통해 콘솔 및 TCP(Logstash 등)로 구조화된 로그를 전송합니다.
트레이스·메트릭·로그 세 신호를 단일 OtelModule.forRoot() 호출 하나로 초기화할 수 있으며, @Trace·@TraceEventHandler 데코레이터로 메서드 단위 계측을 선언적으로 추가할 수 있습니다.
# pnpm
pnpm add @meshestra/otel
# npm
npm install @meshestra/otel
# yarn
yarn add @meshestra/otel{
"@nestjs/common": "^11.x",
"@nestjs/core": "^11.x",
"reflect-metadata": "^0.2.x",
"rxjs": "^7.x",
"winston": "^3.x"
}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();// 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 {}OtelModule은 OTEL_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('주문 생성 시작');
}
}interface OtelConfig {
/** 서비스 식별자. OTLP resource attribute로 사용됩니다. */
serviceName: string;
/** 내보낼 신호(trace / metric / log)별 exporter 설정 목록 */
exporters: 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';
}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()호출 (플러시 보장)
메서드를 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됩니다.
이벤트 핸들러(소비자) 메서드에 사용합니다. 이벤트 객체의 headers에서 W3C Trace Context를 추출해 상위 스팬과 분산 추적 체인을 연결합니다.
@Injectable()
export class OrderEventHandler {
@TraceEventHandler({ name: 'Handle.OrderCreated' })
async handle(event: OrderCreatedEvent) {
// event.headers에서 traceparent를 자동으로 추출하여
// 발행 측의 스팬과 연결된 새 스팬을 시작합니다.
}
}이벤트 객체의 첫 번째 인자(
args[0])에headers프로퍼티가 있어야 합니다. 발행 측에서TraceContextManager.inject()로 헤더를 채워야 체인이 연결됩니다.
이벤트 기반 아키텍처에서 트레이스 컨텍스트를 수동으로 전파할 때 사용하는 정적 유틸리티 클래스입니다.
// 발행 측 — 현재 컨텍스트를 헤더에 주입
const headers = TraceContextManager.inject({});
await eventBus.publish({ payload: event, headers });
// 소비 측 — 헤더에서 컨텍스트 복원
const parentContext = TraceContextManager.extract(event.headers);
// 복원된 컨텍스트 안에서 실행
TraceContextManager.runWith(parentContext, () => { ... });NestJS 없이 순수하게 Winston 로거만 생성할 때 사용합니다.
const logger = createLogger('auth-service', {
url: 'tcp://logstash:5044',
transport: 'tcp',
});
logger.info('서버 시작', { port: 3000 });- 콘솔:
타임스탬프 [서비스명] 레벨: 메시지 {메타}(컬러 출력) - TCP: JSON 구조화 포맷 (
timestamp,level,message,service, ...meta)
마이크로서비스 환경에서 이벤트 발행·소비 간 트레이스 체인을 연결하는 패턴입니다.
[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);
}
}@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)
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특정 신호가 필요 없다면 해당
ExporterConfig를exporters배열에서 제거하면 됩니다.
- Fork this repository
- Create a branch:
git checkout -b feat/my-feature - Commit your changes:
git commit -m "feat: add some feature" - Push the branch:
git push origin feat/my-feature - Open a Pull Request
MIT © meshestra