콘텐츠로 이동
Study Note관측

5. Grafana — 하나의 창

Grafana의 값은 그래프가 아니라 메트릭에서 로그로, 로그에서 트레이스로 두 번 클릭에 가는 것이다

이 장에서 처음 나오는 말1개
Grafana
메트릭·로그·트레이스를 저장하는 곳이 아니라, 세 저장소를 한 시간축에서 조회하고 서로 잇는 창이다.

문제 — 창이 세 개면 아무도 안 본다

섹션 제목: “문제 — 창이 세 개면 아무도 안 본다”

Prometheus UI, Loki 조회, Tempo — 각각 따로 열면 조사할 때마다 이런 일을 한다.

  1. 알림을 받는다. Prometheus UI를 열어 언제부터인지 확인한다 — 03:12

  2. 로그 화면으로 옮겨 시각을 다시 입력하고, 네임스페이스와 앱 이름을 다시 입력한다

  3. 느린 요청의 trace ID를 로그에서 복사해 Tempo 화면에 붙여 넣는다

  4. 그 사이 5분이 지났고, 원인은 아직이다

이 이동을 없애는 것이 Grafana를 쓰는 이유다. 대시보드가 예쁜 건 부수적이다.

Grafana Explore에서 두 질의를 좌우 분할 화면으로 열고 같은 시간 범위에서 비교하는 공식 예시 화면
Explore의 Split은 두 화면의 시간 범위를 묶는다. 이 덱에서는 왼쪽에 메트릭, 오른쪽에 로그를 두고 같은 순간을 비교한다.출처: Grafana 공식 문서 — Get started with Explore

데이터소스와 상관 관계 — 먼저 이것부터 설정한다

섹션 제목: “데이터소스와 상관 관계 — 먼저 이것부터 설정한다”
연결 설정에 필요한 말3개
데이터소스Data Source
Grafana가 붙는 저장소 하나. Prometheus · Loki · Tempo가 각각 하나이며 고유한 uid로 서로를 참조한다.
프로비저닝Provisioning
데이터소스·대시보드를 파일로 미리 넣어 두는 것. UI에서 손으로 만든 설정과 달리 Git으로 관리할 수 있다.
derived field파생 필드
로그에서 trace_id 같은 값을 뽑아 다른 데이터소스로 가는 링크로 만드는 기능.
Grafana에서 Prometheus·Loki·Tempo를 exemplar·derived field·trace to logs·span metrics로 서로 잇는 네 갈래 연결

프로비저닝 파일 하나로 셋을 다 넣는다. UI에서 손으로 만들지 않는다 — 아래 절 참고.

apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: prom # 다른 데이터소스가 이 uid로 참조한다
url: http://kube-prometheus-stack-prometheus.observability.svc:9090
isDefault: true
jsonData:
exemplarTraceIdDestinations:
- name: trace_id
datasourceUid: tempo # 메트릭 → 트레이스
- name: Loki
type: loki
uid: loki
url: http://loki-gateway.observability.svc
jsonData:
derivedFields:
- name: TraceID
matcherRegex: '"trace_id":"(\w+)"' # 로그 형식에 맞춘다
url: '$${__value.raw}'
datasourceUid: tempo # 로그 → 트레이스
- name: Tempo
type: tempo
uid: tempo
url: http://tempo.observability.svc:3200
jsonData:
tracesToLogsV2:
datasourceUid: loki # 트레이스 → 로그
tags: [{ key: 'service.name', value: 'app' }]
spanStartTimeShift: '-5m'
spanEndTimeShift: '5m'
serviceMap:
datasourceUid: prom # 서비스 그래프

tracesToLogsV2의 tags가 하는 일을 알아 두면 디버깅이 쉽다 — 트레이스의 service.name을 로그의 app 라벨에 대응시켜 로그 질의를 자동으로 만든다. 이름이 다르면 여기서 매핑해 주면 되고, 매핑이 틀리면 “트레이스에서 로그로 갔는데 비어 있다”가 된다.

