이번 글에서는 프로메테우스와 그라파나를 활용한 Observability 구성 시리즈의 일곱 번째 포스트로, Observability의 진정한 핵심인 '상관관계(Correlation)'의 중요성과 실제 구현 방법에 대해 알아보겠습니다.
📌 상관관계의 중요성
지금까지 살펴본 Observability의 세 가지 핵심 요소(메트릭, 로그, 트레이스)는 각각 중요한 정보를 제공하지만, 이들을 서로 연결하지 못한다면 복잡한 시스템에서 발생하는 문제를 효과적으로 분석하기 어렵습니다. 상관관계는 이러한 데이터 간의 연결고리를 만들어 총체적인 시스템 이해를 가능하게 합니다.
✅ 상관관계란 무엇인가?
Observability 맥락에서의 상관관계는 서로 다른 데이터 소스(메트릭, 로그, 트레이스)의 정보를 연결하여 단일 이벤트나 문제의 전체적인 그림을 구성하는 능력을 의미합니다.
▶️ 상관관계의 예시:
- 특정 시간대에 CPU 사용률 급증(메트릭)과 관련된 오류 로그(로그) 연결
- 지연 시간이 높은 API 요청(메트릭)과 해당 요청의 전체 경로(트레이스) 연결
- 데이터베이스 쿼리 오류(로그)와 해당 쿼리를 발생시킨 사용자 요청(트레이스) 연결
✅ 왜 상관관계가 중요한가?
복잡한 분산 시스템에서 문제를 효과적으로 진단하고 해결하기 위해서는 각 데이터 소스를 독립적으로 분석하는 것보다 상관관계를 통해 통합된 관점에서 분석하는 것이 필수적입니다.
▶️ 상관관계의 주요 이점:
- 근본 원인 분석 가속화: 증상(메트릭)에서 원인(로그, 트레이스)으로 빠르게 이동
- 컨텍스트 유지: 단편적인 정보가 아닌 전체적인 맥락에서 문제 이해
- 패턴 인식 강화: 시간, 서비스, 사용자 등 다양한 차원에서 패턴 발견
- 선제적 문제 해결: 초기 징후를 종합적으로 분석하여 심각한 장애 예방
- 협업 향상: 개발, 운영, 보안 등 다양한 팀이 동일한 데이터 기반으로 소통
# 상관관계 없이 각 도구에서 개별적으로 문제를 추적하는 경우:
1. 메트릭 대시보드에서 API 서버의 메모리 사용량 증가 감지
2. 로그 시스템에서 "메모리 부족" 오류 메시지 검색
3. 분산 추적 시스템에서 최근 요청들을 수동으로 확인
4. 세 가지 데이터 간의 관계를 수동으로 분석하여 패턴 찾기
# 상관관계가 구현된 경우:
1. 메트릭 대시보드에서 API 서버의 메모리 사용량 증가 감지
2. 연결된 로그와 트레이스 데이터로 직접 이동 (클릭 한 번)
3. 메모리 사용량 증가와 연관된 특정 요청 패턴 즉시 확인
4. 근본 원인(특정 API 엔드포인트의 메모리 누수)을 빠르게 식별
📌 상관관계를 위한 키 식별자
효과적인 상관관계를 구현하기 위해서는 여러 데이터 소스를 연결할 수 있는 공통 식별자가 필요합니다. 이러한 식별자는 다양한 시스템 간에 일관되게 전파되어야 합니다.
✅ 주요 상관관계 식별자
▶️ 트레이스 ID (Trace ID):
- 정의: 단일 요청의 전체 경로를 식별하는 고유 ID
- 사용: 분산 시스템의 여러 서비스에 걸친 요청 추적
- 전파 방식: HTTP 헤더, 메시지 속성, 컨텍스트 객체 등
# HTTP 요청 헤더에 포함된 트레이스 ID 예시
GET /api/users/123 HTTP/1.1
Host: example.com
X-Trace-ID: 4bf92f3577b34da6a3ce929d0e0e4736
X-Span-ID: 00f067aa0ba902b7
▶️ 요청 ID (Request ID):
- 정의: 단일 요청을 식별하는 고유 ID (트레이스 ID와 유사하나 더 단순한 형태)
- 사용: API 게이트웨이나 웹 서버에서 생성하여 백엔드 서비스로 전달
- 전파 방식: HTTP 헤더, 로그 필드, 메트릭 레이블
// Java 서블릿 필터에서 요청 ID 생성 예시
@Component
public class RequestIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestId = request.getHeader("X-Request-ID");
// 요청 ID가 없으면 생성
if (requestId == null || requestId.isEmpty()) {
requestId = UUID.randomUUID().toString();
}
// 응답 헤더에 요청 ID 추가
response.setHeader("X-Request-ID", requestId);
// MDC에 요청 ID 추가 (로깅용)
MDC.put("requestId", requestId);
try {
filterChain.doFilter(request, response);
} finally {
// MDC 정리
MDC.remove("requestId");
}
}
}
▶️ 세션 ID (Session ID):
- 정의: 사용자 세션을 식별하는 고유 ID
- 사용: 사용자 활동 추적, 세션 기반 문제 분석
- 전파 방식: 쿠키, 인증 토큰, 로그 컨텍스트
▶️ 사용자 ID (User ID):
- 정의: 특정 사용자를 식별하는 ID
- 사용: 사용자별 경험 모니터링, 사용자 관련 문제 분석
- 전파 방식: 인증 토큰, 로그 필드, 메트릭 레이블
▶️ 배포 또는 버전 ID:
- 정의: 애플리케이션 배포나 버전을 식별하는 ID
- 사용: 특정 배포나 버전과 관련된 문제 분석
- 전파 방식: 환경 변수, 로그 필드, 메트릭 레이블
✅ W3C Trace Context 표준
분산 시스템에서 상관관계 정보를 일관되게 전파하기 위해 W3C에서는 Trace Context 표준을 제정했습니다. 이 표준은 벤더 중립적인 방식으로 트레이싱 정보를 전달하는 방법을 정의합니다.
▶️ W3C Trace Context의 주요 구성 요소:
- traceparent: 트레이스 ID, 스팬 ID, 샘플링 플래그 등의 주요 추적 정보를 포함
- tracestate: 벤더별 추가 정보를 포함할 수 있는 확장 필드
# W3C Trace Context 헤더 예시
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7
- traceparent 형식: 버전-트레이스ID-부모스팬ID-트레이스플래그
- 버전: 00 (현재 버전)
- 트레이스 ID: 32자리 16진수 (128비트)
- 부모 스팬 ID: 16자리 16진수 (64비트)
- 트레이스 플래그: 8비트 플래그 (01: 샘플링됨, 00: 샘플링되지 않음)
📌 상관관계 구현 방법
이제 실제로 상관관계를 구현하는 방법에 대해 살펴보겠습니다. 주요 단계와 구현 패턴을 알아보겠습니다.
✅ 상관관계 ID 생성 및 전파
▶️ 상관관계 ID 생성:
- 시기: 외부 요청이 시스템에 처음 도착할 때 (예: API 게이트웨이, 로드 밸런서)
- 방법: UUID, ULID 등의 고유 식별자 생성
- 주의 사항: 충분한 고유성 보장, 가독성과 디버깅 편의성 고려
// Go 언어에서 트레이스 ID 생성 예시
import (
"github.com/google/uuid"
"net/http"
)
func TracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 기존 트레이스 ID 확인
traceID := r.Header.Get("X-Trace-ID")
if traceID == "" {
// 없으면 새로 생성
traceID = uuid.New().String()
}
// 요청 및 응답 헤더에 트레이스 ID 설정
r.Header.Set("X-Trace-ID", traceID)
w.Header().Set("X-Trace-ID", traceID)
// 컨텍스트에 트레이스 ID 추가
ctx := context.WithValue(r.Context(), "traceID", traceID)
// 다음 핸들러로 전달
next.ServeHTTP(w, r.WithContext(ctx))
})
}
▶️ 서비스 간 전파:
- 동기 통신: HTTP 헤더, gRPC 메타데이터
- 비동기 통신: 메시지 속성, 이벤트 메타데이터
- 배치 작업: 작업 컨텍스트, 메타데이터 파일
// Spring Boot RestTemplate에서 트레이스 ID 전파 예시
@Component
public class TracingRestTemplateInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// 현재 스레드의 MDC에서 트레이스 ID 가져오기
String traceId = MDC.get("traceId");
// 요청 헤더에 트레이스 ID 추가
request.getHeaders().add("X-Trace-ID", traceId);
request.getHeaders().add("traceparent", "00-" + traceId + "-" + generateSpanId() + "-01");
return execution.execute(request, body);
}
private String generateSpanId() {
// 16자리 16진수 스팬 ID 생성
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
}
✅ 로깅 시스템에 상관관계 정보 포함
로그 메시지에 상관관계 ID를 포함하면 동일한 요청과 관련된 모든 로그를 쉽게 검색하고 분석할 수 있습니다.
▶️ 구조화된 로깅:
- JSON 형식의 로그에 상관관계 ID를 표준 필드로 포함
- MDC(Mapped Diagnostic Context)를 사용하여 로그 패턴에 자동으로 포함
<!-- Logback 구성 예시 (logback.xml) -->
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- MDC 값을 로그에 포함 -->
<includeMdc>true</includeMdc>
<!-- 추가 필드 -->
<customFields>{"application":"user-service","environment":"production"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON" />
</root>
// Java 로깅 예시
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public User findUser(String userId) {
// 상관관계 ID는 이미 필터나 인터셉터에서 MDC에 설정되어 있다고 가정
String traceId = MDC.get("traceId");
String requestId = MDC.get("requestId");
logger.info("사용자 조회 시작: userId={}", userId);
// 로그 출력 예시:
// {"timestamp":"2023-03-15T14:22:33.456Z","level":"INFO","logger":"UserService",
// "message":"사용자 조회 시작: userId=user123","traceId":"abc-123","requestId":"req-456",
// "application":"user-service","environment":"production"}
try {
return userRepository.findById(userId);
} catch (Exception e) {
// 오류 로그에도 동일한 상관관계 ID가 포함됨
logger.error("사용자 조회 중 오류 발생: userId={}", userId, e);
throw e;
}
}
}
✅ 메트릭에 상관관계 레이블 추가
메트릭에 상관관계 정보를 레이블로 추가하면 특정 요청, 사용자, 배포 등과 관련된 메트릭을 필터링하고 분석할 수 있습니다.
▶️ 상관관계 레이블 추가:
- 주의 사항: 카디널리티가 높은 레이블(트레이스 ID, 요청 ID 등)은 제한적으로 사용
- 적절한 레이블: 서비스, 엔드포인트, 환경, 배포 버전 등
// Micrometer + Spring Boot에서 메트릭에 레이블 추가 예시
@Component
public class WebMvcMetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry;
public WebMvcMetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - startTime;
// 메트릭에 레이블 추가 (상관관계 정보 포함)
// 요청 ID와 같은 고카디널리티 레이블은 피함
Timer.builder("http.server.requests")
.tag("method", request.getMethod())
.tag("uri", getUriPattern(request))
.tag("status", String.valueOf(response.getStatus()))
.tag("service", "user-service") // 서비스 식별
.tag("version", getApplicationVersion()) // 배포 버전
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
}
}
private String getUriPattern(HttpServletRequest request) {
// URI 템플릿 패턴 반환 (예: /users/{id})
// ...
}
private String getApplicationVersion() {
// 애플리케이션 버전 정보 반환
// ...
}
}
✅ 트레이스와 로그, 메트릭 연결
분산 추적 시스템에서 트레이스와 로그, 메트릭을 연결하면 완전한 상관관계를 구현할 수 있습니다.
▶️ 트레이스와 로그 연결:
- 로그에 트레이스 ID와 스팬 ID 포함
- 트레이스 스팬에 관련 로그 이벤트 연결
// OpenTelemetry Java SDK에서 트레이스와 로그 연결 예시
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
// 트레이서 및 로거 설정
Tracer tracer = openTelemetry.getTracer("my-service");
Logger logger = LoggerFactory.getLogger(MyService.class);
// 요청 처리
@Override
public void processRequest(HttpServletRequest request) {
// 트레이스 컨텍스트 추출
Context extractedContext = textMapPropagator.extract(Context.current(), request, getter);
Span span = tracer.spanBuilder("processRequest")
.setParent(extractedContext)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 현재 스팬 정보를 MDC에 추가하여 로그에 포함되도록 함
MDC.put("traceId", span.getSpanContext().getTraceId());
MDC.put("spanId", span.getSpanContext().getSpanId());
// 로그와 스팬 이벤트를 모두 기록
logger.info("요청 처리 시작: {}", request.getRequestURI());
span.addEvent("요청 처리 시작", Attributes.of(AttributeKey.stringKey("uri"), request.getRequestURI()));
// 비즈니스 로직 처리
processBusinessLogic(request);
// 처리 완료 로그
logger.info("요청 처리 완료");
span.setStatus(StatusCode.OK);
} catch (Exception e) {
// 오류 발생 시 로그와 스팬에 모두 기록
logger.error("요청 처리 중 오류 발생", e);
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
// MDC 정리
MDC.remove("traceId");
MDC.remove("spanId");
span.end();
}
}
▶️ 트레이스와 메트릭 연결:
- 트레이스 ID로 메트릭 필터링 (제한적으로 사용)
- 메트릭 데이터에서 관련 트레이스로 이동할 수 있는 링크 제공
// Prometheus 클라이언트 라이브러리와 OpenTelemetry 통합 예시 (Go)
import (
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
)
// 요청 처리 시간 히스토그램 메트릭 정의
var requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP 요청 처리 시간 히스토그램",
Buckets: []float64{0.1, 0.3, 0.5, 1, 3, 5},
},
[]string{"method", "endpoint", "status", "service", "version"},
)
func HandleRequest(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
// 트레이스 컨텍스트 추출
ctx := r.Context()
span := trace.SpanFromContext(ctx)
// 요청 처리
// ...
// 메트릭 기록 (상관관계 레이블 포함)
duration := time.Since(startTime).Seconds()
requestDuration.WithLabelValues(
r.Method,
r.URL.Path,
strconv.Itoa(statusCode),
"user-service",
"v1.0.0",
).Observe(duration)
// 트레이스 이벤트에 메트릭 정보 추가
span.AddEvent("metric_recorded", trace.WithAttributes(
attribute.String("metric_name", "http_request_duration_seconds"),
attribute.Float64("duration", duration),
))
}
📌 상관관계 시각화 및 분석
상관관계 정보를 수집한 후에는 이를 효과적으로 시각화하고 분석하는 도구와 방법이 필요합니다.
✅ 통합 대시보드
그라파나와 같은 도구를 사용하여 메트릭, 로그, 트레이스 데이터를 통합한 대시보드를 구성할 수 있습니다.
▶️ 그라파나 통합 대시보드 구성:
- 데이터 소스 통합: Prometheus(메트릭), Loki(로그), Tempo(트레이스) 연결
- 데이터 소스 간 연결: 상관관계 필드를 사용한 데이터 소스 간 이동
- 상황별 대시보드: 서비스, 엔드포인트, 사용자별 대시보드 구성
# 그라파나 대시보드 예시 (JSON)
{
"panels": [
{
"title": "HTTP 요청 속도",
"datasource": "Prometheus",
"targets": [
{
"expr": "sum(rate(http_requests_total{service=\"user-service\"}[5m])) by (endpoint)",
"legendFormat": "{{endpoint}}"
}
],
"links": [
{
"title": "관련 로그 보기",
"url": "/explore?left=%7B\"datasource\":\"Loki\",\"queries\":%5B%7B\"expr\":\"{service=\\\"user-service\\\"}\",%7D%5D%7D",
"targetBlank": true
}
]
},
{
"title": "오류 로그",
"datasource": "Loki",
"targets": [
{
"expr": "{service=\"user-service\"} |= \"ERROR\"",
"refId": "A"
}
],
"options": {
"showTime": true,
"showLabels": false,
"showCommonLabels": true,
"wrapLogMessage": true,
"prettifyLogMessage": true,
"enableLogDetails": true,
"dedupStrategy": "none"
},
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"host": true,
"id": true
},
"renameByName": {}
}
}
]
}
]
}
✅ 시각화 전략
▶️ 시간 기반 상관관계:
- 동일한 시간대의 이벤트 함께 표시: 메트릭 스파이크, 오류 로그, 느린 트레이스를 단일 타임라인에 표시
- 시간 동기화: 모든 데이터 소스의 시간을 정확히 동기화하여 정확한 상관관계 분석
- 범위 선택 연동: 한 패널에서 시간 범위를 선택하면 다른 패널도 자동으로 해당 범위로 필터링
▶️ 컨텍스트 전환 링크:
- 메트릭에서 로그로: 특정 메트릭 데이터 포인트에서 관련 로그로 이동할 수 있는 링크 제공
- 로그에서 트레이스로: 로그 항목에서 관련 트레이스로 이동할 수 있는 링크 제공
- 트레이스에서 메트릭으로: 트레이스 세부 정보에서 관련 메트릭 대시보드로 이동할 수 있는 링크 제공
▶️ 종합 검색 및 필터링:
- 통합 검색: 트레이스 ID, 요청 ID, 사용자 ID 등으로 모든 데이터 소스를 한 번에 검색
- 공통 필터: 서비스, 환경, 버전 등 공통 필터를 적용하여 모든 패널에 일관되게 반영
- 드릴다운 필터링: 상위 수준 뷰에서 클릭하여 더 상세한 정보로 드릴다운
📌 상관관계 구현의 모범 사례 및 주의사항
✅ 모범 사례
▶️ 일관된 식별자 사용:
- 표준화된 식별자: W3C Trace Context와 같은 업계 표준을 적극 활용
- 일관된 형식: 모든 서비스와 시스템에서 동일한 형식의 식별자 사용
- 선택적 세분화: 필요한 수준에 따라 다양한 식별자(트레이스 ID, 요청 ID, 사용자 ID 등) 활용
▶️ 효율적인 구현:
- 자동화된 전파: 상관관계 ID의 자동 생성 및 전파를 위한 미들웨어, 인터셉터, 라이브러리 활용
- 프레임워크 활용: OpenTelemetry, Spring Cloud Sleuth 등 기존 프레임워크 활용
- 샘플링 전략: 고부하 시스템에서는 지능적인 샘플링으로 오버헤드 관리
▶️ 문서화 및 교육:
- 개발자 가이드: 상관관계 ID 사용 및 전파에 대한 명확한 가이드라인 제공
- 표준화된 접근법: 모든 팀이 일관된 방식으로 상관관계를 구현하도록 표준화
- 실제 사례 공유: 상관관계를 통해 해결된 실제 문제 사례를 공유하여 가치 입증
✅ 주의사항
▶️ 성능 영향:
- 로깅 오버헤드: 과도한 로깅과 상관관계 정보 추가로 인한 성능 저하 주의
- 메트릭 카디널리티: 고유 식별자를 메트릭 레이블로 사용할 때 카디널리티 폭발 위험
- 트레이싱 오버헤드: 100% 샘플링 대신 지능적인 샘플링으로 오버헤드 관리
▶️ 복잡성 관리:
- 단계적 구현: 한 번에 모든 상관관계를 구현하기보다 단계적으로 접근
- 필수 식별자 집중: 모든 식별자가 아닌 가장 중요한 식별자에 집중
- 통합 도구 활용: 복잡성을 줄이기 위해 통합된 도구와 프레임워크 활용
▶️ 데이터 보존 정책:
- 일관된 보존 기간: 상관관계 분석을 위해 모든 데이터 소스의 보존 기간 조정
- 선택적 보존: 중요 이벤트와 관련된 데이터는 더 오래 보존
- 규정 준수: 데이터 보존 정책 수립 시 개인정보 보호 규정 고려
📌 결론
Observability의 세 가지 핵심 요소(메트릭, 로그, 트레이스)는 각각 중요한 정보를 제공하지만, 이들 간의 상관관계를 구현할 때 진정한 가치를 발휘합니다. 상관관계를 통해 복잡한 분산 시스템에서 발생하는 문제의 근본 원인을 빠르게 파악하고, 효과적으로 해결할 수 있습니다.
효과적인 상관관계 구현을 위해서는:
- 표준화된 식별자 사용
- 자동화된 전파 메커니즘 구축
- 통합된 시각화 및 분석 도구 활용
- 성능과 복잡성의 균형 유지
이러한 접근법을 통해 Observability의 완전한 잠재력을 실현하고, 더 안정적이고 효율적인 시스템을 운영할 수 있습니다. 상관관계는 단순한 기술적 구현을 넘어, 시스템 문제 해결에 대한 사고방식의 변화를 의미합니다. 개별 데이터 포인트가 아닌, 시스템 전체의 맥락에서 문제를 바라보고 해결하는 능력이야말로 현대적인 Observability의 핵심입니다.