DevOps

OpenTelemetry 실전 가이드 - Spring Boot 분산 추적부터 메트릭까지

백엔드 개발자 김승원 2026. 4. 15. 12:52

들어가며

"주문 API가 간헐적으로 5초 이상 걸린다는데, 어디서 병목인지 모르겠어요." 마이크로서비스 환경에서 이런 문제를 겪으면, 로그만으로는 원인을 찾기가 매우 어렵습니다. 주문 서비스 → 결제 서비스 → 재고 서비스 → 알림 서비스까지, 하나의 요청이 여러 서비스를 거치면서 어느 구간에서 지연이 발생하는지 파악하려면 분산 추적(Distributed Tracing)이 필수입니다.

3~7년차 백엔드 개발자라면 Prometheus와 Grafana는 사용해봤겠지만, 분산 추적까지 도입한 경험은 많지 않을 것입니다. OpenTelemetry는 Traces, Metrics, Logs를 하나의 표준으로 통합한 관측 가능성(Observability)의 사실상 표준입니다. CNCF Graduated 프로젝트로, 벤더 종속 없이 다양한 백엔드(Jaeger, Tempo, Datadog 등)로 데이터를 전송할 수 있습니다.

이 글에서는 OpenTelemetry의 핵심 개념부터 Spring Boot 자동 계측, OTel Collector 설정, Jaeger/Tempo로 분산 추적 시각화, 커스텀 Span 추가, docker-compose로 전체 스택 구축까지 실무에서 바로 적용할 수 있는 내용을 다룹니다.

1. OpenTelemetry 아키텍처

OpenTelemetry(OTel)는 세 가지 핵심 신호(Signal)를 수집합니다.

세 가지 신호

신호 역할 예시 백엔드
Traces 요청 흐름 추적 API 호출 → DB 쿼리 → 외부 API 호출의 전체 경로 Jaeger, Tempo, Zipkin
Metrics 수치 측정 요청 수, 응답 시간, CPU 사용률, 메모리 Prometheus, InfluxDB
Logs 이벤트 기록 에러 로그, 비즈니스 이벤트 Loki, Elasticsearch

전체 아키텍처

┌────────────────────────────────────────────────────────────┐
│                    Application Layer                        │
│                                                            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │ Order    │  │ Payment  │  │ Inventory│  │ Notify   │  │
│  │ Service  │→ │ Service  │→ │ Service  │→ │ Service  │  │
│  │(OTel SDK)│  │(OTel SDK)│  │(OTel SDK)│  │(OTel SDK)│  │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘  │
│       │             │             │             │          │
│       └──────┬──────┴──────┬──────┘             │          │
│              │             │                    │          │
│              ▼             ▼                    ▼          │
│  ┌─────────────────────────────────────────────────────┐  │
│  │              OTel Collector                          │  │
│  │   Receivers → Processors → Exporters                │  │
│  └──────┬──────────┬──────────────┬────────────────────┘  │
│         │          │              │                        │
└─────────┼──────────┼──────────────┼────────────────────────┘
          │          │              │
          ▼          ▼              ▼
     ┌────────┐ ┌──────────┐ ┌──────────┐
     │ Jaeger │ │Prometheus│ │   Loki   │
     │(Traces)│ │(Metrics) │ │ (Logs)   │
     └────┬───┘ └────┬─────┘ └────┬─────┘
          │          │            │
          └────┬─────┘            │
               ▼                  ▼
          ┌─────────────────────────┐
          │       Grafana           │
          │  (통합 시각화 대시보드)    │
          └─────────────────────────┘

Trace의 핵심 개념

# Trace: 하나의 요청이 시스템을 관통하는 전체 경로
# Span: Trace 내의 각 작업 단위
# Context: Trace를 서비스 간에 전파하는 메타데이터

# 예시: POST /orders API 호출의 Trace

