Grafana Loki 완벽 가이드 — 싸고 빠른 로그 수집·저장, ELK 대체의 실전 선택지

Grafana Loki 완벽 가이드 — 싸고 빠른 로그 수집·저장, ELK 대체의 실전 선택지

이 글의 핵심

Grafana Loki는 "로그 본문은 인덱싱하지 않고 라벨만 인덱싱"하는 설계로 Elasticsearch 대비 저장 비용을 극적으로 낮추는 로그 집약 시스템입니다. Prometheus의 설계 철학을 로그에 적용해 Kubernetes 환경에서 사실상 표준이 되어 있고 S3·GCS를 기본 저장소로 쓸 수 있습니다. 이 글은 아키텍처·설치·Promtail/Alloy 수집·LogQL·알림·프로덕션 운영을 실전 중심으로 다룹니다.

Loki가 해결하는 문제

Elasticsearch 기반 로그 스택(ELK)의 전형적 문제:

  • 모든 로그 본문을 인덱싱 → 저장 비용 폭증
  • JVM 운영 부담
  • 스케일 아웃 복잡성
  • 장기 보존을 위한 별도 아키텍처 필요

Loki의 접근:

  • 라벨만 인덱싱 → 인덱스 크기가 전체 로그의 1% 미만
  • 본문은 gzip/zstd 압축 후 object storage에 저장
  • Prometheus와 동일한 라벨 모델 → 운영 개념 통일
  • Stateless 서비스 + object storage로 확장이 단순

아키텍처 (SimpleScalable)

로그 생성                 에이전트(수집)            Loki 분산 서비스              저장
─────────                ───────────              ─────────────────          ────────────
Pod stdout ─► Alloy/Promtail ─► Distributor ─► Ingester ─► S3/GCS/Azure Blob
                                               │  WAL       ▲
                                               ▼            │
                                          Compactor    Query Frontend


                                                       Querier ──► Grafana

핵심 서비스:

  • Distributor: 수신·검증·샤딩
  • Ingester: 메모리 버퍼링 + 주기적 chunk flush
  • Querier: 쿼리 실행
  • Query Frontend: 쿼리 분할·캐시
  • Compactor: 인덱스 최적화·보존 정책

설치

Docker (1노드 개발)

docker run -d --name loki -p 3100:3100 grafana/loki:3.2.0
docker run -d --name grafana -p 3000:3000 \
  -e GF_SECURITY_ADMIN_PASSWORD=admin grafana/grafana:latest

# Grafana에서 Loki datasource URL: http://host.docker.internal:3100

docker-compose (개발)

version: "3.9"
services:
  loki:
    image: grafana/loki:3.2.0
    ports: ["3100:3100"]
    command: -config.file=/etc/loki/config.yaml
    volumes:
      - ./loki-config.yaml:/etc/loki/config.yaml

  alloy:
    image: grafana/alloy:latest
    volumes:
      - /var/log:/var/log:ro
      - ./alloy-config.alloy:/etc/alloy/config.alloy
      - /var/run/docker.sock:/var/run/docker.sock
    command: run /etc/alloy/config.alloy
    depends_on: [loki]

  grafana:
    image: grafana/grafana:latest
    ports: ["3000:3000"]
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

Kubernetes (Helm, 프로덕션)

helm repo add grafana https://grafana.github.io/helm-charts
helm upgrade --install loki grafana/loki \
  --namespace loki --create-namespace \
  --values - <<EOF
deploymentMode: SimpleScalable
loki:
  auth_enabled: false
  schemaConfig:
    configs:
      - from: "2024-01-01"
        store: tsdb
        object_store: s3
        schema: v13
        index: { prefix: loki_index_, period: 24h }
  storage:
    type: s3
    bucketNames:
      chunks: my-loki-chunks
      ruler: my-loki-ruler
    s3:
      region: ap-northeast-2
      # IRSA 사용
read:
  replicas: 3
write:
  replicas: 3
backend:
  replicas: 3
EOF

S3 + IRSA(IAM Roles for Service Accounts) 조합이 AWS에서 표준 배포.

Grafana Alloy로 Kubernetes 로그 수집

// alloy-config.alloy
discovery.kubernetes "pods" {
  role = "pod"
}

