OpenTelemetry에서 Traces에 대한 개념적인 요소를 다루는 글입니다. 실제 구현하는 내용은 링크를 참고해주시길 바랍니다.
시스템이 사용자 요청을 처리하고 응답을 보내는 동안, 내부에서는 수많은 일이 일어납니다. 이 과정에서 시스템의 상태를 외부에서 들여다볼 수 있도록 만드는 능력을 관찰성(Observability)이라고 합니다. 관찰성이 높다는 것은, 시스템이 남긴 Trace, Metrics, Log 같은 데이터를 통해 그 내부 동작과 문제를 빠르게 이해할 수 있다는 의미입니다.
OpenTelemetry는 이러한 관찰성을 확보하기 위한 표준 프레임워크입니다. 시스템이 생성하는 Trace, Metrics, Log 같은 데이터를 직접 생성하고 수집하고 전송하는 역할을 합니다. OpenTelemetry 자체는 데이터를 저장하거나 시각화하지 않으며, 다양한 트레이싱, 메트릭 수집 시스템(Jaeger, Prometheus)과 연동하여 사용합니다. 덕분에 OpenTelemetry를 이용하면 프로그래밍 언어나 인프라 환경에 상관없이 일관된 방식으로 시스템의 내부 상태를 추적하고 관찰할 수 있습니다.
OpenTelemetry의 목적은 Signals를 수집, 처리 및 내보내는 것입니다. Signals는 플랫폼에서 실행 중인 운영 체제 및 애플리케이션의 기본 활동을 설명하는 시스템 출력입니다. OpenTelemetry에서 현재 지원하는 Signals는 Traces, Metrics, Logs, Baggage가 있습니다. 이 글에선 OpenTelemetry의 Trace와 그 기반이 되는 개념들을 알아보고자 합니다.
Trace는 애플리케이션 또는 분산 시스템에서 요청이 이동하는 전체 경로를 나타냅니다. 이 경로는 HTTP 요청-응답 흐름일 수도 있고, 이벤트 기반 아키텍처에서의 메시지 전파일 수도 있습니다. Trace는 여러 개의 Span으로 구성되며, Span은 시스템 내부의 각 작업 단위를 기록합니다. 각 Span은 시작 시각, 종료 시각, 수행 작업, 이벤트, 속성 등을 포함하며, 이 Span들이 부모-자식 관계로 연결되어 하나의 Trace를 구성합니다.시스템이 사용자 요청을 처리하고 응답을 보내는 동안, 내부에서는 수많은 일이 일어납니다. 이 과정에서 시스템의 상태를 외부에서 들여다볼 수 있도록 만드는 능력을 관찰성(Observability)이라고 합니다. 관찰성이 높다는 것은, 시스템이 남긴 Trace, Metrics, Log 같은 데이터를 통해 그 내부 동작과 문제를 빠르게 이해할 수 있다는 의미입니다.
OpenTelemetry는 이러한 관찰성을 확보하기 위한 표준 프레임워크입니다. 시스템이 생성하는 Trace, Metrics, Log 같은 데이터를 직접 생성하고 수집하고 전송하는 역할을 합니다. OpenTelemetry 자체는 데이터를 저장하거나 시각화하지 않으며, 다양한 트레이싱, 메트릭 수집 시스템(Jaeger, Prometheus)과 연동하여 사용합니다. 덕분에 OpenTelemetry를 이용하면 프로그래밍 언어나 인프라 환경에 상관없이 일관된 방식으로 시스템의 내부 상태를 추적하고 관찰할 수 있습니다.
OpenTelemetry의 목적은 Signals를 수집, 처리 및 내보내는 것입니다. Signals는 플랫폼에서 실행 중인 운영 체제 및 애플리케이션의 기본 활동을 설명하는 시스템 출력입니다. OpenTelemetry에서 현재 지원하는 Signals는 Traces, Metrics, Logs, Baggage가 있습니다. 이 글에선 OpenTelemetry의 Trace와 그 기반이 되는 개념들을 알아보고자 합니다.
TracerProvider는 OpenTelemetry에서 Tracer를 생성하고 관리하는 최상위 객체입니다. TracerProvider는 Tracer를 생성하고, 내부적으로 다음과 같은 것들을 관리합니다.
TracerProvider는 상태를 가지는(stateful)객체이기 때문에, 일반적으로 애플리케이션 전체(global)에서 하나만 존재해야 합니다.
Tracer는 Span을 생성하는 도구입니다. 그리고 어떤 모듈/라이브러리/기능 범위를 식별하는 역할도 합니다.(이를 Instrumentation Scope라고 부릅니다.) Tracer는 TracerProvider를 통해 생성하거나 받아와야 합니다.
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('example-instrumentation');
Span은 OpenTelemetry에서 가장 기본적인 단위로, 하나의 작업 단위(unit of work)를 나타냅니다. 작업의 시작 시점과 종료시점을 기록하고, 다양한 메타데이터를 포함할 수 있습니다. 하나의 요청(Trace)은 여러 개의 Span으로 구성될 수 있습니다. 그리고 이 Span들은 서로 부모-자식 관계를 맺으며 요청 흐름을 트리 형태로 표현합니다.
const span = tracer.startSpan('custom-operation');
// 작업 수행
span.end();
| 필드 | 설명 |
|---|
| Name | Span 이름 (예: /api/users) |
|---|
| Start/End Timestamp | 시작 및 종료 시각 |
|---|
| Span Context | Trace ID, Span ID 등 고유 식별자 |
|---|
| Parent Span ID | 부모 Span 식별자 (없으면 Root) |
|---|
| Attributes | 추가 메타데이터 (ex: http.method=GET) |
|---|
| Events | 중요한 순간 기록 |
|---|
| Status | 성공/오류 상태 |
|---|
| Kind | 역할 구분 (Client, Server, Internal 등) |
|---|
참고:
Context Propagation은 여러 Span들을 하나의 요청 흐름(Trace) 안에서 끊김 없이 연결하는 핵심 기술입니다. 요청이 다른 서비스로 넘어가거나 비동기 작업이 발생하더라도, 같은 trace_id를 유지하고 부모-자식 관계를 잇기 위해 Context가 사용됩니다. Context는 현재 활성화된 Span과 추가 정보를 담고 있으며, 코드 실행 흐름에 따라 함께 전파됩니다.
const span = tracer.startSpan('outer-operation');
context.with(trace.setSpan(context.active(), span), () => {
// 이 안에서는 outer-operation이 활성화된 Context
});
Node.js와 Prisma로 예를 들면 await 프리즈마를 호출하고 비동기로 Prisma 작업이 시작할 때 Span을 생성합니다. 생성된 Span을 현재 Context에 등록합니다. 작업 완료 후 span.end()로 종료하면 SpanPorcessor가 호출되어 처리합니다. Context는 비동기 흐름이 끝나면 정리됩니다.
Exporter는 수집된 Span 데이터를 외부 시스템으로 전송하는 역할을 합니다. Exporter는 Trace 데이터를 OpenTelemetry Collector, Jaeger, Zipkin, Prometheus 등 다양한 백엔드로 전송할 수 있습니다.
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
const exporter = new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces'
});
Exporter는 "어디로 보낼지"만 책임집니다. (어떤 시점에 보낼지는 SpanProcessor가 관리합니다.)
SpanProcessor는 Span이 종료될 때, Exporter로 데이터를 넘기는 타이밍과 방식을 관리합니다. 종류는 다음과 같습니다.
주로 BatchSpanProcessor을 효율성을 위해 사용합니다. 작동 방식은
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
앞서 살핀 내용의 간단한 흐름입니다.
+-----+--------------+ +-------------------------+ +----------------+
| | | | | | |
| | | | Batching Span Processor | | SpanExporter |
| | +---> Simple Span Processor +---> (OTLPExporter) |
| | | | | | |
| SDK | Span.start() | +-------------------------+ +----------------+
| | Span.end() |
| | |
| | |
| | |
| | |
+-----+--------------+
Instrumentation은 위에서 설명한 모든 OpenTelemetry 흐름을 실제 애플리케이션 코드에 적용하는 방법입니다.
Instrumentation 방법은 크게 두 가지입니다.
직접 tracer.startSpan()을 호출해 원하는 로직만 추적합니다.
const tracer = trace.getTracer('custom');
const span = tracer.startSpan('business-operation');
// 작업 수행
span.end();
Express, Prisma, HTTP 등 라이브러리 레벨에서
자동으로 Span을 생성하는 Instrumentation 라이브러리를 적용합니다.
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
new HttpInstrumentation();
new ExpressInstrumentation();
이제 NestJS에서 OpenTelemetry의 적용 흐름을 간단하게 살펴보고자 합니다. NestJS에서는 OpenTelemetry의 NodeSDK를 사용하여 관찰성을 구성할 수 있습니다. 이 과정에서 TracerProvider, SpanProcessor, Exporter, Instrumentations, Resource 등을 초기화하고 설정하게 됩니다.
다음은 NodeSDK를 통해 OpenTelemetry를 설정하는 코드 예시입니다.
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: process.env.TRACING_AGENT_URL || 'http://otel-collector:4318/v1/traces',
}),
instrumentations: [
new PrismaInstrumentation(),
new HttpInstrumentation(),
new ExpressInstrumentation(),
new NestInstrumentation(),
new AmqplibInstrumentation(),
new IORedisInstrumentation(),
],
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: process.env.TRACING_SERVICE_NAME || 'nestjs-app',
}),
});
// Enable the API to record telemetry
logger.log('Starting tracing');
sdk.start();
// Gracefully shut down the SDK on process exit
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => logger.log('Tracing terminated'))
.catch((error) => logger.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});
export default sdk;
TraceExporter는 수집된 Span(추적 데이터)을 OpenTelemetry Collector, Jaeger, Prometheus 등 외부 모니터링 시스템으로 전송하는 컴포넌트입니다. NestJS에서는 주로 OTLP( OpenTelemetry Protocol) 기반 Exporter를 사용해 Collector나 Jaeger로 데이터를 보냅니다.
NestJS에서는 다양한 Instrumentation을 함께 사용하여, HTTP 요청부터 DB 쿼리까지 애플리케이션 전반을 자동으로 추적할 수 있습니다.
| Instrumentation | 추적하는 영역 | 설명 |
|---|---|---|
| PrismaInstrumentation | Prisma ORM 쿼리 흐름 | Prisma Client를 통한 DB 접근(쿼리, 트랜잭션 등)을 자동 추적 |
| HttpInstrumentation | HTTP 요청/응답 흐름 | Node.js 기본 http, https 모듈을 통한 모든 요청/응답을 자동 추적 |
| ExpressInstrumentation | Express 라우터/미들웨어 흐름 | Express 서버에서 각 요청 처리 단계를 추적 |
| NestInstrumentation | NestJS Controller/Service 흐름 | NestJS 프레임워크 레벨의 컨트롤러 메소드 실행을 추적 |
| AmqplibInstrumentation | AMQP 메시지 송수신 흐름 | RabbitMQ 등 AMQP 기반 메시지 송수신을 추적 |
| IORedisInstrumentation | Redis 명령 실행 흐름 | ioredis 클라이언트를 통한 Redis 명령 실행을 추적 |
Instrumentation은 내부적으로 Tracer를 사용하여 Span을 생성하고, 비동기 흐름을 따라 Context를 유지하며 상위 Span과 관계를 이어줍니다.
Resource는 "이 데이터가 어디서 왔는지"를 설명하는 메타데이터입니다. (예: 서비스 이름, 버전, 인스턴스 ID 등)
위 설정을 통해 생성되는 Span마다 서비스 정보가 자동으로 포함됩니다.
Jaeger를 통해 수집된 Trace를 시각화하면, 요청 흐름을 트리 형태로 확인할 수 있습니다.