Trace ID: abc123def456
├── Span: HTTP POST /orders (Order Service)        [0ms - 850ms]
│   ├── Span: OrderService.createOrder()           [5ms - 840ms]
│   │   ├── Span: SELECT * FROM products           [10ms - 25ms]
│   │   ├── Span: HTTP POST /payments (Payment)    [30ms - 650ms]
│   │   │   ├── Span: PaymentService.process()      [35ms - 640ms]
│   │   │   │   ├── Span: External API: PG Gateway  [40ms - 600ms] ← 병목!
│   │   │   │   └── Span: INSERT INTO payments       [605ms - 630ms]
│   │   ├── Span: HTTP POST /inventory (Inventory) [655ms - 720ms]
│   │   │   └── Span: UPDATE inventory SET ...       [660ms - 710ms]
│   │   └── Span: INSERT INTO orders                [725ms - 740ms]
│   └── Span: HTTP POST /notify (Notify)           [745ms - 840ms]

# 이 Trace를 보면 PG Gateway 외부 API 호출이 560ms로 병목임을 확인

2. SDK vs Agent 방식

Spring Boot 애플리케이션에 OpenTelemetry를 적용하는 두 가지 방법이 있습니다.

방법 1: Java Agent (자동 계측, 추천)

# OpenTelemetry Java Agent 다운로드
curl -L -o opentelemetry-javaagent.jar \
  https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

# JVM 옵션으로 Agent 실행
java -javaagent:opentelemetry-javaagent.jar \
  -Dotel.service.name=order-service \
  -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
  -Dotel.metrics.exporter=otlp \
  -Dotel.logs.exporter=otlp \
  -jar order-service.jar

# Agent가 자동으로 계측하는 항목:
# - Spring MVC/WebFlux 요청
# - JDBC 쿼리
# - HTTP 클라이언트 (RestTemplate, WebClient, HttpClient)
# - Kafka Producer/Consumer
# - Redis 명령
# - gRPC 호출
# - 200+ 라이브러리 자동 지원

방법 2: SDK (수동 계측)

// build.gradle
dependencies {
    implementation platform('io.opentelemetry:opentelemetry-bom:1.42.0')
    implementation 'io.opentelemetry:opentelemetry-api'
    implementation 'io.opentelemetry:opentelemetry-sdk'
    implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
    implementation 'io.opentelemetry:opentelemetry-semconv'

    // Spring Boot Starter
    implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:2.8.0'
}

비교

항목 Java Agent SDK
설정 방식 JVM 옵션, 코드 변경 불필요 의존성 추가, 코드 변경
자동 계측 200+ 라이브러리 자동 지원 수동으로 추가
커스터마이징 제한적 (환경변수/설정) 세밀한 제어 가능
성능 영향 약간 높음 최소화 가능
권장 상황 대부분의 프로젝트 세밀한 제어가 필요할 때

대부분의 경우 Java Agent + 필요 시 SDK로 커스텀 Span 추가 조합이 가장 실용적입니다.

3. Spring Boot 자동 계측

Spring Boot Starter 설정

// build.gradle
dependencies {
    implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:2.8.0'
}

# application.yml
otel:
  service:
    name: order-service
  exporter:
    otlp:
      endpoint: http://otel-collector:4317
      protocol: grpc
  resource:
    attributes:
      deployment.environment: production
      service.namespace: ecommerce
      service.version: 1.2.0

@WithSpan으로 커스텀 Span 추가

import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;

@Service
public class OrderService {

    // @WithSpan: 메서드 실행을 Span으로 자동 추적
    @WithSpan("OrderService.createOrder")
    public OrderResponse createOrder(
            @SpanAttribute("order.customerId") Long customerId,
            @SpanAttribute("order.itemCount") int itemCount) {

        // 현재 Span에 추가 정보 기록
        Span currentSpan = Span.current();
        currentSpan.setAttribute("order.totalAmount", calculateTotal(itemCount));

        try {
            // 비즈니스 로직
            Order order = processOrder(customerId, itemCount);

            currentSpan.setAttribute("order.id", order.getId());
            currentSpan.setAttribute("order.status", order.getStatus().name());

            return toResponse(order);

        } catch (InsufficientStockException e) {
            // 에러 정보를 Span에 기록
            currentSpan.setStatus(StatusCode.ERROR, "재고 부족");
            currentSpan.recordException(e);
            throw e;
        }
    }

