P99 레이턴시·에러율 SLO 대시보드 구축하기 — Grafana Loki LogQL 실전 가이드
APM 도구 도입 비용이나 코드 수정 없이, 이미 쌓여 있는 로그만으로 P99 레이턴시와 에러율을 측정하고 SLO 대시보드를 구성할 수 있습니다. Grafana Loki는 LogQL이라는 쿼리 언어를 통해 로그를 메트릭으로 변환하는 기능을 제공하며, 그 핵심에는 unwrap 연산자와 avg_over_time, quantile_over_time 함수가 있습니다.
이 글은 Grafana와 Loki(2.3 이상)를 운영해본 경험이 있고, APM 도입 전에 빠르게 SLO 체계를 갖추고 싶은 개발자·SRE를 위한 가이드입니다. JSON 또는 logfmt 형식의 구조화된 로그가 이미 Loki에 수집되고 있다면, 5분 이내에 첫 번째 P99 쿼리를 실행해볼 수 있습니다.
이 글에서는 HTTP API 레이턴시 SLO부터 AWS ALB 로그 기반 Recording Rules, Grafana 대시보드 패널 구성까지 — 로그에서 신뢰할 수 있는 SLI를 추출하고 시각화하는 전체 흐름을 단계별로 살펴봅니다.
목차
개념 이해
SLO 구성 3요소
Loki 기반 SLO 대시보드를 구성하기 전에 핵심 개념 세 가지를 간략히 짚고 넘어가는 것이 좋습니다.
| 구성요소 | 설명 | 예시 |
|---|---|---|
| SLI (Service Level Indicator) | 서비스 품질을 나타내는 측정 지표 | P99 응답시간, 에러율 |
| SLO (Service Level Objective) | SLI에 대한 목표 수치 | P99 < 500ms, 에러율 < 1% |
| Error Budget | SLO를 지키면서 허용되는 장애 여유분 | 30일 기준 43.2분 (99.9% SLO 기준) |
Loki를 사용하면 기존 로그에서 SLI를 직접 추출할 수 있어, 별도의 메트릭 계측(instrumentation) 코드를 추가할 필요가 없습니다.
unwrap — 로그 필드를 메트릭으로 변환하는 핵심 연산자
unwrap은 파싱된 로그 필드에서 숫자 값을 꺼내 메트릭 함수에 전달하는 연산자입니다. 반드시 | json, | logfmt, | regexp 등의 파서 뒤에 위치해야 합니다.
{app="api-server", env="production"}
| json
| unwrap response_time_ms [5m]
unwrap이란? 로그 라인에서 파싱된 숫자 필드를 추출해avg_over_time,quantile_over_time같은 범위 집계 함수에 전달하는 LogQL 연산자입니다.unwrap없이는 로그를 수치형 메트릭으로 다룰 수 없습니다.
로그 형식에 따라 아래와 같이 파서를 선택할 수 있습니다.
# JSON 파서
{app="api"} | json | unwrap response_time_ms
# Logfmt 파서
{app="api"} | logfmt | unwrap latency
# 정규식 파서
{app="api"} | regexp `(?P<duration>\d+)ms` | unwrap duration
# Pattern 파서 (Nginx 액세스 로그 예시)
{app="nginx"} | pattern `<_> - - <_> "<method> <_> <_>" <status> <bytes>` | unwrap bytesavg_over_time과 quantile_over_time
두 함수는 unwrap으로 추출한 값들을 지정한 시간 범위 기준으로 집계합니다.
# 평균 응답시간
avg_over_time(
{app="api-server", env="production"}
| json
| unwrap response_time_ms [5m]
) by (service)
# P95 레이턴시
quantile_over_time(0.95,
{app="api-server", env="production"}
| json
| unwrap response_time_ms [5m]
) by (service)
# P99 레이턴시 + 파싱 에러 제거 (프로덕션 권장 패턴)
quantile_over_time(0.99,
{container="ingress-nginx"}
| json
| unwrap response_latency_seconds
| __error__="" [1m]
) by (cluster)| 함수 | 용도 | φ 값 예시 |
|---|---|---|
avg_over_time(...) |
평균값 계산 (평균 응답시간, 처리량) | — |
quantile_over_time(φ, ...) |
φ-분위수 계산 | P50=0.5, P95=0.95, P99=0.99 |
__error__=""unwrap과정에서 숫자로 변환할 수 없는 필드가 있으면 파싱 오류 로그가 발생합니다.| __error__=""필터를 추가하면 이 오류 로그를 제거해 SLI 수치의 정확도를 높일 수 있습니다. 프로덕션 환경에서는 포함하는 것을 권장합니다.
quantile_over_time은 정확한(exact) 분위수를 계산하는 방식으로 동작합니다. 단, 트래픽이 적은 엔드포인트에서는 샘플 수 자체가 부족해 통계적 신뢰도가 낮아질 수 있으므로, SLO 임계값을 설정할 때는 충분한 샘플이 확보되는지 먼저 확인해보시면 좋습니다.
집계 윈도우 선택 기준
[5m]처럼 시간 범위 윈도우를 선택할 때는 서비스 특성에 맞는 조정이 필요합니다.
| 윈도우 | 적합한 상황 | 주의사항 |
|---|---|---|
[1m] 이하 |
트래픽이 많은 고빈도 API | 로그 수가 적은 엔드포인트는 분위수 신뢰도 저하 |
[5m] |
대부분의 프로덕션 서비스 | 레이턴시 급등 감지와 통계적 정확도 간 균형이 좋음 |
[15m] 이상 |
트래픽이 적은 배치성 서비스 | 레이턴시 급등을 빠르게 감지하기 어려움 |
실무에서 [5m]이 기본값으로 많이 쓰이는 이유는, 1분 이하 윈도우는 로그 수가 적어 분위수 정확도가 떨어지고, 15분 이상 윈도우는 레이턴시 급등 감지가 늦어지기 때문입니다. 자신의 서비스 트래픽 특성에 맞게 조정해보시면 좋습니다.
이제 이 개념들을 실제 SLO 시나리오에 적용해보겠습니다.
실전 적용
예시 1: HTTP API 레이턴시 SLO
시나리오: P99 응답시간 500ms 이하, 30일 기준 99.9% 준수
-- 레이블 이름({app="order-api"}, 필드명 duration_ms 등)은 실제 환경에 맞게 교체해주세요.
-- SLI: P99 레이턴시
quantile_over_time(0.99,
{namespace="production", app="order-api"}
| json
| unwrap duration_ms
| __error__="" [5m]
) by (endpoint)
-- SLI: P95 레이턴시
quantile_over_time(0.95,
{namespace="production", app="order-api"}
| json
| unwrap duration_ms
| __error__="" [5m]
) by (endpoint)
-- SLI: 평균 레이턴시
avg_over_time(
{namespace="production", app="order-api"}
| json
| unwrap duration_ms
| __error__="" [5m]
) by (endpoint)| 쿼리 구성 요소 | 역할 |
|---|---|
{namespace="production", app="order-api"} |
프로덕션 환경의 특정 앱 로그 선택 |
| json |
JSON 형식으로 로그 파싱 |
| unwrap duration_ms |
duration_ms 필드를 수치형으로 추출 |
| __error__="" |
파싱 오류 로그 제거 |
[5m] |
5분 윈도우로 집계 |
by (endpoint) |
엔드포인트별로 분리 |
예시 2: 에러율 SLO
에러율은 unwrap 없이도 rate() 함수와 레이블 필터 조합으로 계산할 수 있습니다.
-- Loki 2.3 이상에서 JSON 파싱 후 숫자형 레이블 비교(>= 500)가 지원됩니다.
-- 구버전 환경이라면 | status_code !~ "1..|2..|3..|4.." 형태로 대체할 수 있습니다.
(
sum(rate(
{app="order-api"} | json | status_code >= 500 [5m]
)) by (service)
/
clamp_min(
sum(rate({app="order-api"} | json [5m])) by (service),
0.001
)
) * 100분모가 0일 때의 처리 트래픽이 없는 구간에서는 분모가 0이 되어
NaN또는 무한대 값이 발생할 수 있습니다.clamp_min(..., 0.001)으로 분모의 최솟값을 지정하거나,or vector(0)을 사용해 기본값을 설정하는 것을 권장합니다. 이 처리가 없으면 알림이 오작동할 수 있습니다.
예시 3: AWS ALB 로그 기반 Recording Rules
대시보드에서 매번 고비용 쿼리를 실행하는 대신, Recording Rules로 결과를 미리 계산해 시계열 메트릭으로 저장해두면 조회 성능을 크게 향상시킬 수 있습니다.
# loki-rules.yaml
groups:
- name: alb_slo_rules
interval: 1m # Recording Rule 실행 주기
rules:
- record: job:alb_request_duration_seconds:p99
expr: |
quantile_over_time(0.99,
{job="alb-logs"}
| logfmt
| unwrap target_processing_time
| __error__="" [5m]
) by (target_group)
- record: job:alb_error_rate:ratio
expr: |
sum(rate({job="alb-logs"} | logfmt | elb_status_code >= 500 [5m]))
/
clamp_min(
sum(rate({job="alb-logs"} | logfmt [5m])),
0.001
)Recording Rules란? 자주 실행되는 고비용 LogQL 쿼리를 미리 계산해 새로운 시계열 메트릭으로 저장하는 기능입니다. Prometheus의 Recording Rules와 동일한 개념으로, 대시보드에서 이미 계산된 메트릭을 조회하므로 쿼리 부하를 크게 줄일 수 있습니다.
interval과 범위 윈도우의 관계interval: 1m으로 설정하면 규칙이 1분마다 실행되는데, 쿼리 윈도우가[5m]이면 연속된 실행 결과 간에 4분치 데이터가 중복 참조됩니다. 이는 의도된 동작이지만, 집계 방식에 따라 이중 계산이 발생할 수 있으므로 주의가 필요합니다. 중복 없이 운영하려면interval: 5m으로 윈도우 크기와 동일하게 맞추는 방법도 있습니다.
예시 4: Grafana 대시보드 패널 구성
SLO 대시보드는 다음 네 가지 패널로 구성하는 것을 권장합니다.
┌────────────────────────────────────────────┐
│ SLO Status Panel │
│ - 현재 SLI 값 (Stat Panel) │
│ - SLO 목표 대비 준수율 (Gauge Panel) │
├────────────────────────────────────────────┤
│ Latency Trend │
│ - P50 / P95 / P99 Time Series │
│ - avg_over_time 평균값 오버레이 │
├────────────────────────────────────────────┤
│ Error Budget │
│ - 남은 에러 버짓 (%) — Stat Panel │
│ - 번-레이트 시계열 — Time Series │
├────────────────────────────────────────────┤
│ Burn Rate Alert Status │
│ - Fast-burn (1h/6h 윈도우) │
│ - Slow-burn (24h/72h 윈도우) │
└────────────────────────────────────────────┘각 패널의 데이터 소스는 예시 3에서 Recording Rules로 저장한 job:alb_request_duration_seconds:p99, job:alb_error_rate:ratio 메트릭을 참조하면 됩니다.
Burn Rate는 현재 에러 소비 속도를 SLO 허용치와 비교한 배율입니다. 예를 들어 99.9% SLO에서 Burn Rate 1은 에러 버짓을 정확히 30일에 소진하는 속도이고, Burn Rate 10은 3일 만에 소진하는 속도입니다. Google SRE 워크북의 멀티-윈도우 번-레이트 패턴은 짧은 윈도우(1h/6h)로 급격한 장애를, 긴 윈도우(24h/72h)로 서서히 진행되는 서비스 저하를 감지하는 이중 알림 전략입니다.
운영 고려사항
장점
| 항목 | 내용 |
|---|---|
| 코드 변경 없음 | 기존 로그에서 SLI를 추출할 수 있어 앱 instrumentation이 불필요합니다 |
| 비용 효율 | Prometheus나 별도 메트릭 저장소 없이 Loki 단독으로 운용 가능합니다 |
| 유연한 필드 추출 | json, logfmt, regexp, pattern 파서로 다양한 로그 형식을 지원합니다 |
| 히스토리컬 분석 | 과거 로그를 재쿼리해 소급으로 SLI를 계산할 수 있습니다 |
| 세분화 SLO | 서비스, 엔드포인트, 리전별 레이블 기반 세분화가 용이합니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 성능 오버헤드 | quantile_over_time은 메모리 집약적 연산으로 로그 볼륨이 클 때 타임아웃이 발생할 수 있습니다 |
Recording Rules로 사전 계산을 적용합니다 |
| 확장성 제약 | 정확한 분위수를 계산하므로 시간 범위가 넓어질수록 메모리 사용량이 선형 증가합니다 | 짧은 집계 윈도우(1m~5m)를 사용하고 Recording Rules로 저장합니다 |
| 희박한 데이터 정확도 | 트래픽이 적은 엔드포인트에서는 샘플 수 부족으로 분위수 신뢰도가 낮아질 수 있습니다 | service 수준으로 레이블 집계 단위를 조정하는 것을 권장합니다 |
| 단일 필드 제한 | 하나의 unwrap 표현식에서 하나의 필드만 추출 가능합니다 |
여러 메트릭이 필요할 경우 별도 쿼리를 사용합니다 |
| 알려진 버그 | 외부 집계함수와 조합(avg by (...) (quantile_over_time(...))) 시 특정 Loki 버전에서 오류가 발생합니다 (Issue #13793) |
Loki 버전을 확인하고 외부 집계를 피합니다 |
| 로그 구조 의존성 | SLI 품질이 로그 포맷의 일관성에 완전히 의존합니다 | 로그 포맷 변경 시 쿼리도 함께 업데이트하는 것이 필요합니다 |
카디널리티(Cardinality)란? 레이블의 고유 값 수를 말합니다.
by (endpoint)처럼 세분화할수록 시계열 수가 폭발적으로 늘어납니다. 예를 들어 엔드포인트가 200개이면 Recording Rule 하나가 200개의 시계열을 생성하며, 이는 Loki와 Grafana의 메모리·인덱스 비용을 직접 증가시킵니다. 엔드포인트 수가 많다면 집계 레이블 수를 줄이거나 레이블 값을 정규화하는 것을 권장합니다.
실무에서 가장 흔한 실수
__error__=""필터를 생략하는 경우 — unwrap 파싱 오류 로그가 SLI 수치에 포함되어 결과가 왜곡됩니다. 특히 일부 로그에 해당 필드가 없거나 문자열이 섞인 경우 수치가 크게 튀어 오를 수 있습니다.- 에러율 쿼리에서 분모 0 처리를 생략하는 경우 — 트래픽이 없는 새벽 시간대에
NaN또는+Inf가 발생해 알림이 오작동하거나 대시보드에 공백이 생깁니다.clamp_min또는or vector(0)으로 처리해두면 이 문제를 방지할 수 있습니다. - Recording Rules의
interval과 범위 윈도우 크기의 관계를 고려하지 않는 경우 —interval: 1m에[5m]윈도우를 함께 쓰면 데이터 중복 참조가 발생합니다. 의도한 집계 방식인지 먼저 확인하고, 중복 없이 운영하려면 interval과 윈도우 크기를 일치시키는 방법도 고려해보시면 좋습니다.
마치며
Grafana Loki의 unwrap, avg_over_time, quantile_over_time을 조합하면, 기존 프로덕션 로그에서 코드 변경 없이 신뢰할 수 있는 SLO 대시보드를 구성할 수 있습니다.
지금 바로 시작해볼 수 있는 3단계를 소개합니다.
- 기존 로그에서 숫자 필드를 확인합니다. Grafana Explore에서
{app="your-app-name"} | json을 실행해response_time_ms,duration,latency같은 파싱 가능한 숫자 필드가 있는지 먼저 확인해보시면 좋습니다. - 예시 1의 P99 쿼리를 자신의 앱에 맞게 수정해 실행해보세요.
app,namespace,unwrap대상 필드명을 교체하는 것만으로 첫 번째 SLI 값을 확인할 수 있습니다. - 검증된 쿼리를 Recording Rules로 등록합니다.
loki-rules.yaml에record: job:your_api_p99_latency_ms형태로 저장하고, 이 메트릭을 Grafana 대시보드의 Time Series 패널과 Gauge 패널에 연결해보시면 됩니다.
로그는 이미 당신의 인프라에 있습니다 — 지금부터는 그 로그가 SLO 계기판이 됩니다.
Grafana SLO 플러그인(grafana-slo-app)과 Alertmanager를 연동해 Fast-burn/Slow-burn 이중 알림 전략을 자동화하는 방법은 다음 글에서 이어서 다룰 예정입니다.
참고 자료
- Metric queries | Grafana Loki documentation
- Alerting and recording rules | Grafana Loki documentation
- How to use LogQL range aggregations in Loki | Grafana Labs
- Best practices for Grafana SLOs | Grafana Cloud documentation
- Introduction to Grafana SLO | Grafana Cloud documentation
- Create Grafana Loki SLO | Nobl9 Documentation
- Grafana Loki: LogQL and Recording Rules for metrics from AWS Load Balancer logs | ITNEXT
- Loki Community Call July 2025: How to generate metrics from logs | Grafana Community
- A Comprehensive Guide to Log Query Language (LogQL) | DEV Community
- Avg with quantile_over_time throws an error · Issue #13793 · grafana/loki
- How to Build SLO Dashboards with Loki Logs | OneUptime