discovery.relabel "pods" {
  targets = discovery.kubernetes.pods.targets
  rule {
    source_labels = ["__meta_kubernetes_namespace"]
    target_label  = "namespace"
  }
  rule {
    source_labels = ["__meta_kubernetes_pod_name"]
    target_label  = "pod"
  }
  rule {
    source_labels = ["__meta_kubernetes_pod_label_app"]
    target_label  = "app"
  }
}

loki.source.kubernetes "pods" {
  targets    = discovery.relabel.pods.output
  forward_to = [loki.process.default.receiver]
}

loki.process "default" {
  forward_to = [loki.write.default.receiver]
  stage.json {
    expressions = { level = "level", msg = "message" }
  }
  stage.labels {
    values = { level = "level" }
  }
}

loki.write "default" {
  endpoint {
    url = "http://loki.loki.svc.cluster.local:3100/loki/api/v1/push"
  }
}

DaemonSet으로 배포하면 모든 Pod의 stdout을 자동 수집하고 Kubernetes 메타데이터를 라벨로 부여합니다.

라벨 설계: Loki의 가장 중요한 선택

좋은 라벨: 카디널리티 낮고(유한 값 집합) 자주 쿼리되는 차원

  • namespace, app, container, level, env, cluster

나쁜 라벨: 카디널리티 폭발의 원인

  • user_id, trace_id, request_id, ip — 로그 본문에 남기고 라벨은 X

너무 많은 라벨 조합(10만+ 스트림)은 Loki 성능을 크게 떨어뜨립니다.

LogQL: Loki 쿼리 언어

PromQL과 유사한 문법 + 로그 필터링.

# 기본
{namespace="prod", app="web"}

# 본문 필터 (line filter)
{namespace="prod", app="web"} |= "error"
{namespace="prod"} != "health"
{namespace="prod"} |~ "timeout|failed"

# JSON 파싱 후 레벨 필터
{app="api"}
  | json
  | level = "error"
  | status >= 500

# 속도(로그 라인 수)
rate({app="api"} |= "error" [5m])

# 집계
sum by (app) (rate({namespace="prod"} |= "error" [5m]))

# 특정 엔드포인트 지연 분포
{app="api"} | json | path = "/api/v1/users"
  | unwrap duration_ms | quantile_over_time(0.99, [5m])

# 특정 trace_id로 연결 로그 찾기
{namespace="prod"} |= "abc123def456"

# 정규식 캡처
{app="nginx"} | regexp `(?P<status>\d{3}) (?P<bytes>\d+)` | status = "500"

Grafana 통합

  1. Grafana → Connections → Data sources → Loki
  2. URL: http://loki:3100
  3. Explore에서 LogQL 쿼리
  4. 대시보드에서 로그 패널·메트릭 패널 혼합
  5. Traces ↔ Logs: Tempo와 연동해 trace_id로 로그 점프

알림 (Ruler)

groups:
  - name: errors
    rules:
      - alert: HighErrorRate
        expr: sum(rate({app="api"} |= "error" [5m])) by (app) > 10
        for: 5m
        labels: { severity: page }
        annotations:
          summary: "High error rate in {{ $labels.app }}"

      - alert: NoLogsFromWeb
        expr: absent(rate({app="web"}[5m]))
        for: 10m
        labels: { severity: warn }

Ruler가 LogQL 쿼리를 주기 평가하고 Alertmanager로 전송합니다. Prometheus와 같은 alert 인프라를 로그에 재사용 가능.

애플리케이션 연동

OpenTelemetry (앱에서 직접 OTLP 전송)

# Python
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
import logging

provider = LoggerProvider()
provider.add_log_record_processor(BatchLogRecordProcessor(
    OTLPLogExporter(endpoint="http://alloy:4318/v1/logs")
))
set_logger_provider(provider)

handler = LoggingHandler(logger_provider=provider)
logging.basicConfig(level=logging.INFO, handlers=[handler])

logging.info("hello from otel", extra={"user_id": 42})

구조화 로그 (JSON)

import pino from "pino"

const logger = pino({
  level: "info",
  messageKey: "message",
  timestamp: pino.stdTimeFunctions.isoTime,
})