    @WithSpan("OrderService.validateStock")
    private void validateStock(
            @SpanAttribute("product.id") Long productId,
            @SpanAttribute("requested.quantity") int quantity) {
        // 재고 확인 로직
    }
}

Baggage로 컨텍스트 전파

import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.context.Context;

@RestController
public class OrderController {

    @PostMapping("/orders")
    public ResponseEntity<OrderResponse> createOrder(
            @RequestBody OrderRequest request,
            @RequestHeader("X-User-Id") String userId) {

        // Baggage: 서비스 간 전파되는 키-값 쌍
        // Trace Context와 함께 다른 서비스로 전달됨
        Baggage baggage = Baggage.builder()
                .put("user.id", userId)
                .put("user.tier", getUserTier(userId))
                .put("request.priority", "high")
                .build();

        try (var scope = baggage.makeCurrent()) {
            // 이 범위 내에서 호출되는 모든 서비스에 baggage가 전파됨
            OrderResponse response = orderService.createOrder(request);
            return ResponseEntity.ok(response);
        }
    }
}

// 다른 서비스에서 Baggage 읽기
@Service
public class PaymentService {

    @WithSpan
    public PaymentResult processPayment(PaymentRequest request) {
        // 전파된 Baggage 읽기
        String userId = Baggage.current().getEntryValue("user.id");
        String userTier = Baggage.current().getEntryValue("user.tier");

        Span.current().setAttribute("payment.user.tier", userTier);

        // VIP 사용자는 다른 PG사 사용
        if ("VIP".equals(userTier)) {
            return premiumPgGateway.process(request);
        }
        return standardPgGateway.process(request);
    }
}

4. OTel Collector 설정

OTel Collector는 텔레메트리 데이터의 수집, 처리, 전송을 담당하는 중앙 컴포넌트입니다.

Collector 설정 파일

# otel-collector-config.yaml

# Receivers: 데이터를 받는 입구
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317    # gRPC (권장)
      http:
        endpoint: 0.0.0.0:4318    # HTTP

  # Prometheus 메트릭 스크래핑
  prometheus:
    config:
      scrape_configs:
        - job_name: 'spring-boot-apps'
          scrape_interval: 15s
          metrics_path: '/actuator/prometheus'
          static_configs:
            - targets: ['order-service:8080', 'payment-service:8080']

# Processors: 데이터 가공/필터링
processors:
  # 배치 처리 (성능 최적화)
  batch:
    timeout: 5s
    send_batch_size: 1024
    send_batch_max_size: 2048

  # 메모리 제한 (OOM 방지)
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128

  # 리소스 속성 추가
  resource:
    attributes:
      - key: environment
        value: production
        action: upsert
      - key: team
        value: backend
        action: upsert

  # 불필요한 Span 필터링 (헬스체크 등)
  filter:
    traces:
      span:
        - 'attributes["http.target"] == "/actuator/health"'
        - 'attributes["http.target"] == "/health"'

  # 샘플링 (전체 Trace의 일부만 수집)
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: error-policy
        type: status_code
        status_code: {status_codes: [ERROR]}  # 에러는 100% 수집
      - name: slow-policy
        type: latency
        latency: {threshold_ms: 1000}          # 1초 이상은 100% 수집
      - name: default-policy
        type: probabilistic
        probabilistic: {sampling_percentage: 10}  # 나머지는 10% 샘플링

# Exporters: 데이터를 보내는 목적지
exporters:
  # Jaeger (Traces)
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true

  # Tempo (Traces, Grafana 통합)
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

  # Prometheus (Metrics)
  prometheus:
    endpoint: 0.0.0.0:8889
    metric_expiration: 5m

  # Loki (Logs)
  loki:
    endpoint: http://loki:3100/loki/api/v1/push

  # 디버깅용 콘솔 출력
  debug:
    verbosity: detailed