Explore — 실제 조사는 여기서 한다

섹션 제목: “Explore — 실제 조사는 여기서 한다”
대시보드와 구분할 말1개
Explore탐색
저장된 대시보드가 아니라 질의를 그때그때 던지고 결과를 비교하는 화면. 사고 조사는 대부분 여기서 한다.

대시보드는 미리 알고 있던 질문에 답한다. 사고는 대개 몰랐던 질문으로 시작하고, 그때 여는 화면이 Explore다. 앞 장들이 쌓아 온 것이 여기서 한 줄기로 이어진다.

  1. 알림에서 시작 — severity=critical, orders 서비스 에러율. 알림의 runbook 링크에 이 조사를 여는 Explore 링크를 넣어 두면 여기서 시간이 는다

  2. 메트릭으로 범위를 좁힌다 (2장)

    sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))

    03:12부터, orders만. 여기서 시간 범위를 03:05~03:30으로 좁혀 둔다 — 이 범위가 다음 단계로 그대로 따라간다

  3. 분할 화면으로 로그를 연다 (Split 버튼) (3장)

    {namespace="prod", app="orders"} |= "error" | json | line_format "{{.msg}} {{.err}}"

    왼쪽 그래프와 오른쪽 로그가 같은 시간축을 쓴다. 그래프에서 드래그해 범위를 줄이면 로그도 같이 줄어든다 — 창을 따로 띄웠을 때와 가장 크게 갈리는 지점이다

  4. 로그 한 줄을 펼쳐 TraceID 링크를 누른다 — 3장에서 넣은 trace_id와 위에서 설정한 derived field가 만나는 자리다. 트레이스가 열린다

  5. 어느 스팬이 시간을 먹었는지 본다 (4장). 그 스팬에서 다시 Logs for this span 을 누르면 그 서비스의 그 시각 로그로 간다

대시보드 — 늘리지 않는 것이 설계다

섹션 제목: “대시보드 — 늘리지 않는 것이 설계다”
층무엇이 있나누가 언제
개요클러스터 전체 상태 한 화면 — 노드·핵심 서비스 RED·발화 중인 알림모니터에 띄워 두는 것. 하나만 만든다
층별노드 · DB · 게이트웨이 · 관측 스택 자신원인을 찾을 때 (USE 지표)
서비스별RED 한 벌 + 그 서비스 로그변수로 하나만 만든다 — 아래

서비스마다 대시보드를 만들지 않는다

섹션 제목: “서비스마다 대시보드를 만들지 않는다”
대시보드를 줄이는 장치1개
템플릿 변수Template Variable
대시보드 위쪽의 드롭다운. $service 값을 바꾸며 여러 서비스가 한 대시보드를 공유하게 한다.

서비스가 30개면 대시보드도 30개가 되는 게 가장 흔한 실패다. 템플릿 변수 하나면 1개로 끝난다.

변수 정의 (Dashboard settings → Variables)
name : service
type : Query
query : label_values(http_requests_total, service) # 실제 값에서 목록을 만든다
multi : true # 여러 개 동시 선택 허용
패널 쿼리에서
sum by (service) (rate(http_requests_total{service=~"$service"}[5m]))
└── 정규식 매칭이라 multi 선택이 그대로 통한다

같은 방식으로 $namespace · $cluster도 만든다. 변수는 로그 패널에도 그대로 쓰인다 — {namespace="$namespace", app="$service"} — 그래서 한 대시보드 안에서 메트릭과 로그가 같은 대상을 가리킨다.

무엇을 알고 싶나패널
시간에 따른 변화Time series — 기본값. 대부분 이것
지금 값 하나 (에러율 · 가동률)Stat — 크게 하나. 개요 대시보드 맨 위
상위 N개 목록 (시계열 많은 메트릭, 재시작 많은 파드)Table
지연 분포Heatmap — 히스토그램 버킷을 그대로 그린다. p99 선 하나보다 훨씬 많이 말한다
상태가 언제 바뀌었나 (Ready → NotReady)State timeline
로그Logs — 대시보드 안에 그대로 넣을 수 있다
서비스 호출 관계Node graph — Tempo의 서비스 그래프 (4장)

