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

lokigrafanaloggingmonitoring

Дашборд Grafana с логами из Loki

Если вы когда-нибудь разворачивали 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 (визуализация).

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:

  1. Откройте http://server-ip:3000, войдите с admin/changeme
  2. Перейдите в Connections - Data Sources - Add data source
  3. Выберите Loki, URL: http://loki:3100
  4. Нажмите 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.

Пример: алерт на всплеск ошибок.

  1. Создайте Alert Rule в Grafana
  2. Запрос:
sum(rate({compose_service=~".+"} |= "error" [5m]))
  1. Условие: значение превышает 0.5 (больше 0.5 ошибок в секунду)
  2. Добавьте 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

КритерийLokiElasticsearch
ИндексированиеТолько меткиПолнотекстовое
RAM (минимум)256 МБ4 ГБ
Язык запросовLogQLKQL / 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 будет достаточно ещё очень долго.

© 2026 Terminal Notes. Built with SvelteKit.