특정 요청 흐름을 살펴보면 다음과 같이 Trace가 구성됩니다:
이러한 Span들이 연결되어 요청 전체를 구성하는 Trace를 완성합니다.

세부적으로 보면
span.start() 호출 시 작업 시작 시각을 기록하고,
span.end() 호출 시 종료 시각을 기록합니다.
Span에는 HTTP 메서드, 요청 경로, DB 쿼리문, 응답 상태 코드 등 다양한 Attribute(속성)들이 자동으로 기록됩니다.
모든 Span은 Resource 정보(서비스 이름 등)를 포함하여 Exporter로 전송됩니다.
이처럼 OpenTelemetry를 활용하면 NestJS 애플리케이션의 전반적인 요청 흐름과 시스템 동작을 손쉽게 관찰할 수 있습니다. 이를 통해 성능 병목을 분석하고, 시스템 안정성을 높이는 데 필요한 인사이트를 얻을 수 있습니다. 이상으로 글을 마무리하겠습니다. 감사합니다.
[참고]
https://opentelemetry.io/docs/concepts/signals/traces/#span-context
https://opentelemetry.io/docs/specs/otel/trace/api/#parent-child-relationship
https://opentelemetry.io/docs/languages/js/context/?utm_source=chatgpt.com
https://opentelemetry.io/docs/concepts/context-propagation/
https://opentelemetry.io/docs/specs/otel/trace/sdk/
https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-context-async-hooks/src/AsyncLocalStorageContextManager.ts
https://nodejs.org/api/async_context.html#class-asynclocalstorage
https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/opentelemetry-tracing?utm_source=chatgpt.com
https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-context-async-hooks/src/AsyncLocalStorageContextManager.ts
https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/Tracer.ts#L67
https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-node/src/NodeTracerProvider.ts