UI에서 만든 대시보드는 파드가 새로 뜨거나 DB를 옮기면 사라진다. 그리고 누가 언제 무엇을 바꿨는지 알 수 없다.

apiVersion: v1
kind: ConfigMap
metadata:
name: dashboard-platform-overview
namespace: observability
labels:
grafana_dashboard: "1" # sidecar가 이 라벨을 보고 집어 간다
data:
platform-overview.json: |
{ "title": "Platform Overview", "panels": [ ... ] }

가장 단순하다. Argo CD로 관리하면 대시보드가 Git에서 리뷰를 거쳐 들어온다 (온프렘 덱 9장).

만드는 방법은 UI에서 만들고 JSON을 내보내는 것이 현실적이다. JSON을 손으로 쓰지 않는다 — UI에서 맞춘 뒤 Export → Save to file로 꺼내 ConfigMap에 넣고, 다음부터는 그 파일이 원본이다.

배포 — Grafana 자신의 상태는 어디에 두나

섹션 제목: “배포 — Grafana 자신의 상태는 어디에 두나”

Grafana는 대시보드·사용자·알림 규칙을 자기 DB에 저장한다. 기본값은 SQLite다.

저장소결과
SQLite (기본)replica를 2개로 못 늘린다. 파드가 옮겨 다니면 PVC에 묶인다
PostgreSQL (온프렘 덱 7장)HA 가능. 백업이 CNPG 체계에 들어온다
# Helm values 발췌
grafana.ini:
database:
type: postgres
host: platform-pg-rw.database.svc:5432 # CNPG의 쓰기 서비스 (온프렘 덱 7장)
name: grafana
user: grafana
ssl_mode: require
replicas: 2
persistence:
enabled: false # DB로 옮겼으면 PVC는 필요 없다

온프렘 덱 5장의 패턴 ①이다. Grafana는 OIDC를 내장 지원하고, 그룹으로 역할까지 매핑된다.

[auth.generic_oauth]
enabled = true
name = Keycloak
client_id = grafana
client_secret = $__file{/etc/secrets/oauth_secret}
scopes = openid profile email groups
auth_url = https://sso.example.internal/realms/corp/protocol/openid-connect/auth
token_url = https://sso.example.internal/realms/corp/protocol/openid-connect/token
api_url = https://sso.example.internal/realms/corp/protocol/openid-connect/userinfo
# 그룹 → Grafana 역할
role_attribute_path = contains(groups[*], 'platform-admins') && 'Admin' || contains(groups[*], 'platform-editors') && 'Editor' || 'Viewer'
role_attribute_strict = true
[auth.anonymous]
enabled = false # 기본으로 꺼 둔다
[users]
auto_assign_org_role = Viewer
확인할 것왜
groups claim이 실제로 오는가안 오면 전원이 Viewer가 된다 → Keycloak Scope와 Mapper
사내 CA를 Grafana가 믿는가Keycloak이 사내 인증서면 토큰 교환이 실패한다 (온프렘 덱 4장)
root_url이 실제 주소와 같은가리다이렉트 URI 불일치의 단골 원인
로컬 admin이 남아 있는가깨진 유리 계정 (온프렘 덱 5장)

알림을 어디에 두나 — Grafana vs Alertmanager

섹션 제목: “알림을 어디에 두나 — Grafana vs Alertmanager”
여기서 비교하는 기능1개
Unified Alerting통합 알림
Grafana가 직접 알림 규칙을 평가하고 보내는 기능. Alertmanager와 역할이 겹치므로 한쪽을 주인으로 정한다.

