Grafana Loki: централизованные логи без Elasticsearch

Если вы когда-нибудь разворачивали ELK-стек (Elasticsearch + Logstash + Kibana) для сбора логов, то знаете, что это тяжёлая артиллерия. Elasticsearch любит оперативную память, Logstash требует тонкой настройки пайплайнов, а Kibana — отдельного изучения. Для крупных проектов с петабайтами логов это оправдано. Но когда у вас 5-15 серверов и несколько десятков контейнеров — хочется чего-то проще.
Grafana Loki — это именно такое решение. Я перешёл на него полтора года назад и с тех пор не оглядывался назад. Расскажу, как устроен Loki, чем он принципиально отличается от Elasticsearch и как развернуть полноценный стек за вечер.
Почему не Elasticsearch
Прежде чем разбирать Loki, стоит понять, какую проблему он решает. Elasticsearch индексирует полное содержимое каждой строки лога. Это мощно — можно искать по любому слову за миллисекунды — но и дорого:
- Память — Elasticsearch рекомендует минимум 4 ГБ RAM на JVM heap, а на практике для комфортной работы нужно 8-16 ГБ
- Диск — полнотекстовый индекс занимает столько же места, сколько сами логи, а иногда и больше
- Сложность — кластеризация, шардирование, маппинги, ILM-политики — всё это нужно понимать и настраивать
- Обслуживание — за Elasticsearch нужно следить: он может уйти в yellow/red status, шарды могут разбалансироваться
Loki работает по другому принципу: он не индексирует содержимое логов. Индексируются только метки (labels) — ключ-значение пары вроде job=nginx, env=production, host=server-01. Сами строки логов хранятся в сжатом виде в объектном хранилище или на файловой системе.
Это значит:
- Потребление ресурсов в 10-20 раз меньше, чем у Elasticsearch
- Хранилище занимает минимум места благодаря сжатию
- Поиск по меткам — мгновенный, поиск по содержимому — чуть медленнее (grep по сжатым чанкам)
- Простота развёртывания — можно запустить в одном бинарнике
Архитектура Loki
Loki состоит из нескольких компонентов, которые можно запускать в одном процессе (single binary mode) или распределять по разным нодам (microservices mode).
Основные компоненты
Distributor — принимает входящие потоки логов от клиентов (Promtail, Fluentd, и другие). Валидирует данные, проверяет лимиты и распределяет потоки по ingester-ам через consistent hashing.
Ingester — буферизует входящие логи в памяти и периодически сбрасывает их в долгосрочное хранилище (chunks). Именно ingester отвечает за создание сжатых чанков из сырых строк логов.
Querier — обрабатывает запросы. Ищет данные и в ingester-ах (свежие данные, ещё не сброшенные на диск), и в хранилище (исторические данные).
Compactor — фоновый процесс для обслуживания индексов. Объединяет мелкие файлы индексов в крупные, применяет retention-политики, удаляет устаревшие данные.
Query Frontend (опционально) — стоит перед querier, разбивает большие запросы на мелкие, кеширует результаты.
Для стека на одном сервере все эти компоненты запускаются в single binary mode — просто один процесс loki, который делает всё.
Развёртывание через Docker Compose
Минимальный стек состоит из трёх сервисов: Loki (хранение и запросы), Promtail (сбор логов), Grafana (визуализация).

