- Published on
Grafana Loki 로그 관리 완전 가이드: LogQL 쿼리·수집 파이프라인·알림 설정
- Authors
- Name
- 들어가며
- 1. Loki 아키텍처 개요
- 2. 저장 구조와 인덱싱 전략
- 3. LogQL 쿼리 문법 심층
- 4. Promtail과 Grafana Alloy 수집 파이프라인
- 5. Kubernetes 환경 로그 수집
- 6. 알림 규칙 설정 (Loki Ruler)
- 7. 대시보드 구성 패턴
- 8. 비교표: Loki vs Elasticsearch vs CloudWatch
- 9. 장애 사례와 복구 절차
- 10. 운영 체크리스트
- 마무리

들어가며
마이크로서비스 아키텍처와 Kubernetes 기반 인프라가 보편화되면서, 수백 개의 컨테이너에서 쏟아지는 로그를 효율적으로 수집하고 분석하는 것이 운영의 핵심 과제가 되었다. Elasticsearch 기반 ELK 스택이 오랫동안 로그 관리의 표준이었지만, 대규모 환경에서 높은 인프라 비용과 운영 복잡도가 문제로 지적되어 왔다.
Grafana Loki는 "로그를 위한 Prometheus"라는 철학으로 탄생한 로그 수집 시스템이다. 로그 내용 전체를 인덱싱하는 대신 레이블 메타데이터만 인덱싱함으로써 저장 비용을 극적으로 줄이면서도, LogQL이라는 강력한 쿼리 언어를 통해 실시간 로그 분석과 메트릭 추출을 지원한다.
이 글에서는 Loki의 아키텍처부터 LogQL 쿼리 문법, 수집 파이프라인 구성, 알림 설정, 그리고 실전 운영 패턴까지 종합적으로 다룬다.
1. Loki 아키텍처 개요
Loki는 마이크로서비스 아키텍처로 설계되어, 각 컴포넌트를 독립적으로 수평 확장할 수 있다. 핵심 컴포넌트는 다음과 같다.
Distributor
수집 에이전트(Promtail, Alloy 등)로부터 로그 push 요청을 수신하는 첫 번째 컴포넌트다. 수신된 로그 스트림의 유효성을 검증한 뒤, 일관된 해시 링(consistent hash ring)을 사용하여 적절한 Ingester로 라우팅한다. 복제 팩터(replication factor)에 따라 여러 Ingester에 동시에 전송하여 데이터 유실을 방지한다.
Ingester
Distributor로부터 전달받은 로그를 메모리에 버퍼링한 뒤, 압축된 청크(chunk) 형태로 장기 스토리지(S3, GCS, Azure Blob 등)에 기록하는 컴포넌트다. 쿼리 요청이 들어오면 아직 플러시되지 않은 인메모리 데이터도 함께 반환한다.
Querier
LogQL 쿼리를 처리하는 읽기 경로의 핵심 컴포넌트다. Ingester의 인메모리 데이터와 장기 스토리지의 청크 데이터를 병합하여 쿼리 결과를 생성한다. Query Frontend를 통해 쿼리를 분할하고 캐싱하여 대규모 범위 쿼리의 성능을 최적화한다.
Compactor
장기 스토리지에 저장된 인덱스 파일을 압축하고 최적화하는 백그라운드 컴포넌트다. 보존 정책(retention policy) 적용과 삭제 처리도 담당한다.
# Loki 마이크로서비스 모드 기본 설정 예시
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
path_prefix: /loki
storage:
s3:
endpoint: s3.amazonaws.com
bucketnames: loki-chunks
region: ap-northeast-2
access_key_id: ACCESS_KEY
secret_access_key: SECRET_KEY
replication_factor: 3
ring:
kvstore:
store: memberlist
schema_config:
configs:
- from: '2024-01-01'
store: tsdb
object_store: s3
schema: v13
index:
prefix: index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/tsdb-index
cache_location: /loki/tsdb-cache
limits_config:
max_query_parallelism: 32
max_query_series: 500
retention_period: 30d
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
2. 저장 구조와 인덱싱 전략
Loki의 가장 큰 차별점은 레이블 기반 인덱싱 전략이다. Elasticsearch가 로그 내용의 모든 토큰을 역색인(inverted index)으로 구축하는 반면, Loki는 레이블 메타데이터만 인덱싱하고 로그 본문은 압축된 청크로 오브젝트 스토리지에 저장한다.
인덱스와 청크의 분리
- 인덱스: 레이블 조합과 시간 범위를 매핑하는 소량의 메타데이터. TSDB(Time Series Database) 형식으로 저장
- 청크: 실제 로그 라인을 gzip/snappy로 압축하여 저장. S3, GCS 등 저비용 오브젝트 스토리지 활용
이 구조 덕분에 하루 100GB의 로그를 처리할 때, Elasticsearch 대비 약 70~80%의 스토리지 비용을 절약할 수 있다.
레이블 설계 원칙
레이블 카디널리티(cardinality)가 높으면 인덱스 크기가 폭발적으로 증가하므로, 다음 원칙을 지켜야 한다.
- 정적 레이블 사용: namespace, service, environment 등 값의 종류가 제한된 속성
- 동적 값 금지: user_id, request_id, IP 주소 등 무한히 증가하는 값은 레이블로 사용하지 않음
- 파싱으로 대체: 동적 속성은 LogQL 파이프라인에서 추출하여 필터링
3. LogQL 쿼리 문법 심층
LogQL은 PromQL에서 영감을 받은 Loki의 쿼리 언어로, 로그 스트림 셀렉터와 파이프라인 스테이지로 구성된다.
3.1 로그 스트림 셀렉터
중괄호 안에 레이블 매처를 지정하여 대상 로그 스트림을 선택한다.
# 정확한 일치
{namespace="production", app="api-gateway"}
# 부정 일치
{namespace="production", app!="debug-tool"}
# 정규식 매칭
{namespace="production", app=~"api-.+"}
# 정규식 제외
{namespace=~"prod|staging", app!~"test-.+"}
3.2 파이프라인 스테이지
스트림 셀렉터 뒤에 파이프(|) 기호로 여러 처리 단계를 연결한다.
라인 필터(Line Filter)
# 문자열 포함 필터
{app="api-gateway"} |= "error"
# 문자열 미포함 필터
{app="api-gateway"} != "healthcheck"
# 정규식 필터
{app="api-gateway"} |~ "status=[45]\\d{2}"
# 정규식 제외 필터
{app="api-gateway"} !~ "GET /health"
파서(Parser)
# JSON 로그 파싱 - 모든 JSON 필드를 레이블로 추출
{app="api-gateway"} | json
# 특정 JSON 필드만 추출
{app="api-gateway"} | json level, method, duration
# logfmt 형식 파싱
{app="api-gateway"} | logfmt
# 정규식 파싱 - 패턴 매칭으로 필드 추출
{app="nginx"} | regexp `(?P<ip>\\S+) - - \\[(?P<ts>.+?)\\] "(?P<method>\\S+) (?P<path>\\S+)"`
# pattern 파서 - 간결한 패턴 매칭
{app="nginx"} | pattern `<ip> - - [<_>] "<method> <path> <_>" <status> <size>`
레이블 필터(Label Filter)
# 파싱 후 추출된 레이블로 필터링
{app="api-gateway"} | json | level="error"
{app="api-gateway"} | json | duration > 500ms
{app="api-gateway"} | json | status >= 400 and method="POST"
3.3 메트릭 쿼리
LogQL에서 로그 스트림을 메트릭으로 변환하여 시계열 데이터를 생성할 수 있다. Grafana 대시보드와 알림 규칙에서 핵심적으로 사용된다.
# 초당 로그 발생률 (로그 범위 집계)
rate({app="api-gateway"} |= "error" [5m])
# 특정 상태 코드의 초당 발생률
sum(rate({app="api-gateway"} | json | status >= 500 [5m])) by (method)
# 응답 시간 분포 (quantile 추출)
quantile_over_time(0.99, {app="api-gateway"} | json | unwrap duration [5m]) by (method)
# 바이트 단위 전송량 합계
sum(bytes_over_time({app="nginx"} [1h])) by (namespace)
# 에러율 계산 (에러 로그 수 / 전체 로그 수)
sum(rate({app="api-gateway"} | json | level="error" [5m]))
/
sum(rate({app="api-gateway"} [5m]))
4. Promtail과 Grafana Alloy 수집 파이프라인
Promtail (레거시 에이전트)
Promtail은 Loki 전용 로그 수집 에이전트로, 각 노드에서 DaemonSet으로 실행되어 로그 파일을 감시하고 Loki로 전송한다. 2025년 2월 공식 LTS(Long-Term Support) 모드로 전환되었으며, 2026년 3월 EOL이 예정되어 있다.
# Promtail 설정 예시
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki-gateway:3100/loki/api/v1/push
tenant_id: default
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_name]
target_label: pod
# 컨테이너 이름 레이블 추가
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
pipeline_stages:
# Docker 로그 형식 파싱
- docker: {}
# JSON 로그 파싱
- json:
expressions:
level: level
msg: message
# 레이블 설정
- labels:
level:
# 타임스탬프 추출
- timestamp:
source: time
format: RFC3339Nano
Grafana Alloy (차세대 에이전트)
Grafana Alloy는 Promtail의 후속으로, 로그뿐 아니라 메트릭, 트레이스, 프로파일링까지 단일 에이전트로 수집하는 OpenTelemetry 기반의 통합 텔레메트리 콜렉터다.
// Grafana Alloy 설정 예시 (River 문법)
discovery.kubernetes "pods" {
role = "pod"
}
discovery.relabel "pod_logs" {
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_container_name"]
target_label = "container"
}
}
loki.source.kubernetes "pod_logs" {
targets = discovery.relabel.pod_logs.output
forward_to = [loki.process.pipeline.receiver]
}
loki.process "pipeline" {
stage.json {
expressions = {
level = "level",
message = "msg",
}
}
stage.labels {
values = {
level = "",
}
}
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki-gateway:3100/loki/api/v1/push"
}
}
5. Kubernetes 환경 로그 수집
Kubernetes 환경에서 Loki를 배포할 때는 Helm 차트를 사용하는 것이 표준 방식이다.
# Loki Helm 차트 설치 (Simple Scalable 모드)
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm install loki grafana/loki \
--namespace observability \
--create-namespace \
--values loki-values.yaml
# Grafana Alloy DaemonSet 설치
helm install alloy grafana/alloy \
--namespace observability \
--values alloy-values.yaml
Kubernetes 환경에서 주의해야 할 수집 설정 포인트는 다음과 같다.
- 네임스페이스 필터링: 불필요한 시스템 로그(kube-system 등)를 제외하여 비용 절감
- 멀티테넌시:
X-Scope-OrgID헤더를 활용하여 팀별 로그 격리 - 리소스 제한: Ingester와 Querier의 메모리 제한을 적절히 설정하여 OOM 방지
- PVC 관리: Ingester의 WAL(Write-Ahead Log)을 위한 영구 볼륨 확보
6. 알림 규칙 설정 (Loki Ruler)
Loki Ruler는 LogQL 메트릭 쿼리를 주기적으로 평가하여, 임계값을 초과하면 Alertmanager로 알림을 전송한다. Prometheus의 알림 규칙과 동일한 YAML 형식을 사용한다.
# loki-alert-rules.yaml
groups:
- name: application-errors
rules:
# HTTP 5xx 에러 급증 감지
- alert: HighHTTP5xxRate
expr: |
sum(rate({namespace="production"} | json | status >= 500 [5m])) by (app)
> 10
for: 5m
labels:
severity: critical
team: backend
annotations:
summary: 'HTTP 5xx 에러율 급증'
description: '앱 {{ .Labels.app }}에서 5분간 초당 10건 이상의 5xx 에러가 발생하고 있습니다.'
# 에러 로그 비율 감시
- alert: HighErrorLogRatio
expr: |
sum(rate({namespace="production"} | json | level="error" [10m])) by (app)
/
sum(rate({namespace="production"} [10m])) by (app)
> 0.05
for: 10m
labels:
severity: warning
team: platform
annotations:
summary: '에러 로그 비율 5% 초과'
description: '앱 {{ .Labels.app }}의 에러 로그 비율이 10분간 5%를 초과했습니다.'
# 로그 수집 중단 감지
- alert: LogIngestionStopped
expr: |
sum(rate({namespace="production"} [15m])) by (app) == 0
for: 15m
labels:
severity: critical
team: platform
annotations:
summary: '로그 수집 중단 감지'
description: '앱 {{ .Labels.app }}에서 15분간 로그가 수집되지 않고 있습니다.'
- name: security-alerts
rules:
# 인증 실패 다수 발생
- alert: BruteForceAttempt
expr: |
sum(rate({app="auth-service"} |= "authentication failed" [5m])) by (source_ip)
> 5
for: 2m
labels:
severity: critical
team: security
annotations:
summary: '무차별 대입 공격 의심'
description: 'IP {{ .Labels.source_ip }}에서 5분간 초당 5건 이상의 인증 실패가 발생했습니다.'
Ruler 설정을 Loki에 적용하려면 다음과 같이 구성한다.
# Loki ruler 설정 블록
ruler:
storage:
type: local
local:
directory: /loki/rules
rule_path: /loki/rules-temp
alertmanager_url: http://alertmanager:9093
ring:
kvstore:
store: memberlist
enable_api: true
evaluation_interval: 1m
7. 대시보드 구성 패턴
Grafana에서 Loki 데이터 소스를 활용한 효과적인 대시보드 구성 패턴은 다음과 같다.
핵심 패널 구성
- 로그 볼륨 히스토그램: 시간대별 로그 발생량을 레벨(info, warn, error) 기준으로 스택
- 에러율 시계열 그래프: 서비스별 에러 로그 비율을 실시간으로 모니터링
- Top-N 에러 메시지 테이블: 가장 빈번한 에러 패턴을 집계하여 우선순위 파악
- 로그 탐색 패널: 변수(variable)를 활용한 동적 필터링으로 드릴다운
Grafana 변수 설정
namespace변수:label_values(namespace)쿼리로 동적 네임스페이스 선택app변수:label_values(app)쿼리로 서비스 필터링- 변수 체이닝을 통한 계층적 필터: namespace 선택 후 해당 namespace의 app만 표시
8. 비교표: Loki vs Elasticsearch vs CloudWatch
| 항목 | Grafana Loki | Elasticsearch | AWS CloudWatch Logs | | ------------------------ | ----------------------------- | ------------------------------------- | ------------------- | ------------------- | | 인덱싱 방식 | 레이블만 인덱싱 | 전문 역색인(full-text inverted index) | 로그 그룹 기반 | | 스토리지 비용 | 매우 낮음 (오브젝트 스토리지) | 높음 (SSD 필요) | 중간 (종량제) | | 쿼리 언어 | LogQL (PromQL 유사) | Lucene / KQL / ES | QL | CloudWatch Insights | | 전문 검색 | 제한적 (브루트포스) | 매우 강력 | 중간 | | K8s 통합 | 네이티브 | 추가 설정 필요 | EKS 통합 | | 운영 난이도 | 낮음중간 | 높음 (JVM 튜닝) | 매우 낮음 (관리형) | | 수평 확장 | 컴포넌트별 독립 확장 | 샤드/레플리카 관리 | 자동 | | 알림 통합 | Ruler + Alertmanager | Watcher / ElastAlert | CloudWatch Alarms | | 멀티테넌시 | 네이티브 지원 | 인덱스 분리로 구현 | 계정/리전 분리 | | 일일 100GB 예상 비용 | 월 50100 USD | 월 300600 USD | 월 150300 USD |
선택 기준 요약
- Loki: Kubernetes 환경에서 비용 효율적인 로그 관리가 목표. 레이블 기반 필터링으로 충분한 경우
- Elasticsearch: 비정형 로그에 대한 강력한 전문 검색이 필수. 보안 분석(SIEM) 용도
- CloudWatch: AWS 네이티브 워크로드에서 운영 부담을 최소화하고 싶은 경우
9. 장애 사례와 복구 절차
사례 1: Ingester OOM (Out of Memory)
증상: Ingester 파드가 반복적으로 OOMKilled, 로그 수집 중단
원인: 레이블 카디널리티 폭발로 인한 인메모리 스트림 과다 생성
복구 절차:
- 높은 카디널리티 레이블 식별:
logql쿼리로 고유 스트림 수 확인 - Promtail/Alloy 설정에서 문제 레이블 제거 또는 relabel
- Ingester 메모리 한도 상향 조정 (임시 조치)
limits_config.max_streams_per_user값을 적절히 제한
사례 2: 쿼리 타임아웃
증상: Grafana 대시보드에서 쿼리 로딩이 30초 이상 소요되거나 타임아웃
원인: 과도한 시간 범위 쿼리 또는 비효율적인 LogQL
복구 절차:
- 쿼리 범위를 줄이고 스트림 셀렉터를 구체화
- Query Frontend의
split_queries_by_interval설정으로 쿼리 분할 - 자주 사용하는 쿼리는 Recording Rule로 사전 계산
- 캐시(memcached, Redis) 설정 확인 및 적용
사례 3: 청크 저장 실패
증상: Ingester 로그에 S3/GCS 업로드 에러 반복 발생
복구 절차:
- 오브젝트 스토리지 IAM 권한 확인
- 네트워크 연결 상태 점검
- Ingester의 WAL(Write-Ahead Log) 정상 여부 확인
flush_on_shutdown: true설정으로 안전한 종료 보장
10. 운영 체크리스트
배포 전 체크리스트
- 레이블 카디널리티 설계 검토 완료
- 보존 정책(retention) 설정
- 오브젝트 스토리지 버킷 및 IAM 권한 구성
- 멀티테넌시 전략 수립 (필요 시)
- 리소스 제한(requests/limits) 설정
운영 중 체크리스트
- Ingester 메모리 사용률 모니터링 (80% 미만 유지)
- 로그 수집 지연(lag) 모니터링
- 쿼리 응답 시간 SLO 준수 여부 확인
- 청크 저장 성공률 모니터링
- Compactor 작업 정상 수행 확인
성능 최적화 체크리스트
- Query Frontend 캐시 적용 (memcached 권장)
- Recording Rule로 자주 사용되는 메트릭 사전 계산
- 불필요한 로그 드롭 규칙 적용 (debug 레벨 등)
- 청크 압축 알고리즘 최적화 (snappy vs gzip)
- 인덱스 기간(period) 적절성 검토
마무리
Grafana Loki는 "모든 로그를 인덱싱할 필요는 없다"는 발상의 전환을 통해, 대규모 Kubernetes 환경에서 비용 효율적인 로그 관리를 가능하게 한다. LogQL의 파이프라인 기반 쿼리, Ruler를 통한 알림, 그리고 Grafana 생태계와의 긴밀한 통합은 Loki를 클라우드 네이티브 옵저버빌리티의 핵심 도구로 자리매김하게 했다.
특히 Promtail에서 Grafana Alloy로의 전환이 진행됨에 따라, 로그뿐 아니라 메트릭, 트레이스, 프로파일링까지 단일 에이전트로 통합 수집하는 시대가 열리고 있다. 기존 ELK 스택의 높은 운영 비용에 부담을 느끼는 팀이라면, Loki 도입을 적극 검토해 볼 시점이다.
운영에서 가장 중요한 것은 레이블 설계와 카디널리티 관리다. 올바른 레이블 전략 없이는 Loki라 하더라도 스토리지와 성능 문제에 직면할 수 있다. 이 글에서 다룬 아키텍처 이해, LogQL 활용, 알림 설정, 그리고 장애 대응 패턴을 기반으로 안정적인 로그 관리 체계를 구축하기 바란다.