OpenTelemetry Trace 살펴보기

이동우·2025년 4월 28일

NestJS

목록 보기
1/1
post-thumbnail

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와 그 기반이 되는 개념들을 알아보고자 합니다.

1. 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와 그 기반이 되는 개념들을 알아보고자 합니다.

2. TracerProvider

TracerProvider는 OpenTelemetry에서 Tracer를 생성하고 관리하는 최상위 객체입니다. TracerProvider는 Tracer를 생성하고, 내부적으로 다음과 같은 것들을 관리합니다.

  • 등록된 SpanProcessor 목록
  • 사용 중인 Exporter 정보
  • Resource(서비스 이름, 인스턴스 ID 등 메타정보)
  • 만들어진 Tracer 인스턴스 목록

TracerProvider는 상태를 가지는(stateful)객체이기 때문에, 일반적으로 애플리케이션 전체(global)에서 하나만 존재해야 합니다.

3. Tracer

Tracer는 Span을 생성하는 도구입니다. 그리고 어떤 모듈/라이브러리/기능 범위를 식별하는 역할도 합니다.(이를 Instrumentation Scope라고 부릅니다.) Tracer는 TracerProvider를 통해 생성하거나 받아와야 합니다.

import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('example-instrumentation');

4. Span

Span은 OpenTelemetry에서 가장 기본적인 단위로, 하나의 작업 단위(unit of work)를 나타냅니다. 작업의 시작 시점과 종료시점을 기록하고, 다양한 메타데이터를 포함할 수 있습니다. 하나의 요청(Trace)은 여러 개의 Span으로 구성될 수 있습니다. 그리고 이 Span들은 서로 부모-자식 관계를 맺으며 요청 흐름을 트리 형태로 표현합니다.

const span = tracer.startSpan('custom-operation');
// 작업 수행
span.end();

Span의 주요 필드

필드설명
NameSpan 이름 (예: /api/users)
Start/End Timestamp시작 및 종료 시각
Span ContextTrace ID, Span ID 등 고유 식별자
Parent Span ID부모 Span 식별자 (없으면 Root)
Attributes추가 메타데이터 (ex: http.method=GET)
Events중요한 순간 기록
Status성공/오류 상태
Kind역할 구분 (Client, Server, Internal 등)

참고:

  • Attributes는 표준화된 Semantic Attribute를 사용하는 것이 권장됩니다.
  • Events는 작업 도중 중요한 이벤트(예: DB 쿼리 시작 등)를 기록할 때 사용합니다.
  • Links는 Trace를 넘어 여러 흐름을 연결할 때 사용합니다.

5. Context Propagation

Context Propagation은 여러 Span들을 하나의 요청 흐름(Trace) 안에서 끊김 없이 연결하는 핵심 기술입니다. 요청이 다른 서비스로 넘어가거나 비동기 작업이 발생하더라도, 같은 trace_id를 유지하고 부모-자식 관계를 잇기 위해 Context가 사용됩니다. Context는 현재 활성화된 Span과 추가 정보를 담고 있으며, 코드 실행 흐름에 따라 함께 전파됩니다.

  • Node.js: AsyncLocalStorage를 활용해 비동기 흐름을 따라 Context를 유지합니다.
  • Java: ThreadLocal을 사용해 스레드별 Context를 유지합니다.
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는 비동기 흐름이 끝나면 정리됩니다.

6. Trace Exporters

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와 연결되어,
  • Span이 종료되면 Exporter를 통해 외부로 보내집니다.

Exporter는 "어디로 보낼지"만 책임집니다. (어떤 시점에 보낼지는 SpanProcessor가 관리합니다.)

7. SpanProcessor

SpanProcessor는 Span이 종료될 때, Exporter로 데이터를 넘기는 타이밍과 방식을 관리합니다. 종류는 다음과 같습니다.

  • SimpleSpanProcessor: Span이 끝날 때마다 즉시 Exporter로 전송합니다.
  • BatchSpanProcessor: Span들을 메모리에 모아 일정량 또는 일정 주기마다 묶어서 Export 합니다.