둘 다 알림을 보낼 수 있어서 섞으면 이중 발송이나 사각지대가 생긴다. 하나로 정한다.

Alertmanager 중심 (권장)Grafana Unified Alerting 중심
규칙이 사는 곳PrometheusRule CRD — Git으로 관리Grafana DB (프로비저닝하면 파일로도)
여러 데이터소스Prometheus 메트릭만Loki·SQL 등 여러 소스에 걸쳐 규칙을 짤 수 있다
Grafana가 죽으면알림은 계속 간다알림도 멈춘다
언제인프라 알림 전부로그 기반 알림처럼 Prometheus로 못 짜는 것만
터미널 창
# ① 데이터소스가 다 붙었나 (Grafana UI: Connections → Data sources → Test)
kubectl -n observability port-forward svc/grafana 3000:80
curl -s -u admin:<pw> http://localhost:3000/api/datasources | jq '.[] | {name, uid, type}'
# ② 상관 링크가 실제로 뜨나 — 눈으로 확인해야 하는 항목
# Explore → Loki에서 trace_id 있는 로그 한 줄 → 우측에 TraceID 링크 버튼이 보이는가
# 그 버튼을 눌러 Tempo로 넘어가는가
# Tempo 스팬에서 "Logs for this span"이 비어 있지 않은가
# ③ 대시보드가 프로비저닝됐나
curl -s -u admin:<pw> "http://localhost:3000/api/search?type=dash-db" | jq '.[].title'
# ④ SSO 로그인 흐름 (브라우저에서)
# /login → Keycloak → 돌아온 뒤 우상단 프로필의 Role이 기대한 값인가
kubectl -n observability logs deploy/grafana | grep -i 'oauth\|role'
증상흔한 원인확인
데이터소스 Test 실패서비스 주소 오타 · NetworkPolicy파드에서 직접 curl
로그에 트레이스 링크가 없음matcherRegex가 실제 로그 형식과 불일치로그 한 줄을 보고 정규식 수정
링크는 뜨는데 트레이스가 없음그 요청이 샘플링에서 빠졌다4장 tail 정책
트레이스 → 로그가 비어 있음tracesToLogsV2의 태그 매핑 불일치service.name ↔ 로그 라벨 이름 확인
exemplar가 안 보임앱이 exemplar를 안 내보냄 · Prometheus 설정 미비/metrics에 # {trace_id=...} 주석이 있나
변수 드롭다운이 비어 있음label_values() 대상 메트릭이 없음Explore에서 그 메트릭을 직접 질의
전원이 Viewergroups claim 미도착ID 토큰 디코드 (온프렘 덱 5장)
대시보드가 사라짐UI에서 만들었고 파드가 재생성됨프로비저닝으로 옮긴다
알림이 두 번 옴Grafana와 Alertmanager 양쪽에 규칙한쪽으로 통일
  • Grafana의 값은 대시보드가 아니라 세 신호 사이의 이동을 없애는 것이다
  • derived field · exemplar · trace to logs 셋을 먼저 설정한다. 대시보드보다 먼저다
  • 조사는 Explore의 분할 화면에서 한다 — 메트릭과 로그가 같은 시간축을 쓰는 것이 핵심이고, 끝나면 링크를 사고 기록에 남긴다
  • 대시보드는 계층 셋 + 템플릿 변수로 개수를 억누른다. 서비스마다 만들지 말고 $service 하나로 돌려 쓴다
  • 대시보드는 코드로(ConfigMap 또는 Operator). UI에서 만들어 Export하는 것이 현실적이다
  • Grafana 자신의 DB는 PostgreSQL(CNPG) 로 — SQLite면 HA가 안 된다. 대신 순환 의존을 만들지 않도록 깨진 유리 경로를 남긴다
  • SSO는 그룹 → 역할 매핑까지. groups claim이 안 오면 전원이 Viewer가 된다
  • 인프라 알림은 Alertmanager에. Grafana가 죽어도 알림은 가야 한다