관측성 분석: Logs/Metrics/Traces 상관 분석
지원 버전: Loki 3.0+, Tempo 2.4+, Prometheus 2.50+, Grafana 10.0+ 마지막 업데이트: 2026년 2월 23일
< 이전: 운영 알림 구성 | 목차 | 다음: 관측성 스택 운영 >
목차
상관 분석 전략
효과적인 관측성(Observability)은 로그, 메트릭, 트레이스 세 가지 신호(Three Pillars)를 상호 연결하여 분석할 때 진정한 가치를 발휘합니다. 상관 분석을 통해 문제의 근본 원인을 신속하게 파악할 수 있습니다.
Trace ID 전파
분산 시스템에서 요청을 추적하려면 Trace ID를 모든 서비스 간에 전파해야 합니다.
W3C TraceContext 표준
W3C TraceContext는 분산 추적을 위한 표준화된 HTTP 헤더 형식입니다.
# W3C TraceContext 헤더 형식
traceparent: 00-{trace-id}-{span-id}-{flags}
tracestate: {vendor-specific-data}
# 예시
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7- trace-id: 32자리 16진수 (128비트)
- span-id: 16자리 16진수 (64비트)
- flags: 2자리 16진수 (샘플링 결정 등)
B3 헤더 형식 (Zipkin 호환)
# B3 Single Header
b3: {TraceId}-{SpanId}-{SamplingState}-{ParentSpanId}
# B3 Multiple Headers
X-B3-TraceId: 80f198ee56343ba864fe8b2a57d3eff7
X-B3-SpanId: e457b5a2e4d86bd1
X-B3-ParentSpanId: 05e3ac9a4f6e3b90
X-B3-Sampled: 1OpenTelemetry SDK 계측 패턴
애플리케이션에서 OpenTelemetry SDK를 사용하여 자동 및 수동 계측을 구현합니다.
Go 애플리케이션 계측
package main
import (
"context"
"net/http"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
var tracer trace.Tracer
func initTracer() func() {
ctx := context.Background()
// OTLP exporter 설정
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
log.Fatal(err)
}
// Resource 설정 (서비스 메타데이터)
res, _ := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("my-service"),
semconv.ServiceVersion("1.0.0"),
semconv.DeploymentEnvironment("production"),
),
)
// TracerProvider 설정
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
// Propagator 설정 (W3C TraceContext + Baggage)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
tracer = tp.Tracer("my-service")
return func() {
tp.Shutdown(ctx)
}
}
func main() {
cleanup := initTracer()
defer cleanup()
// HTTP 핸들러에 자동 계측 적용
handler := otelhttp.NewHandler(http.HandlerFunc(handleRequest), "handle-request")
http.Handle("/api/v1/users", handler)
http.ListenAndServe(":8080", nil)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 수동 span 생성
ctx, span := tracer.Start(ctx, "process-user-request")
defer span.End()
// span에 속성 추가
span.SetAttributes(
semconv.HTTPMethod(r.Method),
semconv.HTTPRoute("/api/v1/users"),
)
// 비즈니스 로직 수행
processRequest(ctx)
}
func processRequest(ctx context.Context) {
_, span := tracer.Start(ctx, "database-query")
defer span.End()
// DB 쿼리 실행
}Python 애플리케이션 계측
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.baggage.propagation import W3CBaggagePropagator
import logging
# Resource 설정
resource = Resource.create({
SERVICE_NAME: "python-api-service",
"service.version": "1.0.0",
"deployment.environment": "production",
})
# TracerProvider 설정
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(
OTLPSpanExporter(endpoint="otel-collector:4317", insecure=True)
)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
# Propagator 설정
set_global_textmap(CompositePropagator([
TraceContextTextMapPropagator(),
W3CBaggagePropagator(),
]))
# tracer 인스턴스
tracer = trace.get_tracer(__name__)
# Flask 자동 계측
from flask import Flask
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()
@app.route('/api/orders')
def get_orders():
# 현재 span 컨텍스트 가져오기
current_span = trace.get_current_span()
trace_id = format(current_span.get_span_context().trace_id, '032x')
# 로그에 trace_id 포함
logging.info(f"Processing order request", extra={
'trace_id': trace_id,
'span_id': format(current_span.get_span_context().span_id, '016x')
})
# 수동 span 생성
with tracer.start_as_current_span("fetch-orders-from-db") as span:
span.set_attribute("db.system", "postgresql")
span.set_attribute("db.operation", "SELECT")
orders = fetch_orders()
return ordersExemplars: 메트릭과 트레이스 연결
Exemplars는 메트릭과 특정 트레이스를 연결하여 이상 징후가 발생한 정확한 요청을 추적할 수 있게 합니다.
// Go에서 Prometheus 메트릭에 Exemplar 추가
import (
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
)
var httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path", "status"},
)
func recordMetricWithExemplar(ctx context.Context, duration float64, labels prometheus.Labels) {
span := trace.SpanFromContext(ctx)
traceID := span.SpanContext().TraceID().String()
// Exemplar와 함께 메트릭 기록
httpRequestDuration.With(labels).(prometheus.ExemplarObserver).ObserveWithExemplar(
duration,
prometheus.Labels{"trace_id": traceID},
)
}Log-Trace 상관관계
로그에 Trace ID를 주입하여 로그와 트레이스를 연결합니다.
구조화된 로깅 (JSON)
{
"timestamp": "2025-01-15T10:30:45.123Z",
"level": "ERROR",
"message": "Failed to process payment",
"service": "payment-service",
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"span_id": "b7ad6b7169203331",
"user_id": "user-12345",
"order_id": "order-67890",
"error": "insufficient_funds"
}Logback 설정 (Java/Spring)
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<provider class="net.logstash.logback.composite.loggingevent.LoggingEventCompositeJsonProvider">
<providers>
<timestamp/>
<logLevel/>
<message/>
<mdc/>
<stackTrace/>
</providers>
</provider>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON"/>
</root>
</configuration>아키텍처 다이어그램
상관 분석 워크플로우
문제 발생 시 상관 분석을 통한 근본 원인 분석 워크플로우입니다.
- Alert 발생: Prometheus에서 알림 트리거
- Metric 확인: 관련 메트릭 대시보드에서 이상 패턴 확인
- Trace 조회: Exemplar를 통해 또는 시간 범위로 관련 트레이스 검색
- Span 분석: 트레이스 내 개별 span에서 지연 또는 오류 구간 식별
- Log 조회: Trace ID로 관련 로그 필터링하여 상세 컨텍스트 확인
- 근본 원인 파악: 수집된 정보를 종합하여 문제 원인 도출
Loki LogQL 분석
Loki의 LogQL은 Prometheus의 PromQL에서 영감을 받은 로그 쿼리 언어입니다. 스트림 선택자와 필터를 조합하여 로그를 검색하고 분석합니다.
오류율 계산
# 네임스페이스별 ERROR 로그 발생률 (5분 윈도우)
sum(rate({namespace="production"} |= "ERROR" [5m])) by (app)
# 서비스별 오류 로그 비율
sum(rate({app="api-gateway"} |= "error" [5m]))
/
sum(rate({app="api-gateway"} [5m]))
# HTTP 5xx 오류 로그 카운트
sum(count_over_time({namespace="production"} | json | status_code >= 500 [1h])) by (service)지연 시간 추출
JSON 형식 로그에서 지연 시간을 추출하고 분석합니다.
# 지연 시간 500ms 초과 로그 필터링
{app="api-service"} | json | latency_ms > 500
# 지연 시간 분포 분석 (히스토그램)
quantile_over_time(0.95, {app="api-service"} | json | unwrap latency_ms [5m]) by (endpoint)
# P99 지연 시간이 높은 엔드포인트 찾기
topk(5,
quantile_over_time(0.99,
{app="api-service"} | json | unwrap response_time_ms [5m]
) by (endpoint)
)
# 평균 지연 시간 추세
avg_over_time({app="api-service"} | json | unwrap latency_ms [5m]) by (endpoint)로그 기반 알림 규칙
Loki Ruler를 사용하여 로그 기반 알림을 설정합니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: loki-rules
namespace: monitoring
data:
rules.yaml: |
groups:
- name: log-based-alerts
interval: 1m
rules:
# 오류 로그 급증 알림
- alert: HighErrorLogRate
expr: |
sum(rate({namespace="production"} |= "ERROR" [5m])) by (app) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "높은 오류 로그 발생률: {{ $labels.app }}"
description: "{{ $labels.app }}에서 분당 10건 이상의 오류 로그 발생"
# 특정 오류 패턴 감지
- alert: DatabaseConnectionError
expr: |
count_over_time({app=~".+"} |~ "connection refused|timeout|connection reset" [5m]) > 5
for: 2m
labels:
severity: critical
annotations:
summary: "데이터베이스 연결 오류 감지"
description: "데이터베이스 연결 관련 오류 로그가 5건 이상 발생했습니다"
# OOM 킬러 감지
- alert: OOMKillerDetected
expr: |
count_over_time({unit="kernel"} |= "Out of memory: Killed process" [5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "OOM Killer 활성화 감지"
description: "커널에서 OOM Killer가 프로세스를 종료했습니다"
# 보안 관련 로그 감지
- alert: SuspiciousLoginAttempts
expr: |
count_over_time({app="auth-service"} |= "authentication failed" [5m]) > 50
for: 2m
labels:
severity: warning
annotations:
summary: "의심스러운 로그인 시도 감지"
description: "5분 내 50회 이상의 인증 실패가 발생했습니다"레이블 전략과 카디널리티
Loki에서 효율적인 쿼리를 위한 레이블 전략입니다.
# 권장 레이블 (낮은 카디널리티)
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
# 정적이고 유한한 값을 가진 레이블만 사용
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
# 높은 카디널리티 레이블 제외 (pod name, node name 등)
# 피해야 할 레이블 (높은 카디널리티)
# - pod: 동적으로 생성되는 파드 이름 (예: api-6d4f5b7c8d-xj2kl)
# - request_id: 요청마다 고유한 값
# - user_id: 사용자 IDLogQL 패턴 매칭 및 파싱
# Nginx 액세스 로그 패턴 파싱
{app="nginx"}
| pattern `<ip> - - [<_>] "<method> <path> <_>" <status> <size>`
| status >= 400
# JSON 로그 파싱 및 필터링
{app="api-service"}
| json
| level = "error"
| message =~ ".*timeout.*"
| line_format "{{.timestamp}} [{{.level}}] {{.service}}: {{.message}}"
# 정규식을 사용한 필드 추출
{app="api-service"}
| regexp `(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}) (?P<level>\w+) (?P<message>.*)`
| level = "ERROR"
# logfmt 형식 파싱
{app="go-service"}
| logfmt
| duration > 1s
| status_code >= 500집계 쿼리
# 상위 5개 오류 발생 엔드포인트
topk(5,
sum(count_over_time({app="api-service"} | json | status_code >= 500 [1h])) by (endpoint)
)
# 시간별 오류 분포
sum(count_over_time({namespace="production"} |= "ERROR" [1h])) by (app)
# 백분위수 계산 (지연 시간)
quantile_over_time(0.99,
{app="api-service"} | json | unwrap latency_ms [5m]
) by (endpoint)
# 로그 볼륨 비율
sum(rate({namespace=~".+"}[5m])) by (namespace) / ignoring(namespace) group_left
sum(rate({namespace=~".+"}[5m]))멀티라인 로그 처리
스택 트레이스와 같은 멀티라인 로그를 처리합니다.
# Promtail 설정에서 멀티라인 처리
scrape_configs:
- job_name: java-apps
kubernetes_sd_configs:
- role: pod
pipeline_stages:
- multiline:
firstline: '^\d{4}-\d{2}-\d{2}'
max_wait_time: 3s
max_lines: 128
- regex:
expression: '^(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}) (?P<level>\w+) (?P<message>[\s\S]*)'
- labels:
level:전체 Loki 알림 규칙 예시
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: loki-alerting-rules
namespace: monitoring
labels:
release: prometheus
spec:
groups:
- name: loki.log.alerts
rules:
# 로그 수집 지연
- alert: LokiIngestionLag
expr: |
sum(loki_distributor_bytes_received_total) == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Loki 로그 수집 중단"
description: "Loki가 5분 이상 로그를 수집하지 못하고 있습니다"
# 로그 저장 실패
- alert: LokiIngesterFailures
expr: |
rate(loki_ingester_chunk_stored_bytes_total[5m]) == 0
for: 5m
labels:
severity: warning
annotations:
summary: "Loki 로그 저장 실패"
description: "Loki Ingester가 로그를 저장하지 못하고 있습니다"Prometheus PromQL 패턴
PromQL은 시계열 데이터를 쿼리하고 분석하기 위한 강력한 표현식 언어입니다.
RPS (Requests Per Second) 계산
# 서비스별 초당 요청 수
sum(rate(http_requests_total[5m])) by (service)
# 엔드포인트별 RPS
sum(rate(http_requests_total[5m])) by (method, path)
# 전체 클러스터 RPS
sum(rate(http_requests_total[5m]))
# 증가 추세 비교 (이전 시간 대비)
sum(rate(http_requests_total[5m]))
/
sum(rate(http_requests_total[5m] offset 1h))RED 메소드 구현
RED(Rate, Errors, Duration) 메소드는 서비스 모니터링의 황금 신호입니다.
# Rate: 초당 요청 수
sum(rate(http_requests_total[5m])) by (service)
# Errors: 오류율 (5xx 응답 비율)
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
# Duration: 요청 지연 시간 백분위수
# P50
histogram_quantile(0.50,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
# P95
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
# P99
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
# 평균 지연 시간
sum(rate(http_request_duration_seconds_sum[5m])) by (service)
/
sum(rate(http_request_duration_seconds_count[5m])) by (service)Istio Service Mesh 메트릭
Istio 환경에서 서비스 간 통신 메트릭을 분석합니다.
# 서비스별 요청 수
sum(rate(istio_requests_total[5m])) by (destination_service_name)
# 서비스 간 요청 실패율
sum(rate(istio_requests_total{response_code=~"5.."}[5m])) by (source_app, destination_app)
/
sum(rate(istio_requests_total[5m])) by (source_app, destination_app)
# P99 지연 시간
histogram_quantile(0.99,
sum(rate(istio_request_duration_milliseconds_bucket[5m])) by (le, destination_service_name)
)
# 서비스 메시 트래픽 볼륨
sum(rate(istio_tcp_sent_bytes_total[5m])) by (source_app, destination_app)
# mTLS 적용률
sum(rate(istio_requests_total{connection_security_policy="mutual_tls"}[5m]))
/
sum(rate(istio_requests_total[5m]))
# Envoy 프록시 오류
sum(rate(envoy_cluster_upstream_cx_connect_fail[5m])) by (cluster_name)ALB 메트릭 (CloudWatch 연동)
Amazon Managed Prometheus(AMP)에서 CloudWatch 메트릭을 쿼리합니다.
# ALB 평균 응답 시간
avg(aws_alb_target_response_time_average) by (load_balancer)
# ALB 요청 수
sum(rate(aws_alb_request_count_sum[5m])) by (load_balancer)
# ALB HTTP 5xx 오류 수
sum(rate(aws_alb_httpcode_target_5xx_count_sum[5m])) by (load_balancer)
# ALB 활성 연결 수
sum(aws_alb_active_connection_count_sum) by (load_balancer)
# Target Group 건강 상태
aws_alb_tg_healthy_host_count /
(aws_alb_tg_healthy_host_count + aws_alb_tg_unhealthy_host_count)
# Unhealthy target 감지
aws_alb_tg_unhealthy_host_count > 0AMP (Amazon Managed Prometheus) 쿼리 패턴
# 크로스 리전 메트릭 집계
sum(rate(http_requests_total{region=~"ap-northeast-1|ap-northeast-2"}[5m])) by (service, region)
# 장기 보존 데이터 쿼리 (30일 추세)
avg_over_time(http_requests_total[30d])
# 다중 클러스터 비교
sum(rate(http_requests_total[5m])) by (cluster)
# AMP 메트릭 수집 상태
up{job="amp-remote-write"}Recording Rules
자주 사용하는 쿼리를 Recording Rule로 사전 계산하여 성능을 최적화합니다.
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: recording-rules
namespace: monitoring
labels:
release: prometheus
spec:
groups:
- name: service.slo.rules
interval: 30s
rules:
# 서비스별 요청률
- record: service:http_requests_total:rate5m
expr: sum(rate(http_requests_total[5m])) by (service)
# 서비스별 오류율
- record: service:http_requests_errors:rate5m
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
# 서비스별 P99 지연 시간
- record: service:http_request_duration_seconds:p99
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
# 서비스별 P95 지연 시간
- record: service:http_request_duration_seconds:p95
expr: |
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
# 서비스별 평균 지연 시간
- record: service:http_request_duration_seconds:avg
expr: |
sum(rate(http_request_duration_seconds_sum[5m])) by (service)
/
sum(rate(http_request_duration_seconds_count[5m])) by (service)
- name: node.resources.rules
interval: 60s
rules:
# 노드별 CPU 사용률
- record: node:cpu_utilization:ratio
expr: |
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance)
# 노드별 메모리 사용률
- record: node:memory_utilization:ratio
expr: |
1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
# 노드별 디스크 사용률
- record: node:disk_utilization:ratio
expr: |
1 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})
- name: namespace.resources.rules
interval: 60s
rules:
# 네임스페이스별 CPU 사용량
- record: namespace:container_cpu_usage_seconds:sum_rate
expr: |
sum(rate(container_cpu_usage_seconds_total{container!=""}[5m])) by (namespace)
# 네임스페이스별 메모리 사용량
- record: namespace:container_memory_working_set_bytes:sum
expr: |
sum(container_memory_working_set_bytes{container!=""}) by (namespace)
# 네임스페이스별 네트워크 수신 대역폭
- record: namespace:container_network_receive_bytes:sum_rate
expr: |
sum(rate(container_network_receive_bytes_total[5m])) by (namespace)Recording Rules YAML 전체 예시
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: complete-recording-rules
namespace: monitoring
labels:
release: prometheus
app: kube-prometheus-stack
spec:
groups:
# SLO 관련 recording rules
- name: slo.rules
interval: 30s
rules:
- record: slo:service_availability:ratio_rate5m
expr: |
1 - (
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
)
- record: slo:service_latency_p99:seconds
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
# 리소스 효율성 rules
- name: efficiency.rules
interval: 60s
rules:
- record: pod:cpu_usage_vs_request:ratio
expr: |
sum(rate(container_cpu_usage_seconds_total{container!=""}[5m])) by (namespace, pod)
/
sum(kube_pod_container_resource_requests{resource="cpu"}) by (namespace, pod)
- record: pod:memory_usage_vs_request:ratio
expr: |
sum(container_memory_working_set_bytes{container!=""}) by (namespace, pod)
/
sum(kube_pod_container_resource_requests{resource="memory"}) by (namespace, pod)
# Istio 관련 rules
- name: istio.rules
interval: 30s
rules:
- record: istio:service_request_rate:sum_rate5m
expr: sum(rate(istio_requests_total[5m])) by (destination_service_name)
- record: istio:service_error_rate:ratio_rate5m
expr: |
sum(rate(istio_requests_total{response_code=~"5.."}[5m])) by (destination_service_name)
/
sum(rate(istio_requests_total[5m])) by (destination_service_name)
- record: istio:service_latency_p99:milliseconds
expr: |
histogram_quantile(0.99,
sum(rate(istio_request_duration_milliseconds_bucket[5m])) by (le, destination_service_name)
)Tempo TraceQL 분석
Grafana Tempo의 TraceQL은 분산 트레이스를 쿼리하기 위한 언어입니다.
기본 TraceQL 구문
# 기본 쿼리 구조
{ <span selector> }
# 서비스 이름으로 필터링
{ resource.service.name = "api-gateway" }
# Span 속성으로 필터링
{ span.http.status_code >= 500 }
# 여러 조건 조합
{ resource.service.name = "api-gateway" && span.http.status_code >= 500 }
# 정규식 매칭
{ span.http.url =~ "/api/v1/users.*" }지연 시간 분석
# 2초 이상 걸린 트레이스
{ duration > 2s }
# 특정 서비스에서 지연이 발생한 트레이스
{ resource.service.name = "database-service" && duration > 1s }
# 오류와 함께 높은 지연 시간
{ duration > 2s && status = error }
# Span 레벨 지연 시간 분석
{ span.name = "db.query" && span.duration > 500ms }오류 트레이스 검색
# HTTP 5xx 오류가 있는 트레이스
{ span.http.status_code >= 500 }
# 오류 상태의 트레이스
{ status = error }
# 특정 오류 메시지 포함
{ span.error.message =~ ".*timeout.*" }
# gRPC 오류 코드
{ span.rpc.grpc.status_code != 0 }
# 데이터베이스 오류
{ span.db.system = "postgresql" && status = error }서비스 의존성 매핑
# 특정 서비스를 호출하는 모든 서비스
{ resource.service.name != "target-service" } >> { resource.service.name = "target-service" }
# 서비스 간 호출 경로
{ resource.service.name = "frontend" } >> { resource.service.name = "api-gateway" } >> { resource.service.name = "user-service" }
# 외부 API 호출 추적
{ span.http.url =~ "https://external-api.com.*" }Span 속성 필터링
# HTTP 메서드별 필터링
{ span.http.method = "POST" }
# 특정 엔드포인트
{ span.http.route = "/api/v1/orders" }
# 데이터베이스 쿼리 분석
{ span.db.system = "mysql" && span.db.operation = "SELECT" }
# Kafka 메시지 추적
{ span.messaging.system = "kafka" && span.messaging.destination = "orders-topic" }
# 사용자별 요청 추적
{ span.user.id = "user-12345" }구조적 쿼리 (부모-자식 관계)
# 부모 span 다음에 오는 자식 span
{ resource.service.name = "api-gateway" } >> { resource.service.name = "user-service" }
# 자식 span이 오류인 트레이스
{ resource.service.name = "api-gateway" } >> { status = error }
# 특정 깊이의 span
{ resource.service.name = "frontend" } >> { } >> { span.db.system = "postgresql" }
# 형제 span 관계
{ resource.service.name = "api-gateway" }
>> ({ span.http.route = "/users" } || { span.http.route = "/orders" })트레이스 비교 (배포 전후)
# 특정 버전의 트레이스
{ resource.service.version = "v2.0.0" }
# 배포 환경별 비교
{ resource.deployment.environment = "canary" && duration > 1s }
# 특정 시간 범위의 트레이스 (Grafana UI에서 시간 선택기 사용)
{ resource.service.name = "api-gateway" }서비스 그래프 생성
Tempo는 트레이스 데이터로부터 서비스 그래프를 자동 생성할 수 있습니다.
# Tempo 설정에서 서비스 그래프 활성화
apiVersion: v1
kind: ConfigMap
metadata:
name: tempo-config
namespace: monitoring
data:
tempo.yaml: |
metrics_generator:
registry:
external_labels:
source: tempo
cluster: eks-production
storage:
path: /var/tempo/wal
remote_write:
- url: http://prometheus:9090/api/v1/write
traces_storage:
path: /var/tempo/traces
processor:
service_graphs:
enabled: true
histogram_buckets: [0.1, 0.2, 0.4, 0.8, 1.6, 3.2]
dimensions: [service.namespace]
span_metrics:
enabled: true
histogram_buckets: [0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]TraceQL 고급 패턴
# 느린 데이터베이스 쿼리를 포함한 트레이스
{ } >> { span.db.system != "" && span.duration > 100ms }
# 재시도가 발생한 트레이스 (동일 span.name이 여러 번)
{ } | count() > 1 by (span.name)
# 특정 사용자의 전체 요청 경로
{ span.user.id = "user-12345" } >> { }*
# 캐시 미스 패턴
{ span.cache.hit = false }
# 서킷 브레이커 트리거
{ span.circuitbreaker.state = "open" }Grafana 대시보드
Grafana에서 메트릭, 로그, 트레이스를 통합하여 시각화하고 상관 분석을 수행합니다.
RED 메소드 대시보드
{
"panels": [
{
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "sum(rate(http_requests_total[5m])) by (service)",
"legendFormat": "{{ service }}"
}
]
},
{
"title": "Error Rate",
"type": "graph",
"targets": [
{
"expr": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) by (service) / sum(rate(http_requests_total[5m])) by (service)",
"legendFormat": "{{ service }}"
}
]
},
{
"title": "Latency (P99)",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))",
"legendFormat": "{{ service }}"
}
]
}
]
}USE 메소드 (인프라)
USE(Utilization, Saturation, Errors) 메소드는 리소스 모니터링에 적합합니다.
{
"panels": [
{
"title": "CPU Utilization",
"type": "gauge",
"targets": [
{
"expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
"legendFormat": "CPU Usage %"
}
],
"fieldConfig": {
"defaults": {
"max": 100,
"thresholds": {
"steps": [
{ "value": 0, "color": "green" },
{ "value": 70, "color": "yellow" },
{ "value": 85, "color": "red" }
]
}
}
}
},
{
"title": "Memory Utilization",
"type": "gauge",
"targets": [
{
"expr": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100",
"legendFormat": "Memory Usage %"
}
]
},
{
"title": "Disk Saturation (I/O Wait)",
"type": "graph",
"targets": [
{
"expr": "rate(node_cpu_seconds_total{mode=\"iowait\"}[5m]) * 100",
"legendFormat": "{{ instance }}"
}
]
}
]
}크로스 데이터소스 링킹
Grafana에서 Prometheus, Loki, Tempo 간의 연결을 설정합니다.
Prometheus → Tempo (Exemplars)
# Grafana 데이터소스 설정
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
jsonData:
httpMethod: POST
exemplarTraceIdDestinations:
- name: trace_id
datasourceUid: tempo
urlDisplayLabel: View Trace
- name: Tempo
type: tempo
uid: tempo
url: http://tempo:3200
jsonData:
tracesToLogs:
datasourceUid: loki
tags: ['service.name', 'trace_id']
spanStartTimeShift: '-1h'
spanEndTimeShift: '1h'
filterByTraceID: true
filterBySpanID: trueLoki → Tempo (Derived Fields)
# Loki 데이터소스에서 Derived Fields 설정
datasources:
- name: Loki
type: loki
url: http://loki:3100
jsonData:
derivedFields:
- name: TraceID
matcherRegex: '"trace_id":"([a-f0-9]+)"'
url: 'http://grafana:3000/explore?orgId=1&left=["now-1h","now","Tempo",{"query":"${__value.raw}"}]'
datasourceUid: tempo대시보드 프로비저닝
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboards-config
namespace: monitoring
data:
dashboards.yaml: |
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: 'Kubernetes'
type: file
disableDeletion: false
updateIntervalSeconds: 30
options:
path: /var/lib/grafana/dashboards/default
- name: 'observability'
orgId: 1
folder: 'Observability'
type: file
disableDeletion: false
options:
path: /var/lib/grafana/dashboards/observability변수 템플릿
네임스페이스 및 서비스 필터링을 위한 대시보드 변수입니다.
{
"templating": {
"list": [
{
"name": "namespace",
"type": "query",
"datasource": "Prometheus",
"query": "label_values(kube_namespace_labels, namespace)",
"multi": true,
"includeAll": true
},
{
"name": "service",
"type": "query",
"datasource": "Prometheus",
"query": "label_values(http_requests_total{namespace=~\"$namespace\"}, service)",
"multi": true,
"includeAll": true
},
{
"name": "pod",
"type": "query",
"datasource": "Prometheus",
"query": "label_values(kube_pod_info{namespace=~\"$namespace\"}, pod)",
"multi": true,
"includeAll": true
}
]
}
}통합 관측성 대시보드 JSON
{
"title": "Service Observability",
"uid": "service-observability",
"tags": ["kubernetes", "observability"],
"timezone": "browser",
"schemaVersion": 30,
"panels": [
{
"id": 1,
"title": "Request Rate",
"type": "stat",
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 },
"targets": [
{
"datasource": "Prometheus",
"expr": "sum(rate(http_requests_total{namespace=~\"$namespace\", service=~\"$service\"}[5m]))"
}
],
"fieldConfig": {
"defaults": {
"unit": "reqps"
}
}
},
{
"id": 2,
"title": "Error Rate",
"type": "stat",
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 },
"targets": [
{
"datasource": "Prometheus",
"expr": "sum(rate(http_requests_total{namespace=~\"$namespace\", service=~\"$service\", status=~\"5..\"}[5m])) / sum(rate(http_requests_total{namespace=~\"$namespace\", service=~\"$service\"}[5m]))"
}
],
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"thresholds": {
"steps": [
{ "value": 0, "color": "green" },
{ "value": 0.01, "color": "yellow" },
{ "value": 0.05, "color": "red" }
]
}
}
}
},
{
"id": 3,
"title": "P99 Latency",
"type": "stat",
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
"targets": [
{
"datasource": "Prometheus",
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{namespace=~\"$namespace\", service=~\"$service\"}[5m])) by (le))"
}
],
"fieldConfig": {
"defaults": {
"unit": "s"
}
}
},
{
"id": 4,
"title": "Active Traces",
"type": "stat",
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 },
"targets": [
{
"datasource": "Tempo",
"queryType": "search",
"search": "{ resource.service.name = \"$service\" }"
}
]
},
{
"id": 5,
"title": "Request Rate Over Time",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 },
"targets": [
{
"datasource": "Prometheus",
"expr": "sum(rate(http_requests_total{namespace=~\"$namespace\", service=~\"$service\"}[5m])) by (service)",
"legendFormat": "{{ service }}"
}
],
"options": {
"legend": { "displayMode": "list" }
}
},
{
"id": 6,
"title": "Latency Distribution",
"type": "heatmap",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 },
"targets": [
{
"datasource": "Prometheus",
"expr": "sum(rate(http_request_duration_seconds_bucket{namespace=~\"$namespace\", service=~\"$service\"}[5m])) by (le)",
"legendFormat": "{{ le }}"
}
]
},
{
"id": 7,
"title": "Recent Error Logs",
"type": "logs",
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 12 },
"targets": [
{
"datasource": "Loki",
"expr": "{namespace=~\"$namespace\", app=~\"$service\"} |= \"error\" | json"
}
],
"options": {
"showTime": true,
"showLabels": false,
"wrapLogMessage": true
}
},
{
"id": 8,
"title": "Service Map",
"type": "nodeGraph",
"gridPos": { "h": 10, "w": 12, "x": 0, "y": 20 },
"targets": [
{
"datasource": "Tempo",
"queryType": "serviceMap"
}
]
},
{
"id": 9,
"title": "Slow Traces",
"type": "table",
"gridPos": { "h": 10, "w": 12, "x": 12, "y": 20 },
"targets": [
{
"datasource": "Tempo",
"queryType": "search",
"search": "{ resource.service.name = \"$service\" && duration > 1s }"
}
],
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {},
"indexByName": {
"traceID": 0,
"serviceName": 1,
"duration": 2,
"startTime": 3
}
}
}
]
}
]
}참고 자료
- Grafana Loki LogQL 문서
- Prometheus PromQL 문서
- Grafana Tempo TraceQL 문서
- OpenTelemetry SDK 문서
- Grafana Dashboard Best Practices
관련 문서
< 이전: 운영 알림 구성 | 목차 | 다음: 관측성 스택 운영 >