주로 BatchSpanProcessor을 효율성을 위해 사용합니다. 작동 방식은

  1. Span을 메모리에 쌓아둠
  2. 일정 개수 이상 쌓이거나, 타이머가 만료되면 한 번에 Export
  3. 앱 종료시 쌓인 Span들도 전부 Export(flush)
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()   |
  |     |              |
  |     |              |
  |     |              |
  |     |              |
  +-----+--------------+

8. Instrumentation

Instrumentation은 위에서 설명한 모든 OpenTelemetry 흐름을 실제 애플리케이션 코드에 적용하는 방법입니다.

Instrumentation 방법은 크게 두 가지입니다.

(1) Code-based Instrumentation (직접 작성)

직접 tracer.startSpan()을 호출해 원하는 로직만 추적합니다.

const tracer = trace.getTracer('custom');
const span = tracer.startSpan('business-operation');
// 작업 수행
span.end();
  • 직접 제어가 가능합니다.
  • 하지만 매 작업마다 수작업 추가 필요합니다.

(2) Auto Instrumentation (자동 삽입)

Express, Prisma, HTTP 등 라이브러리 레벨에서

자동으로 Span을 생성하는 Instrumentation 라이브러리를 적용합니다.

import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';

new HttpInstrumentation();
new ExpressInstrumentation();
  • 코드 수정 없이 기본적인 관찰이 가능합니다.
  • 복잡한 비즈니스 로직은 수동 Code-based Instrumentation과 병행하는 경우가 많습니다.

이제 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;

Trace Exporter

TraceExporter는 수집된 Span(추적 데이터)을 OpenTelemetry Collector, Jaeger, Prometheus 등 외부 모니터링 시스템으로 전송하는 컴포넌트입니다. NestJS에서는 주로 OTLP( OpenTelemetry Protocol) 기반 Exporter를 사용해 Collector나 Jaeger로 데이터를 보냅니다.

Instrumentations

NestJS에서는 다양한 Instrumentation을 함께 사용하여, HTTP 요청부터 DB 쿼리까지 애플리케이션 전반을 자동으로 추적할 수 있습니다.

Instrumentation추적하는 영역설명
PrismaInstrumentationPrisma ORM 쿼리 흐름Prisma Client를 통한 DB 접근(쿼리, 트랜잭션 등)을 자동 추적
HttpInstrumentationHTTP 요청/응답 흐름Node.js 기본 http, https 모듈을 통한 모든 요청/응답을 자동 추적
ExpressInstrumentationExpress 라우터/미들웨어 흐름Express 서버에서 각 요청 처리 단계를 추적
NestInstrumentationNestJS Controller/Service 흐름NestJS 프레임워크 레벨의 컨트롤러 메소드 실행을 추적
AmqplibInstrumentationAMQP 메시지 송수신 흐름RabbitMQ 등 AMQP 기반 메시지 송수신을 추적
IORedisInstrumentationRedis 명령 실행 흐름ioredis 클라이언트를 통한 Redis 명령 실행을 추적

Instrumentation은 내부적으로 Tracer를 사용하여 Span을 생성하고, 비동기 흐름을 따라 Context를 유지하며 상위 Span과 관계를 이어줍니다.

Resource

Resource는 "이 데이터가 어디서 왔는지"를 설명하는 메타데이터입니다. (예: 서비스 이름, 버전, 인스턴스 ID 등)

위 설정을 통해 생성되는 Span마다 서비스 정보가 자동으로 포함됩니다.


실제 동작 예시

Jaeger를 통해 수집된 Trace를 시각화하면, 요청 흐름을 트리 형태로 확인할 수 있습니다.


특정 요청 흐름을 살펴보면 다음과 같이 Trace가 구성됩니다:

  1. HttpInstrumentation: 외부에서 들어온 HTTP 요청을 감지하고 Span 생성
  2. ExpressInstrumentation: Express 미들웨어 흐름을 추적
  3. NestInstrumentation: NestJS Controller와 Service 로직 추적
  4. PrismaInstrumentation: 내부에서 실행되는 DB 쿼리(Prisma ORM)를 작업 단위로 Span 생성

이러한 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

0개의 댓글