Logging Loki

Централизованное логирование — сбор логов приложений, контейнеров и инфраструктуры в единой системе. Оно позволяет искать события по всем экземплярам сервисов, сохранять логи после перезапуска, строить дашборды и связывать ошибки с метриками.

Типичный поток:

Приложение / контейнер → Promtail → Loki → Grafana

Loki — система агрегации логов от Grafana Labs. По модели она похожа на Prometheus: группирует данные по labels и поддерживает LogQL. Loki не строит полнотекстовый индекс для каждой строки: индексируются labels, а записи хранятся в сжатых блоках. Поэтому выбор labels особенно важен.

Содержание


Централизованное логирование

При локальном хранении логи находятся на конкретном сервере или внутри контейнера. При масштабировании приходится вручную искать нужный экземпляр сервиса. Если контейнер удалён, его логи могут быть потеряны.

Централизованная система:

Типичные компоненты:

  1. Источник — приложение, ОС, reverse proxy, база данных или контейнер.
  2. Агент — читает логи, добавляет labels и отправляет записи.
  3. Хранилище — принимает и хранит данные.
  4. Интерфейс — поиск, дашборды и алерты.

Для контейнеров предпочтительно писать логи в stdout и stderr, а доставку поручать платформе или агенту.

Структурированные логи

Для серверных приложений удобно использовать JSON:

{
  "timestamp": "2026-09-21T14:32:10Z",
  "level": "error",
  "service": "orders",
  "message": "database timeout",
  "request_id": "req-8d91",
  "trace_id": "abc123",
  "duration_ms": 2500,
  "status": 500
}

Полезные поля:

Поле Назначение
timestamp Время события, желательно UTC
level debug, info, warn, error
service Имя сервиса
environment Окружение
message Описание события
request_id Идентификатор запроса
trace_id Идентификатор трассировки
duration_ms Продолжительность операции
status HTTP-код или результат

Не следует писать в логи пароли, токены, приватные ключи, Authorization, платёжные данные и другие чувствительные сведения. Маскирование на агенте полезно как дополнительный слой, но лучше не выводить секрет из приложения вообще.


Loki и архитектура

Loki принимает записи через HTTP API, группирует их по labels, сжимает и сохраняет в chunks. LogQL сначала выбирает потоки по labels, а затем фильтрует строки и извлекает поля.

Однопроцессный режим

Для разработки и небольших стендов:

Promtail → Loki → локальный диск

Преимущества — простота и низкие требования. Недостатки — единственная точка отказа и риск потери данных при удалении volume.

Масштабируемый режим

Клиенты → Distributor → Ingester → объектное хранилище
                    ▲
                    │
Grafana → Query Frontend → Querier
Компонент Назначение
Distributor Принимает и распределяет записи
Ingester Формирует chunks и сохраняет данные
Querier Выполняет запросы
Query Frontend Планирует, разбивает и кеширует запросы
Compactor Обслуживает индекс и retention
Ruler Выполняет recording и alerting rules

Процесс записи:

  1. Агент формирует batch.
  2. Loki принимает записи.
  3. Записи группируются по одинаковым labels.
  4. Ingester формирует chunks.
  5. Chunks и индекс сохраняются в хранилище.

Процесс чтения выполняется в обратном направлении: по labels выбираются потоки, читаются chunks, затем применяются фильтры, парсеры и агрегации.


Потоки и labels

Поток логов — набор записей с одинаковым набором labels:

{service="orders", environment="production", instance="orders-1"}

Другой instance создаёт отдельный поток.

Хорошие labels

Обычно полезны labels с ограниченным количеством значений:

job
service
application
environment
cluster
namespace
pod
container
host
level

Пример выборки:

{service="orders", environment="production"}

Опасные labels

Не следует без необходимости добавлять значения с высокой кардинальностью:

request_id
trace_id
user_id
session_id
order_id
full_url
client_ip
timestamp

