들어가며
"주문 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 설정으로 로컬 환경에서 먼저 경험해보고, 점진적으로 운영 환경에 도입하는 것을 권장합니다. 한 번 분산 추적의 효과를 경험하면, 없이는 디버깅할 수 없게 됩니다.
'DevOps' 카테고리의 다른 글
| Nginx 완벽 가이드 - 리버스 프록시부터 로드 밸런싱까지 (1) | 2026.04.15 |
|---|---|
| Git 고급 전략 - 브랜치 전략부터 위기 탈출까지 (1) | 2026.04.15 |
| Docker Compose 실전 - Spring Boot + DB + Redis + Kafka 개발 환경 구축 (0) | 2026.04.14 |
| Prometheus + Grafana 실전 구축 - Spring Boot 모니터링 완벽 가이드 (2) | 2026.04.09 |
| Terraform으로 인프라 코드화 - AWS 실전 예제로 배우는 IaC (0) | 2026.04.07 |