Loki и LogQL: логи без накладных расходов Elasticsearch
Published: 2026-03-21
Loki индексирует только метки логов, а не их содержимое. Это делает его существенно дешевле Elasticsearch — в 10 раз меньше диска, минимальный CPU при приёме. Компромисс: полнотекстовый поиск медленнее (regex-сканирование строк, а не инвертированный индекс). Для большинства задач — поиска ошибок, трассировки запросов, отладки — LogQL достаточно быстр, а экономия реальна.
Установка через Helm
yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: loki
namespace: observability
spec:
chart:
spec:
chart: loki
version: "6.x"
sourceRef:
kind: HelmRepository
name: grafana
values:
deploymentMode: SingleBinary
loki:
commonConfig:
replication_factor: 1
storage:
type: filesystem
auth_enabled: false
limits_config:
retention_period: 30d
ingestion_rate_mb: 16
ingestion_burst_size_mb: 32
singleBinary:
replicas: 1
persistence:
enabled: true
storageClass: local-path
size: 50Gi
monitoring:
serviceMonitor:
enabled: true
labels:
release: kube-prom-stack
SingleBinary запускает все компоненты в одном поде. Для HA — SimpleScalable или Distributed с объектным хранилищем.
Grafana Alloy (замена Promtail)
Promtail устарел, его заменяет Grafana Alloy. Alloy отправляет логи в Loki, метрики в Prometheus и трейсы в Tempo:
yamlvalues:
alloy:
configMap:
content: |
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_label_app"]
target_label = "app"
}
}
loki.source.kubernetes "pods" {
targets = discovery.relabel.pods.output
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki.observability.svc.cluster.local:3100/loki/api/v1/push"
}
}
Основы LogQL
Stream selectors:
logql{namespace="app", app="my-service"}
{namespace="app"} |= "ERROR"
{namespace="app"} |~ "timeout|refused"
{namespace="app"} != "healthz"
|=строка содержит подстроку|~строка совпадает с regex!=строка не содержит подстроку!~строка не совпадает с regex
Парсинг JSON:
logql{namespace="app", app="my-service"}
| json
| level="error"
| line_format "{{.message}}"
| json парсит строку как JSON и извлекает поля. Фильтр | level="error" применяется к извлечённому полю.
Rate-запросы для Grafana:
logqlrate({namespace="app"} |= "ERROR" [5m])
Количество ошибок по сервисам:
logqlsum by (app) (
rate({namespace="app"} |= "ERROR" [5m])
)
Datasource в Grafana
yamladditionalDataSources:
- name: Loki
type: loki
url: http://loki.observability.svc.cluster.local:3100
jsonData:
derivedFields:
- name: TraceID
matcherRegex: '"traceId":"(\w+)"'
url: "${__value.raw}"
datasourceUid: jaeger
derivedFields превращает trace ID в кликабельную ссылку, открывающую трейс в Jaeger.
Loki vs Elasticsearch
| Возможность | Loki | Elasticsearch |
|---|---|---|
| Стоимость хранилища | Низкая (индекс только меток) | Высокая (полный инвертированный индекс) |
| CPU при приёме | Минимальный | Значительный |
| Полнотекстовый поиск | Regex-сканирование (медленнее) | Инвертированный индекс (быстро) |
| Фильтрация по полям | Через JSON-парсер | Встроенная |
| Интеграция | Grafana | Kibana |
Диагностика
Too many streams: избегайте меток с высокой кардинальностью (имя пода, request ID). Ограничьтесь метками namespace, app, job, level.
Превышен лимит приёма: увеличьте ingestion_rate_mb в limits_config.
Таймаут запросов: добавьте |= "search term" перед парсингом — быстрая фильтрация по меткам сочетается с фильтром по содержимому, который сужает выборку.