Каждое уникальное сочетание увеличивает число потоков и нагрузку на индекс. Такие значения лучше оставлять внутри JSON:

{service="orders"} | json | request_id="req-8d91"

Promtail

Promtail — агент, который читает логи, обнаруживает источники, добавляет labels, выполняет pipeline stages и отправляет записи в Loki. Он умеет работать с файлами, Docker-контейнерами, Kubernetes pods, JSON, logfmt и регулярными выражениями. В новых системах также может применяться Grafana Alloy.

Минимальная конфигурация

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /var/lib/promtail/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: application
    static_configs:
      - targets:
          - localhost
        labels:
          job: application
          service: orders
          environment: production
          __path__: /var/log/myapp/*.log

positions.filename хранит позицию чтения каждого файла. Его следует размещать в постоянном volume, иначе после перезапуска агент может повторно отправить старые записи.

Pipeline stages

Pipeline stages обрабатывают строку до отправки.

Разбор JSON:

pipeline_stages:
  - json:
      expressions:
        timestamp: timestamp
        level: level
        service: service
        message: message
  - timestamp:
      source: timestamp
      format: RFC3339
  - labels:
      level:
      service:

Разбор logfmt:

pipeline_stages:
  - logfmt:
      mapping:
        level:
        service:
        duration_ms:
        message:
  - labels:
      level:
      service:

Маскирование токена:

pipeline_stages:
  - replace:
      expression: '(?i)(token=)[^&\\s]+'
      replace: '${1}[REDACTED]'

Сбор Docker-логов

scrape_configs:
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
    relabel_configs:
      - source_labels: [__meta_docker_container_name]
        regex: '/(.*)'
        target_label: container
      - source_labels: [__meta_docker_container_label_com_docker_compose_service]
        target_label: service
    pipeline_stages:
      - docker: {}

Доступ к /var/run/docker.sock очень чувствителен: Docker API даёт широкие права управления Engine. Ограничивайте доступ, не публикуйте socket наружу и при необходимости используйте socket proxy или сбор файлов логов без Docker API.


LogQL

LogQL поддерживает:

Общий конвейер:

селектор потоков → фильтр строк → парсер → фильтр полей → агрегация

Селекторы labels

{service="orders"}
{service="orders", environment="production"}

Операторы:

Оператор Значение
= Точное совпадение
!= Не равно
=~ Совпадение с регулярным выражением
!~ Исключение по регулярному выражению

Примеры:

{service=~"orders|payments"}
{environment!="development"}

Фильтрация срок

{service="orders"} |= "error"
{service="api"} != "/health"
{job="nginx"} |~ " 5[0-9]{2} "

Если достаточно точного текста, лучше использовать |=, а не regexp.

Парсинг JSON и logfmt

{service="orders"}
  | json
  | level="error"
{service="orders"}
  | json
  | duration_ms > 1000
{service="orders"}
  | json
  | request_id="req-8d91"
{service="orders"}
  | logfmt
  | level="error"

Pattern parser для фиксированного текстового формата:

{job="nginx"}
  | pattern `<ip> - - <_> "<method> <path> <_>" <status> <size>`
  | status >= 500

Проверка ошибок парсинга:

{service="orders"} | json | __error__ != ""

Форматирование результата:

{service="orders"}
  | json
  | line_format `{{.level}} {{.request_id}} {{.message}}`

Метрики из логов

Количество строк за пять минут:

count_over_time({service="orders"}[5m])

Количество ошибок:

sum(count_over_time({service="orders"} |= "error" [5m]))

Частота ошибок по сервисам:

sum by (service) (
  rate({environment="production"} |= "error" [5m])
)

Объём логов:

sum by (service) (
  bytes_rate({environment="production"}[5m])
)

Средняя продолжительность операции:

avg_over_time(
  {service="orders"}
    | json
    | unwrap duration_ms
    | __error__ = ""
  [5m]
)

95-й перцентиль:

quantile_over_time(
  0.95,
  {service="orders"}
    | json
    | unwrap duration_ms
    | __error__ = ""
  [5m]
)

Для основных SLI лучше использовать нативные метрики приложения. Метрики из логов полезны как дополнение.


Связь логов с метриками

Метрики отвечают на вопрос «что происходит?», а логи — «почему это произошло?».

Пример расследования:

  1. На дашборде Prometheus выросла доля HTTP 5xx.
  2. В Grafana выбирается тот же временной диапазон.
  3. Открываются логи соответствующего service и environment.
  4. Выполняется фильтрация по level, status или тексту ошибки.
  5. По request_id или trace_id находится конкретная операция.

Для корреляции используются общие labels:

service
environment
cluster
namespace
pod
instance
region

Метрика:

http_requests_total{service="orders", environment="production", status="500"}

Поток Loki:

{service="orders", environment="production"}

Grafana может передавать значения переменных из панели метрик в запрос Loki. Derived fields позволяют извлекать trace_id из логов и создавать ссылку в системе трассировки:

Метрика → Логи → Trace

Recording rules

Частый тяжёлый запрос можно считать заранее:

groups:
  - name: loki-rules
    interval: 1m
    rules:
      - record: service:log_errors:rate5m
        expr: |
          sum by (service) (
            rate({environment="production"} |= "error" [5m])
          )

Перед созданием правила проверьте, что фильтр действительно находит ошибки и не создаёт ложные совпадения.


Grafana и алертинг

Loki добавляется в Grafana как data source. В Docker Compose обычно используется адрес:

http://loki:3100

В разделе Explore можно выбрать Loki, указать диапазон времени, выбрать labels, выполнить LogQL-запрос, просмотреть строки и переключиться к графику временного ряда.

Типы панелей:

Пример запроса с переменными дашборда:

{environment="$environment", service=~"$service"}
  | json
  | level=~"$level"

Полезный дашборд содержит общий объём логов, частоту error и warn, HTTP 5xx, последние ошибки, p95 latency и ошибки разбора.

Алертинг по логам

Алерт должен использовать metric query:

sum(
  rate(
    {service="orders", environment="production"}
      | json
      | level="error"
    [5m]
  )
) > 0.1

Правило можно выполнять через Loki Ruler и передавать в Alertmanager. Alertmanager группирует уведомления, маршрутизирует их и поддерживает silences.

Алерты по логам следует подтверждать метриками доступности: отсутствие логов может означать отказ самого приложения или агента, а не отсутствие ошибок.


Хранение и безопасность

Retention

Срок хранения выбирают по требованиям бизнеса, аудита, безопасности и стоимости. Пример для локального стенда:

limits_config:
  retention_period: 168h

168h — семь суток.

Локальный filesystem подходит для разработки. Для production обычно используют объектное хранилище, резервирование, compactor и lifecycle policy.

Упрощённая оценка объёма:

объём логов в сутки × срок хранения × коэффициент сжатия и служебных данных

Реальный объём удобно оценивать через bytes_rate.

Безопасность

Loki не следует публиковать без защиты. Используйте TLS, reverse proxy, аутентификацию, авторизацию, firewall или внутреннюю сеть, разграничение ролей Grafana и секрет-хранилище для credentials.

Пример защищённого клиента:

clients:
  - url: https://logs.example.com/loki/api/v1/push
    basic_auth:
      username: promtail
      password_file: /run/secrets/loki_password
    tls_config:
      ca_file: /etc/promtail/ca.crt

Агенту нужны только права чтения журналов и отправки данных. Не следует использовать --privileged без обоснования.


Практический пример

Структура проекта:

logging-stack/
├── compose.yml
├── loki-config.yml
├── promtail-config.yml
└── logs/app.log

compose.yml

services:
  loki:
    image: grafana/loki:3
    command: -config.file=/etc/loki/loki-config.yml
    volumes:
      - ./loki-config.yml:/etc/loki/loki-config.yml:ro
      - loki-data:/loki
    ports:
      - "127.0.0.1:3100:3100"

  promtail:
    image: grafana/promtail:3
    command: -config.file=/etc/promtail/promtail-config.yml
    volumes:
      - ./promtail-config.yml:/etc/promtail/promtail-config.yml:ro
      - ./logs:/var/log/app:ro
      - promtail-positions:/var/lib/promtail
    depends_on:
      - loki

  grafana:
    image: grafana/grafana:11
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "127.0.0.1:3000:3000"
    depends_on:
      - loki

volumes:
  loki-data:
  promtail-positions:
  grafana-data:

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:
    instance_addr: 127.0.0.1
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

Конфигурация предназначена для учебного стенда. В production нужны защищённый доступ, retention и подходящее долговременное хранилище.

promtail-config.yml

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /var/lib/promtail/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: demo
    static_configs:
      - targets:
          - localhost
        labels:
          job: demo
          service: orders
          environment: development
          __path__: /var/log/app/*.log
    pipeline_stages:
      - json:
          expressions:
            timestamp: timestamp
            level: level
      - timestamp:
          source: timestamp
          format: RFC3339
      - labels:
          level:

logs/app.log

{"timestamp":"2026-09-21T14:30:00Z","level":"info","request_id":"req-001","duration_ms":120,"status":200,"message":"order list returned"}
{"timestamp":"2026-09-21T14:31:00Z","level":"warn","request_id":"req-002","duration_ms":950,"status":200,"message":"slow database query"}
{"timestamp":"2026-09-21T14:32:00Z","level":"error","request_id":"req-003","duration_ms":2500,"status":500,"message":"database timeout"}

Запуск:

docker compose up -d
docker compose ps
curl http://localhost:3100/ready

Grafana будет доступна на http://localhost:3000. В качестве Loki data source укажите http://loki:3100.

Проверочные запросы:

{service="orders", environment="development"}
{service="orders", environment="development"} | json | level="error"
{service="orders", environment="development"} | json | duration_ms > 500

Диагностика и best practices

Loki не готов

curl http://localhost:3100/ready
docker compose logs loki

Проверяйте YAML, права на volume, заполнение диска, схему хранения и доступность object storage.

Promtail не отправляет записи

docker compose logs promtail

Проверьте путь __path__, права чтения, доступность Loki, positions file и pipeline stages. Убедитесь, что временные метки не слишком далеко от текущего времени.

В Grafana нет данных

Проверьте URL data source, Docker-сеть, диапазон времени и labels. Начните с запроса:

{job=~".+"}

Затем постепенно сужайте его.

Повторяющиеся записи

Причинами могут быть потеря positions file, два агента для одного файла, одновременная отправка через агент и logging driver или неправильная обработка ротации файлов.

Медленные запросы

Уменьшите диапазон времени, добавьте точные labels, примените ранний |=, избегайте избыточных regexp, проверьте кардинальность и вынесите повторяющиеся агрегации в recording rules.

Best practices

Краткая шпаргалка LogQL

{service="orders"}
{service="orders"} |= "error"
{service="orders"} | json | level="error"
{service="orders"} | json | duration_ms > 1000
count_over_time({service="orders"}[5m])
sum by (service) (rate({environment="production"} |= "error" [5m]))
sum by (service) (bytes_rate({environment="production"}[5m]))
{service="orders"} | json | __error__ != ""

Итог

Loki строит централизованное логирование вокруг потоков и labels. Promtail обнаруживает источники, обрабатывает записи и отправляет их в Loki. LogQL используется для поиска, разбора JSON и вычисления метрик из логов. Grafana связывает логи с метриками и трассировками.

Главные правила: использовать структурированные записи, не помещать секреты в логи, контролировать кардинальность labels, хранить идентификаторы корреляции внутри событий, защищать хранилище и мониторить весь конвейер доставки.