Создаём структуру проекта:
mkdir -p loki-stack/{loki,promtail}
cd loki-stack docker-compose.yml
services:
loki:
image: grafana/loki:3.1.0
container_name: loki
restart: unless-stopped
ports:
- "3100:3100"
volumes:
- ./loki/loki-config.yml:/etc/loki/local-config.yaml
- loki-data:/loki
command: -config.file=/etc/loki/local-config.yaml
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--output-document=-", "http://localhost:3100/ready"]
interval: 15s
timeout: 5s
retries: 5
promtail:
image: grafana/promtail:3.1.0
container_name: promtail
restart: unless-stopped
volumes:
- ./promtail/promtail-config.yml:/etc/promtail/config.yml
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
command: -config.file=/etc/promtail/config.yml
depends_on:
loki:
condition: service_healthy
grafana:
image: grafana/grafana:11.1.0
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=changeme
- GF_AUTH_ANONYMOUS_ENABLED=false
volumes:
- grafana-data:/var/lib/grafana
depends_on:
loki:
condition: service_healthy
volumes:
loki-data:
grafana-data: Конфигурация Loki
Файл loki/loki-config.yml:
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: "2024-01-01"
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 30d
max_query_length: 720h
max_query_parallelism: 4
ingestion_rate_mb: 10
ingestion_burst_size_mb: 20
per_stream_rate_limit: 5MB
per_stream_rate_limit_burst: 15MB
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
delete_request_store: filesystem
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100 Конфигурация Promtail
Promtail — это агент, который собирает логи и отправляет их в Loki. Файл promtail/promtail-config.yml:
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# Системные логи
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: syslog
host: my-server
__path__: /var/log/syslog
- job_name: auth
static_configs:
- targets:
- localhost
labels:
job: auth
host: my-server
__path__: /var/log/auth.log
# Docker-контейнеры
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
target_label: 'container'
- source_labels: ['__meta_docker_container_log_stream']
target_label: 'stream'
- source_labels: ['__meta_docker_container_label_com_docker_compose_service']
target_label: 'compose_service' Promtail автоматически обнаруживает Docker-контейнеры через Docker socket и начинает собирать их логи. При этом метки контейнеров (имя, compose-сервис, stream) автоматически становятся label-ами в Loki.
Запуск
docker compose up -d После запуска добавляем Loki как источник данных в Grafana:
- Откройте
http://server-ip:3000, войдите сadmin/changeme - Перейдите в Connections - Data Sources - Add data source
- Выберите Loki, URL:
http://loki:3100 - Нажмите Save & Test
LogQL: язык запросов
LogQL — это язык запросов Loki, вдохновлённый PromQL из Prometheus. Он состоит из двух типов запросов: log queries (возвращают строки логов) и metric queries (возвращают числовые значения).
Базовые фильтры
Каждый запрос начинается с log stream selector — фильтра по меткам:
{job="syslog"} Это вернёт все строки из потока с меткой job=syslog. К селектору можно добавлять фильтры по содержимому:
{job="syslog"} |= "error" Операторы фильтрации:
|=— строка содержит подстроку!=— строка не содержит подстроку|~— строка соответствует regex!~— строка не соответствует regex
Фильтры можно комбинировать:
{compose_service="api"} |= "error" != "healthcheck" |~ "status=[45]\d{2}" Этот запрос найдёт ошибки в логах API-сервиса, исключит healthcheck-запросы и отфильтрует только HTTP-статусы 4xx и 5xx.
Парсинг логов
LogQL умеет парсить структурированные логи прямо в запросе:
# Парсинг JSON-логов
{compose_service="api"} | json | status >= 500
# Парсинг по шаблону (logfmt)
{job="syslog"} | logfmt | level="error"
# Парсинг по regex-паттерну
{job="nginx"} | regexp `(?P<ip>S+) S+ S+ [.+] "(?P<method>S+) (?P<path>S+) .+" (?P<status>d+) (?P<size>d+)`
| status >= 400 После парсинга появляются новые поля, по которым можно фильтровать и агрегировать.
Метрические запросы
Метрические запросы оборачивают log query в агрегирующую функцию:
# Количество ошибок в секунду за последние 5 минут
rate({compose_service="api"} |= "error" [5m])
# Количество логов по уровню за минуту
sum by (level) (
rate({compose_service="api"} | json [1m])
)
# Количество уникальных IP за 5 минут
count_over_time(
{job="nginx"} | regexp `(?P<ip>S+) .+` | distinct ip [5m]
)
# Процентиль времени ответа (p99)
quantile_over_time(0.99,
{compose_service="api"}
| json
| unwrap response_time_ms [5m]
) by (method) Полезные запросы на каждый день
# Топ-10 самых частых ошибок
topk(10,
sum by (error) (
count_over_time({compose_service="api"} | json | level="error" [1h])
)
)
# Сервисы с наибольшим объёмом логов
topk(5,
sum by (compose_service) (
bytes_over_time({compose_service=~".+"} [1h])
)
)
# SSH brute-force попытки
rate({job="auth"} |= "Failed password" [5m]) Настройка Grafana-дашборда для логов
Grafana имеет встроенный Explore-раздел для работы с логами, но для повседневного мониторинга удобнее создать дашборд.
Вот пример дашборда с ключевыми панелями:
Панель 1 — Log Volume (гистограмма). Тип визуализации: Time Series. Запрос:
sum by (compose_service) (
rate({compose_service=~".+"} [1m])
) Показывает объём логов по сервисам во времени. Резкие всплески — это либо ошибка, либо атака, либо дебаг-логи, которые забыли отключить.
Панель 2 — Error Rate (stat). Тип: Stat. Запрос:
sum(rate({compose_service=~".+"} |= "error" [5m])) Одно число — текущая частота ошибок в секунду по всем сервисам.
Панель 3 — Logs (таблица логов). Тип: Logs. Запрос:
{compose_service=~".+"} | json Интерактивная таблица с фильтрацией. В Grafana можно кликнуть по любой метке и сразу отфильтровать.
Alerting на основе логов
Loki поддерживает ruler — компонент, который периодически выполняет запросы и генерирует алерты. Но проще всего настроить алерты через Grafana Alerting.
Пример: алерт на всплеск ошибок.
- Создайте Alert Rule в Grafana
- Запрос:
sum(rate({compose_service=~".+"} |= "error" [5m])) - Условие: значение превышает 0.5 (больше 0.5 ошибок в секунду)
- Добавьте contact point (Telegram, Slack, email)
Ещё несколько полезных алертов:
# Алерт на OOM Killer
rate({job="syslog"} |= "Out of memory" [5m]) > 0
# Алерт на неудачные SSH-входы (больше 10 в минуту)
rate({job="auth"} |= "Failed password" [1m]) > 0.16
# Алерт на отсутствие логов от сервиса (сервис лёг)
absent_over_time({compose_service="api"} [15m]) absent_over_time — особенно полезный запрос. Если сервис перестал слать логи — значит, он упал. Это надёжнее, чем мониторить только ошибки.
Best practices для меток (labels)
Метки — это ключевой элемент Loki. Правильное использование меток критически влияет на производительность.
Используйте статические метки с низкой кардинальностью:
job— тип сервиса (nginx, api, postgres)env— окружение (production, staging)host— имя хостаcompose_service— имя сервиса из Docker Compose
Не используйте динамические метки с высокой кардинальностью:
- IP-адреса клиентов
- ID запросов
- Имена пользователей
- Timestamps
Каждая уникальная комбинация меток создаёт отдельный поток (stream). Если у вас 1000 уникальных IP и 10 сервисов — это 10 000 потоков. Loki начнёт тормозить и потреблять много памяти.
Правило простое: если значение метки может принимать сотни или тысячи уникальных значений — это не метка, это поле для парсинга через | json или | logfmt.
Multi-tenancy
Loki поддерживает мультитенантность — разделение логов между командами или проектами. Включается параметром auth_enabled: true в конфигурации.
После включения каждый запрос должен содержать заголовок X-Scope-OrgID. Promtail настраивается так:
clients:
- url: http://loki:3100/loki/api/v1/push
tenant_id: team-backend В Grafana tenant указывается в настройках data source в поле HTTP Headers:
- Header:
X-Scope-OrgID - Value:
team-backend
Это удобно, когда несколько команд используют один Loki, но каждая должна видеть только свои логи.
Retention: управление хранением
Retention настраивается в секции limits_config и compactor:
limits_config:
retention_period: 30d
compactor:
retention_enabled: true
retention_delete_delay: 2h
delete_request_store: filesystem Можно задать разные сроки хранения для разных потоков через retention_stream:
limits_config:
retention_period: 30d
retention_stream:
- selector: '{job="nginx"}'
priority: 1
period: 7d
- selector: '{env="staging"}'
priority: 2
period: 3d
- selector: '{job="auth"}'
priority: 3
period: 90d В этом примере логи nginx хранятся 7 дней, staging — 3 дня, auth-логи — 90 дней, а всё остальное — 30 дней. Приоритет определяет порядок применения правил.
Сравнение: Loki vs Elasticsearch
| Критерий | Loki | Elasticsearch |
|---|---|---|
| Индексирование | Только метки | Полнотекстовое |
| RAM (минимум) | 256 МБ | 4 ГБ |
| Язык запросов | LogQL | KQL / Lucene |
| Полнотекстовый поиск | Grep по чанкам | Индексированный поиск |
| Сложность развёртывания | Один бинарник | Кластер нод |
| Интеграция с Grafana | Нативная | Через плагин |
| Стоимость хранения | Низкая (сжатие) | Высокая (индексы) |
| Масштабирование | Горизонтальное | Горизонтальное |
Когда выбрать Loki: у вас уже есть Grafana, вам нужен лёгкий стек, ресурсы ограничены, вы работаете с label-based фильтрацией.
Когда выбрать Elasticsearch: вам критичен мгновенный полнотекстовый поиск по миллиардам строк, нужен сложный анализ текста, у вас есть ресурсы и команда для обслуживания.
Подводные камни
Chunk flush на остановке. При остановке Loki ingester должен успеть сбросить буфер на диск. Используйте docker compose stop (не kill) и настройте ingester.wal для защиты от потери данных.
Лимиты по умолчанию. Дефолтные лимиты Loki достаточно жёсткие. Если видите ошибки 429 Too Many Requests или rate limit exceeded — проверьте ingestion_rate_mb и per_stream_rate_limit.
Большие строки логов. Loki по умолчанию отклоняет строки длиннее определённого размера. Если ваши приложения пишут stack trace-ы в одну строку — увеличьте max_line_size или настройте multiline в Promtail.
Заключение
Loki занимает нишу между «логи пишутся в файлы на каждом сервере, смотрим через tail и grep» и «полноценный ELK-стек с выделенным кластером». Для большинства self-hosted проектов и небольших продакшенов это оптимальный выбор. Развёртывание за час, потребление ресурсов минимальное, интеграция с Grafana нативная, а LogQL позволяет строить метрики прямо по логам.
Начните с single binary mode и Promtail. Когда (и если) упрётесь в производительность — Loki можно масштабировать горизонтально, вынести хранилище в S3 и разнести компоненты по нодам. Но скорее всего, для стека до 20 серверов, single binary на выделенном VPS с 2 ГБ RAM будет достаточно ещё очень долго.