# Service: 파이프라인 구성
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, filter, tail_sampling, batch, resource]
      exporters: [otlp/tempo]

    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, batch, resource]
      exporters: [prometheus]

    logs:
      receivers: [otlp]
      processors: [memory_limiter, batch, resource]
      exporters: [loki]

  telemetry:
    logs:
      level: info

5. Jaeger/Tempo로 분산 추적 시각화

Grafana Tempo 설정

# tempo-config.yaml
server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317

storage:
  trace:
    backend: local
    local:
      path: /var/tempo/traces
    wal:
      path: /var/tempo/wal

metrics_generator:
  registry:
    external_labels:
      source: tempo
      cluster: production
  storage:
    path: /var/tempo/generator/wal
    remote_write:
      - url: http://prometheus:9090/api/v1/write
        send_exemplars: true

overrides:
  defaults:
    metrics_generator:
      processors: [service-graphs, span-metrics]

Grafana 데이터소스 설정

# grafana-datasources.yaml
apiVersion: 1
datasources:
  - name: Tempo
    type: tempo
    access: proxy
    url: http://tempo:3200
    jsonData:
      tracesToMetrics:
        datasourceUid: prometheus
        tags:
          - key: service.name
            value: service
      tracesToLogs:
        datasourceUid: loki
        tags: ['service.name']
        mappedTags: [{key: 'service.name', value: 'service'}]
        mapTagNamesEnabled: true
        filterByTraceID: true
      serviceMap:
        datasourceUid: prometheus
      nodeGraph:
        enabled: true

  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    jsonData:
      exemplarTraceIdDestinations:
        - name: traceID
          datasourceUid: tempo

  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100
    jsonData:
      derivedFields:
        - name: TraceID
          datasourceUid: tempo
          matcherRegex: 'traceId=(\w+)'
          url: '$${__value.raw}'

6. 커스텀 Span과 Attribute 추가

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.context.Scope;

@Service
public class OrderProcessingService {

    private final Tracer tracer;

    public OrderProcessingService(OpenTelemetry openTelemetry) {
        this.tracer = openTelemetry.getTracer("order-processing", "1.0.0");
    }

    public OrderResult processOrder(OrderRequest request) {
        // 커스텀 Span 생성
        Span span = tracer.spanBuilder("process-order")
                .setAttribute("order.customer_id", request.getCustomerId())
                .setAttribute("order.item_count", request.getItems().size())
                .startSpan();

        try (Scope scope = span.makeCurrent()) {
            // Step 1: 재고 확인
            validateInventory(request);

            // Step 2: 가격 계산
            BigDecimal totalAmount = calculatePrice(request);
            span.setAttribute("order.total_amount", totalAmount.doubleValue());

            // Step 3: 결제 처리
            PaymentResult payment = processPayment(request, totalAmount);

            // Step 4: 주문 저장
            Order order = saveOrder(request, payment);

            // 이벤트 기록 (타임스탬프 포함)
            span.addEvent("order-completed", Attributes.of(
                    AttributeKey.longKey("order.id"), order.getId(),
                    AttributeKey.stringKey("order.status"), "COMPLETED"
            ));

            return OrderResult.success(order);

        } catch (Exception e) {
            span.setStatus(StatusCode.ERROR, e.getMessage());
            span.recordException(e);
            return OrderResult.failure(e.getMessage());
        } finally {
            span.end();
        }
    }

    private void validateInventory(OrderRequest request) {
        // 자식 Span 생성 (부모 Span을 자동으로 참조)
        Span span = tracer.spanBuilder("validate-inventory")
                .startSpan();

        try (Scope scope = span.makeCurrent()) {
            for (OrderItem item : request.getItems()) {
                span.addEvent("checking-stock", Attributes.of(
                        AttributeKey.longKey("product.id"), item.getProductId(),
                        AttributeKey.longKey("requested.quantity"), (long) item.getQuantity()
                ));

                int available = inventoryClient.checkStock(item.getProductId());
                if (available < item.getQuantity()) {
                    span.setStatus(StatusCode.ERROR, "Insufficient stock for product: " + item.getProductId());
                    throw new InsufficientStockException(item.getProductId());
                }
            }
        } finally {
            span.end();
        }
    }
}