logger.info({ userId: 42, traceId }, "User logged in")
// {"level":30,"time":"...","userId":42,"traceId":"...","message":"User logged in"}

Alloy의 stage.json이 이를 파싱해 level·traceId 등을 필드로 인식합니다.

보존 정책·비용 관리

limits_config:
  retention_period: 30d       # 기본 보존
  retention_stream:
    - selector: '{namespace="prod", app="critical"}'
      priority: 1
      period: 90d
    - selector: '{namespace="dev"}'
      priority: 2
      period: 7d

compactor:
  working_directory: /var/loki/compactor
  retention_enabled: true
  retention_delete_delay: 2h

스트림별 차등 보존으로 비용·컴플라이언스 균형.

성능 튜닝

  • Chunk 크기: chunk_target_size: 1572864 (1.5MB) 권장
  • Shard: 고트래픽 라벨은 split_queries_by_interval로 병렬 쿼리
  • 캐시: Memcached/Redis로 쿼리 결과 캐시
  • Parallel queries: max_query_parallelism: 32
  • Ingester WAL: 디스크 성능이 쓰기 지연을 좌우

Kubernetes 내 관측 권장 구성

Grafana Alloy (DaemonSet)
  ├─ 로그 → Loki
  ├─ 메트릭 → Prometheus (또는 Mimir)
  ├─ 트레이스 → Tempo
  └─ 프로파일 → Pyroscope

Grafana
  ├─ Dashboards
  ├─ Explore (모든 시그널 하나의 UI)
  └─ Alerting

이 스택을 LGTM(Loki, Grafana, Tempo, Mimir) 또는 Grafana Phlare/Pyroscope까지 포함해 LGTM+P로 부릅니다.

트러블슈팅

too many streams

라벨 카디널리티 폭발. user_id 같은 고유 ID를 라벨로 쓰지 말 것. Grafana의 cardinality dashboard로 원인 라벨 식별.

쿼리가 느림

  • 시간 범위 좁히기
  • 라벨로 먼저 필터
  • {app="web"} |= "error"{app="web"} | json | msg =~ "error" 보다 훨씬 빠름
  • Query Frontend 샤딩·캐시 확인

S3 비용 증가

  • 작은 chunk 다량 → compactor가 주기적으로 통합. 설정 점검
  • Lifecycle 정책으로 오래된 객체를 Glacier로

Ingester OOM

  • 메모리 대비 들어오는 스트림 많음 → 샤드 증설
  • chunk_idle_period 줄여 자주 flush

로그 유실

  • Distributor의 rate limit 도달 → ingestion_rate_mb 상향
  • Alloy의 retry·buffer 설정 확인

체크리스트

  • 에이전트로 Alloy 채택 (OTel 호환)
  • 라벨 카디널리티 < 수천 스트림 유지
  • S3/GCS + compactor로 장기 보존
  • 구조화 로그 (JSON) 표준화
  • traceId 필드를 로그에 포함해 Tempo와 연결
  • LogQL 알림 → Alertmanager
  • 보존 정책·스트림별 차등
  • Cardinality dashboard 주기 점검
  • Grafana에서 메트릭·로그·트레이스 통합 탐색

마무리

Loki는 “로그 비용은 폭발적으로 늘어나지만 실제로는 라벨 + grep이면 충분하다”는 실용적 통찰에서 출발한 시스템입니다. Elasticsearch·Splunk 같은 full-text 엔진의 파워는 포기하지만 Kubernetes·OTel 시대의 대부분 관측 요구를 훨씬 저렴하고 단순하게 해결합니다. 2026년 현재 Grafana Cloud·AWS Managed Grafana·온프렘 셀프호스트 어디서나 성숙한 운영이 가능하며, LGTM 스택으로 메트릭·로그·트레이스·프로파일을 통일된 UI에서 다루는 경험은 관측 플랫폼의 새 표준을 제시합니다. 지금 Elasticsearch를 쓰고 있다면 부분적으로 Loki를 도입해 비용·운영 부담을 측정해보길 권합니다.

관련 글

  • Prometheus & Grafana 완벽 가이드
  • Elasticsearch 완벽 가이드
  • OpenTelemetry 완벽 가이드
  • Kubernetes 관측 완벽 가이드