7. Context Propagation

서비스 간 Trace 연결을 위한 Context Propagation은 OpenTelemetry의 핵심입니다.

HTTP 헤더 기반 전파 (W3C TraceContext)

// 자동 전파: Java Agent 또는 Spring Boot Starter가 자동 처리
// RestTemplate, WebClient, HttpClient 등에서 자동으로 헤더 주입

// 전파되는 HTTP 헤더 예시:
// traceparent: 00-abc123def456789012345678abcdef12-1234567890abcdef-01
// tracestate: vendor1=value1,vendor2=value2

// RestTemplate에서 자동 전파 (Agent/Starter가 자동 설정)
@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(30))
                .build();
        // OTel Agent/Starter가 자동으로 TracingInterceptor 추가
    }
}

// WebClient에서도 자동 전파
@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(WebClient.Builder builder) {
        return builder
                .baseUrl("http://payment-service:8080")
                .build();
        // OTel이 자동으로 ExchangeFilterFunction 추가
    }
}

// Kafka에서의 Context Propagation
// OTel Agent가 Kafka Producer/Consumer에서 자동으로 Context 전파
// Producer: 메시지 헤더에 trace context 주입
// Consumer: 메시지 헤더에서 trace context 추출

@Service
public class OrderEventPublisher {

    private final KafkaTemplate<String, OrderEvent> kafkaTemplate;

    public void publishOrderCreated(Order order) {
        // OTel Agent가 자동으로 traceparent 헤더를 Kafka 메시지에 주입
        kafkaTemplate.send("order-events",
                order.getId().toString(),
                new OrderCreatedEvent(order));
    }
}

@KafkaListener(topics = "order-events")
public void handleOrderEvent(OrderCreatedEvent event) {
    // OTel Agent가 자동으로 Kafka 메시지에서 trace context를 추출
    // 동일한 Trace ID로 연결됨
    inventoryService.reserveStock(event.getOrderId(), event.getItems());
}

8. 실무 구성: docker-compose로 OTel 스택 구축

# docker-compose.yml
version: '3.8'

services:
  # Spring Boot 애플리케이션
  order-service:
    build: ./order-service
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - OTEL_SERVICE_NAME=order-service
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
      - OTEL_METRICS_EXPORTER=otlp
      - OTEL_LOGS_EXPORTER=otlp
      - JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar
    depends_on:
      - otel-collector
    networks:
      - observability

  payment-service:
    build: ./payment-service
    ports:
      - "8081:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - OTEL_SERVICE_NAME=payment-service
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
      - JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar
    depends_on:
      - otel-collector
    networks:
      - observability

  # OpenTelemetry Collector
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.108.0
    command: ["--config", "/etc/otel-collector-config.yaml"]
    volumes:
      - ./config/otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "8889:8889"   # Prometheus exporter
    depends_on:
      - tempo
      - prometheus
      - loki
    networks:
      - observability

  # Grafana Tempo (Traces)
  tempo:
    image: grafana/tempo:2.6.0
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./config/tempo-config.yaml:/etc/tempo.yaml
      - tempo-data:/var/tempo
    ports:
      - "3200:3200"   # Tempo API
    networks:
      - observability

  # Prometheus (Metrics)
  prometheus:
    image: prom/prometheus:v2.54.0
    volumes:
      - ./config/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--web.enable-remote-write-receiver'
      - '--enable-feature=exemplar-storage'
    networks:
      - observability

  # Loki (Logs)
  loki:
    image: grafana/loki:3.2.0
    volumes:
      - ./config/loki-config.yaml:/etc/loki/config.yaml
      - loki-data:/loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/config.yaml
    networks:
      - observability

  # Grafana (시각화)
  grafana:
    image: grafana/grafana:11.2.0
    volumes:
      - ./config/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
      - grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_AUTH_DISABLE_LOGIN_FORM=true
    depends_on:
      - prometheus
      - tempo
      - loki
    networks:
      - observability

volumes:
  tempo-data:
  prometheus-data:
  loki-data:
  grafana-data:

networks:
  observability:
    driver: bridge

Prometheus 설정

# config/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  # OTel Collector의 Prometheus Exporter
  - job_name: 'otel-collector'
    static_configs:
      - targets: ['otel-collector:8889']

  # Spring Boot Actuator
  - job_name: 'spring-boot'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['order-service:8080', 'payment-service:8080']

Dockerfile (Spring Boot + OTel Agent)

# Dockerfile
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

# OpenTelemetry Java Agent 다운로드
ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.8.0/opentelemetry-javaagent.jar /app/opentelemetry-javaagent.jar

# 애플리케이션 JAR 복사
COPY build/libs/order-service.jar /app/app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

실행 및 확인

# 전체 스택 실행
docker-compose up -d

# 서비스 상태 확인
docker-compose ps

# 테스트 요청
curl -X POST http://localhost:8080/api/orders \
  -H "Content-Type: application/json" \
  -d '{"customerId": 1, "items": [{"productId": 100, "quantity": 2}]}'

# Grafana 접속: http://localhost:3000
# → Explore → Tempo → Service Graph에서 서비스 간 호출 관계 시각화
# → 특정 Trace ID로 전체 요청 흐름 확인

# Prometheus 쿼리 예시 (Grafana에서)
# - 서비스별 요청 수: sum(rate(http_server_request_duration_seconds_count[5m])) by (service_name)
# - P99 응답 시간: histogram_quantile(0.99, rate(http_server_request_duration_seconds_bucket[5m]))
# - 에러율: sum(rate(http_server_request_duration_seconds_count{http_response_status_code=~"5.."}[5m])) / sum(rate(http_server_request_duration_seconds_count[5m]))

마치며

OpenTelemetry는 마이크로서비스 환경에서 관측 가능성을 확보하는 핵심 도구입니다. Traces, Metrics, Logs를 하나의 표준으로 통합하여 벤더 종속 없이 유연한 모니터링 스택을 구축할 수 있습니다. 이 글에서 다룬 내용을 정리합니다.

  • Java Agent로 시작: 코드 변경 없이 200+ 라이브러리를 자동 계측합니다. JVM 옵션에 Agent JAR만 추가하면 됩니다.
  • @WithSpan으로 비즈니스 로직 추적: 자동 계측으로 부족한 부분은 @WithSpan과 커스텀 Span으로 보완합니다. 비즈니스 메트릭(주문 금액, 사용자 등급 등)을 Attribute로 추가하면 분석에 유용합니다.
  • OTel Collector는 중앙 허브: Receivers로 데이터를 받고, Processors로 필터링/샘플링하고, Exporters로 다양한 백엔드에 전송합니다. 샘플링 전략으로 비용을 제어합니다.
  • Grafana 통합 스택: Tempo(Traces) + Prometheus(Metrics) + Loki(Logs)를 Grafana로 통합 시각화합니다. Trace에서 로그로, 메트릭에서 Trace로 연결하는 상관 분석이 핵심입니다.
  • Context Propagation이 핵심: W3C TraceContext 헤더로 서비스 간 Trace가 연결됩니다. HTTP, gRPC, Kafka 모두 자동 전파를 지원합니다.

관측 가능성은 장애 대응뿐 아니라 성능 최적화, 용량 계획, 비즈니스 분석까지 활용됩니다. 이 글의 docker-compose 설정으로 로컬 환경에서 먼저 경험해보고, 점진적으로 운영 환경에 도입하는 것을 권장합니다. 한 번 분산 추적의 효과를 경험하면, 없이는 디버깅할 수 없게 됩니다.