50 микросервисов и ни одного лога: внедряем Grafana Loki для стримингового сервиса

Задача клиента: стриминговый сервис тонет в логах

В начале 2026 года к нам обратилась команда онлайн-кинотеатра StreamVibe из Москвы. У компании было 50 микросервисов, развёрнутых в Kubernetes: транскодирование видео, рекомендательная система, биллинг, CDN-оркестрация, пользовательские профили и ещё десятки внутренних сервисов. Ежедневная аудитория — 400 000 пользователей, пиковая нагрузка во время премьер — до 80 000 одновременных сессий.

Проблема была в том, что логи жили только внутри контейнеров. При каждом перезапуске пода (а это происходило десятки раз в день при автоскейлинге) все логи безвозвратно терялись. Команда из 8 разработчиков тратила до 3 часов в день на ручной поиск ошибок, подключаясь к нодам по SSH и перебирая kubectl logs. Два крупных инцидента — падение биллинга и сбой транскодера — так и не удалось полноценно расследовать из-за отсутствия исторических логов.

«Мы как слепые котята. Сервис падает, а мы даже не можем посмотреть, что происходило за минуту до сбоя» — технический директор StreamVibe.

Клиент рассматривал ELK Stack, но после оценки стоимости — кластер Elasticsearch на 3 ноды по 64 ГБ RAM — решил искать более лёгкую альтернативу. Специалисты АйТи Фреш предложили Grafana Loki — систему агрегации логов, которая индексирует только метаданные (labels), а не содержимое, что кратно снижает требования к ресурсам.

Почему Loki, а не ELK Stack

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

КритерийELK StackGrafana Loki
ИндексацияПолнотекстовая (каждое слово)Только labels (namespace, pod, container)
RAM на ноду32–64 ГБ4–8 ГБ
Хранилище 30 дней (50 сервисов)~2 ТБ SSD~300 ГБ (S3-совместимое)
Сложность кластера3+ ноды Elasticsearch1 read + 1 write + S3
Интеграция с GrafanaЧерез плагинНативная
Стоимость инфраструктуры/мес~120 000 ₽~25 000 ₽

Для стримингового сервиса, где нужно быстро фильтровать логи по сервису и уровню ошибки, а не искать произвольный текст в миллионах строк, Loki подходил идеально. Решение утвердили за 2 дня.

Архитектура решения

Мы спроектировали следующую архитектуру:

  • Promtail — агент сбора логов, развёрнутый как DaemonSet на каждой ноде Kubernetes
  • Grafana Loki — бэкенд в режиме Simple Scalable (read + write + backend), развёрнутый через Helm
  • MinIO — S3-совместимое хранилище для чанков и индексов (у клиента уже был кластер)
  • Grafana — визуализация, дашборды, алертинг

Весь стек был развёрнут внутри того же Kubernetes-кластера, что минимизировало latency между Promtail и Loki.

Установка Grafana Loki в Kubernetes через Helm

Для продакшн-инсталляции мы выбрали официальный Helm-чарт grafana/loki в режиме Simple Scalable Deployment (SSD). Этот режим разделяет Loki на три компонента — read, write и backend — что позволяет масштабировать каждый независимо.

Подготовка namespace и Helm-репозитория

# Создаём выделенный namespace для стека мониторинга
kubectl create namespace monitoring

# Добавляем Helm-репозиторий Grafana
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# Проверяем доступные версии чарта
helm search repo grafana/loki --versions | head -10

На момент внедрения актуальной была версия чарта 6.6.2 с Loki 3.0.

Конфигурация values.yaml для Loki

Наши инженеры подготовили кастомный values.yaml с учётом нагрузки клиента — 50 сервисов генерировали около 15 ГБ логов в сутки:

# values-loki.yaml
loki:
  schemaConfig:
    configs:
      - from: 2024-04-01
        store: tsdb
        object_store: s3
        schema: v13
        index:
          prefix: loki_index_
          period: 24h
  storage:
    type: s3
    s3:
      endpoint: minio.storage.svc.cluster.local:9000
      bucketnames: loki-chunks
      access_key_id: ${MINIO_ACCESS_KEY}
      secret_access_key: ${MINIO_SECRET_KEY}
      s3ForcePathStyle: true
      insecure: true
  commonConfig:
    replication_factor: 2
  limits_config:
    ingestion_rate_mb: 16
    ingestion_burst_size_mb: 32
    max_entries_limit_per_query: 10000
    retention_period: 720h   # 30 дней
  compactor:
    retention_enabled: true
    retention_delete_delay: 2h
    compaction_interval: 10m

write:
  replicas: 2
  resources:
    requests:
      cpu: 500m
      memory: 1Gi
    limits:
      cpu: "2"
      memory: 4Gi
  persistence:
    size: 20Gi
    storageClass: fast-ssd

read:
  replicas: 2
  resources:
    requests:
      cpu: 500m
      memory: 1Gi
    limits:
      cpu: "2"
      memory: 4Gi

backend:
  replicas: 1
  resources:
    requests:
      cpu: 250m
      memory: 512Mi
    limits:
      cpu: "1"
      memory: 2Gi

gateway:
  replicas: 2
  ingress:
    enabled: true
    hosts:
      - host: loki.internal.streamvibe.ru
        paths:
          - path: /
            pathType: Prefix

Ключевые параметры, которые мы настроили:

  • schema v13 + TSDB store — новейший формат хранения, на 40% эффективнее по дисковому вводу/выводу по сравнению с boltdb-shipper
  • replication_factor: 2 — каждый чанк хранится в двух экземплярах для отказоустойчивости
  • retention_period: 720h — автоматическое удаление логов старше 30 дней
  • ingestion_rate_mb: 16 — ограничение скорости приёма для защиты от лог-штормов

Деплой и проверка

# Деплой Loki
helm upgrade --install loki grafana/loki \
  -n monitoring \
  -f values-loki.yaml \
  --version 6.6.2

# Проверяем статус подов
kubectl -n monitoring get pods -l app.kubernetes.io/name=loki

# Ожидаемый вывод:
# NAME                           READY   STATUS    RESTARTS   AGE
# loki-backend-0                 1/1     Running   0          2m
# loki-read-0                    1/1     Running   0          2m
# loki-read-1                    1/1     Running   0          2m
# loki-write-0                   1/1     Running   0          2m
# loki-write-1                   1/1     Running   0          2m
# loki-gateway-7d8f9b6c4-x2k9l  1/1     Running   0          2m
# loki-gateway-7d8f9b6c4-m4n7p  1/1     Running   0          2m

# Проверяем health-эндпоинт
kubectl -n monitoring port-forward svc/loki-gateway 3100:80 &
curl -s http://localhost:3100/ready
# ready

Настройка Promtail: сбор логов со всех контейнеров

Promtail — это агент, который читает логи контейнеров с файловой системы ноды и отправляет их в Loki. В Kubernetes он разворачивается как DaemonSet, чтобы на каждой ноде работал ровно один экземпляр.

Деплой Promtail через Helm

# values-promtail.yaml
config:
  clients:
    - url: http://loki-gateway.monitoring.svc.cluster.local/loki/api/v1/push
      tenant_id: streamvibe
      batchwait: 1s
      batchsize: 1048576  # 1 МБ
  snippets:
    pipelineStages:
      - cri: {}
      - multiline:
          firstline: '^\d{4}-\d{2}-\d{2}|^\[\d{4}|^{"timestamp"'
          max_wait_time: 3s
          max_lines: 128
      - json:
          expressions:
            level: level
            service: service
            trace_id: trace_id
      - labels:
          level:
          service:
      - timestamp:
          source: timestamp
          format: RFC3339Nano
      - output:
          source: message

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

tolerations:
  - effect: NoSchedule
    operator: Exists
# Деплой Promtail
helm upgrade --install promtail grafana/promtail \
  -n monitoring \
  -f values-promtail.yaml \
  --version 6.16.4

# Проверяем, что Promtail запущен на всех нодах
kubectl -n monitoring get pods -l app.kubernetes.io/name=promtail -o wide

# Проверяем targets — какие контейнеры обнаружены
kubectl -n monitoring port-forward ds/promtail 3101:3101 &
curl -s http://localhost:3101/targets | jq '.[] | length'
# 127  (все контейнеры всех подов на ноде)

Pipeline stages: парсинг JSON-логов микросервисов

Микросервисы StreamVibe использовали структурированный JSON-формат логирования. Пример строки лога из сервиса транскодера:

{"timestamp":"2026-02-15T14:32:01.443Z","level":"error","service":"transcoder","trace_id":"abc123def456","message":"FFmpeg process exited with code 137","video_id":"v-98234","codec":"h265","duration_ms":45200}

Наши pipeline stages выполняли следующую обработку:

  1. cri — парсинг CRI-формата контейнерных логов (Kubernetes оборачивает каждую строку в свой формат)
  2. multiline — склейка многострочных стектрейсов в одну запись
  3. json — извлечение полей level, service, trace_id из JSON
  4. labels — промоутинг level и service в labels Loki для быстрой фильтрации
  5. timestamp — использование таймстампа из лога вместо времени приёма
  6. output — поле message становится основным текстом лог-записи

Такая обработка позволяла Loki индексировать логи по labels {namespace="production", pod="transcoder-7b8c9d-x2k", level="error", service="transcoder"}, а полнотекстовый поиск использовать только при необходимости.

Relabeling: обогащение метаданными Kubernetes

Помимо парсинга содержимого, Promtail автоматически обогащает каждую запись метаданными Kubernetes через service discovery. Мы дополнительно настроили relabeling для удобства фильтрации:

# Дополнение к values-promtail.yaml
config:
  snippets:
    extraRelabelConfigs:
      # Добавляем label с именем deployment
      - source_labels: [__meta_kubernetes_pod_label_app]
        target_label: app
      # Добавляем label с версией
      - source_labels: [__meta_kubernetes_pod_label_version]
        target_label: version
      # Отбрасываем системные контейнеры istio-proxy
      - source_labels: [__meta_kubernetes_container_name]
        regex: istio-proxy
        action: drop

Это позволило фильтровать логи по деплойменту и версии приложения, а также исключить шумные sidecar-контейнеры сервисной сетки.

LogQL: язык запросов для расследования инцидентов

LogQL — это язык запросов Loki, синтаксически близкий к PromQL. Наши инженеры подготовили для команды StreamVibe набор готовых запросов для типовых задач.

Базовые запросы: фильтрация по labels и тексту

# Все ошибки транскодера за последний час
{namespace="production", service="transcoder", level="error"}

# Поиск по конкретному видео ID
{namespace="production", service="transcoder"} |= "v-98234"

# Regex-поиск: все HTTP-ошибки 5xx в API-gateway
{namespace="production", app="api-gateway"} |~ "HTTP/1.1\" 5[0-9]{2}"

# Исключение шумных health-check логов
{namespace="production", app="api-gateway"} != "/healthz" != "/readyz"

# Поиск по trace_id для сквозной трассировки
{namespace="production"} |= "abc123def456"

Важная особенность LogQL: сначала Loki фильтрует по labels (это мгновенно, так как labels индексированы), а затем применяет текстовые фильтры к содержимому. Поэтому чем точнее labels, тем быстрее запрос.

Метрические запросы: rate, count, quantile

# Частота ошибок по сервисам за последние 5 минут (errors per second)
sum by(service) (
  rate({namespace="production", level="error"} [5m])
)

# Топ-5 самых шумных сервисов (строк логов в секунду)
topk(5,
  sum by(app) (
    rate({namespace="production"} [5m])
  )
)

# Процент ошибок от общего числа записей для биллинга
sum(rate({service="billing", level="error"} [15m]))
/
sum(rate({service="billing"} [15m]))
* 100

# Количество уникальных ошибок за час (по первым 100 символам)
count_over_time(
  {namespace="production", level="error"}
    | line_format "{{.message | trunc 100}}"
  [1h]
)

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

JSON-парсинг прямо в запросе

# Извлечение поля duration_ms из JSON и фильтрация медленных операций
{service="transcoder"}
  | json
  | duration_ms > 30000
  | line_format "video={{.video_id}} codec={{.codec}} duration={{.duration_ms}}ms"

# Статистика по кодекам: среднее время транскодирования
avg by(codec) (
  {service="transcoder"}
    | json
    | unwrap duration_ms [5m]
)

# Подсчёт ошибок по HTTP-статусам
sum by(status) (
  count_over_time(
    {app="api-gateway"}
      | json
      | status >= 400
    [1h]
  )
)

JSON-парсинг выполняется в query-time, что медленнее, чем pre-indexed labels, но позволяет анализировать любые поля без изменения конфигурации Promtail.

Дашборды Grafana: от общей картины до деталей

Визуализация — ключевая часть системы мониторинга. Мы создали 4 дашборда, покрывающих все потребности команды: обзорный, посервисный, для расследования инцидентов и для мониторинга самого Loki.

Обзорный дашборд: здоровье платформы

Основной дашборд включал следующие панели:

  • Error Rate Heatmap — тепловая карта частоты ошибок по сервисам, позволяющая мгновенно увидеть аномалии
  • Log Volume — общий объём логов по уровням (info/warn/error/fatal)
  • Top Errors — таблица с самыми частыми ошибками за последний час
  • Deployment Events — аннотации на графиках с моментами деплоев

Пример JSON-модели панели Error Rate:

{
  "type": "timeseries",
  "title": "Error Rate by Service",
  "datasource": { "type": "loki", "uid": "loki-ds" },
  "targets": [
    {
      "expr": "sum by(service) (rate({namespace=\"production\", level=\"error\"} [$__interval]))",
      "legendFormat": "{{service}}"
    }
  ],
  "fieldConfig": {
    "defaults": {
      "unit": "reqps",
      "thresholds": {
        "steps": [
          { "color": "green", "value": null },
          { "color": "yellow", "value": 0.5 },
          { "color": "red", "value": 2 }
        ]
      }
    }
  }
}

Дашборд расследования инцидентов

Для удобства on-call инженеров мы создали интерактивный дашборд с переменными:

  • $service — выпадающий список сервисов (автозаполнение из labels Loki)
  • $level — уровень логирования
  • $search — произвольный текстовый поиск
  • $trace_id — для сквозного поиска по trace ID

Определение переменных в Grafana:

# Variable: service (type: Query)
# Data source: Loki
# Query: label_values({namespace="production"}, service)
# Refresh: On Dashboard Load

# Variable: search (type: Text box)
# Default: (empty)

# Variable: trace_id (type: Text box)
# Default: (empty)

Основная панель с логами использовала динамический LogQL-запрос:

{namespace="production", service=~"$service", level=~"$level"}
  |= "$search"
  |= "$trace_id"

Инженер мог выбрать сервис, уровень, ввести поисковую строку — и мгновенно увидеть отфильтрованный поток логов с возможностью раскрыть каждую запись для просмотра полного JSON.

Мониторинг здоровья самого Loki

Отдельный дашборд отслеживал состояние компонентов Loki — чтобы система мониторинга сама не стала слепым пятном:

# Задержка приёма логов (ingestion lag)
histogram_quantile(0.99,
  sum by(le) (
    rate(loki_distributor_ingester_append_latency_seconds_bucket[5m])
  )
)

# Скорость записи чанков в S3
sum(rate(loki_ingester_chunks_flushed_total[5m]))

# Ошибки записи
sum by(reason) (
  rate(loki_ingester_chunks_flush_failures_total[5m])
)

# Использование памяти ingester-ами
sum by(pod) (
  container_memory_working_set_bytes{namespace="monitoring", container="loki"}
) / 1024 / 1024 / 1024

Алертинг: уведомления об ошибках в Telegram и PagerDuty

Дашборды хороши для расследований, но критические ошибки должны приходить сами — в мессенджер или систему дежурств. Мы настроили двухуровневую систему алертинга.

Alerting Rules в Grafana

Мы создали набор alert rules прямо в Grafana (начиная с версии 9.x Grafana имеет встроенную систему алертинга):

# grafana-alerts.yaml (provisioning)
apiVersion: 1
groups:
  - orgId: 1
    name: StreamVibe Production Alerts
    folder: Production
    interval: 1m
    rules:
      - uid: error-rate-spike
        title: "High Error Rate"
        condition: C
        data:
          - refId: A
            datasourceUid: loki-ds
            model:
              expr: |
                sum by(service) (
                  rate({namespace="production", level="error"} [5m])
                )
          - refId: B
            datasourceUid: __expr__
            model:
              type: reduce
              reducer: max
              expression: A
          - refId: C
            datasourceUid: __expr__
            model:
              type: threshold
              expression: B
              conditions:
                - evaluator:
                    type: gt
                    params: [5]
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "Error rate > 5/sec in {{ $labels.service }}"
          description: "Service {{ $labels.service }} has error rate {{ $values.B }} errors/sec for 3+ minutes"

      - uid: log-volume-drop
        title: "Log Volume Drop (possible outage)"
        condition: C
        data:
          - refId: A
            datasourceUid: loki-ds
            model:
              expr: |
                sum(rate({namespace="production"} [5m]))
          - refId: B
            datasourceUid: __expr__
            model:
              type: reduce
              reducer: last
              expression: A
          - refId: C
            datasourceUid: __expr__
            model:
              type: threshold
              expression: B
              conditions:
                - evaluator:
                    type: lt
                    params: [10]
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Log volume dropped below 10 lines/sec"
          description: "Total log ingest rate is {{ $values.B }}/sec — possible service outage"

Contact Points: Telegram и PagerDuty

Для routing алертов мы настроили два канала:

# grafana-contact-points.yaml
apiVersion: 1
contactPoints:
  - orgId: 1
    name: telegram-oncall
    receivers:
      - uid: telegram-1
        type: telegram
        settings:
          bottoken: "$TELEGRAM_BOT_TOKEN"
          chatid: "-1001234567890"
          parse_mode: HTML
          message: |
            🔥 {{ .CommonLabels.alertname }}
            Severity: {{ .CommonLabels.severity }}
            Service: {{ .CommonLabels.service }}
            {{ .CommonAnnotations.description }}
  - orgId: 1
    name: pagerduty-critical
    receivers:
      - uid: pd-1
        type: pagerduty
        settings:
          integrationKey: "$PAGERDUTY_KEY"
          severity: "{{ .CommonLabels.severity }}"
# grafana-notification-policies.yaml
apiVersion: 1
policies:
  - orgId: 1
    receiver: telegram-oncall
    group_by: ["alertname", "service"]
    group_wait: 30s
    group_interval: 5m
    repeat_interval: 4h
    routes:
      - receiver: pagerduty-critical
        matchers:
          - severity = critical
        continue: true

Логика маршрутизации: все алерты идут в Telegram-чат дежурной команды. Критические — дополнительно в PagerDuty для автоматического вызова on-call инженера.

Политики ретеншена и оптимизация хранения

При генерации 15 ГБ логов в сутки хранение без ограничений быстро заполнит любой диск. Мы настроили гибкую политику ретеншена с разными сроками для разных типов логов.

Per-tenant и per-stream retention

# Дополнение к loki config (values-loki.yaml)
loki:
  limits_config:
    retention_period: 720h  # 30 дней по умолчанию
    per_stream_rate_limit: 5MB
    per_stream_rate_limit_burst: 15MB
  compactor:
    retention_enabled: true
    retention_delete_delay: 2h
    delete_request_store: s3
  overrides:
    # Для tenant "streamvibe" кастомные лимиты
    streamvibe:
      retention_period: 720h
      retention_stream:
        - selector: '{level="debug"}'
          priority: 1
          period: 72h        # Debug-логи: 3 дня
        - selector: '{level="info"}'
          priority: 2
          period: 168h       # Info: 7 дней
        - selector: '{level="error"}'
          priority: 3
          period: 2160h      # Ошибки: 90 дней
        - selector: '{service="billing"}'
          priority: 4
          period: 4320h      # Биллинг: 180 дней (compliance)

Такая стратегия позволила сократить объём хранилища на 60% — debug-логи (составлявшие 70% объёма) удалялись через 3 дня, а критически важные ошибки биллинга хранились полгода для аудита.

Мониторинг использования хранилища

Для контроля за ростом хранилища мы добавили метрики в дашборд Loki Health:

# Общий объём данных в S3
sum(loki_store_series_total)

# Скорость роста хранилища (ГБ/день)
sum(increase(loki_ingester_bytes_received_total[24h])) / 1024 / 1024 / 1024

# Эффективность компактора: удалено чанков
sum(increase(loki_compactor_deleted_series_total[24h]))

Дополнительно мы настроили алерт на случай, если хранилище MinIO заполнится более чем на 80%, с автоматическим уведомлением в Telegram инфраструктурной команды.

Результаты внедрения

Внедрение Grafana Loki заняло 8 рабочих дней. Вот измеримые результаты, которые зафиксировала команда StreamVibe через месяц работы:

  • Время расследования инцидента сократилось с 3 часов до 15 минут — благодаря централизованным логам с поиском по trace_id
  • MTTR (Mean Time To Resolve) уменьшился на 74% — с 47 минут до 12 минут
  • Стоимость инфраструктуры мониторинга — 25 000 ₽/мес вместо 120 000 ₽/мес на ELK Stack
  • 0 потерянных логов за весь период — Promtail с WAL гарантирует доставку даже при перезапуске
  • Покрытие — 100% микросервисов и 100% окружений (production, staging, dev)
  • Proactive alerting — 12 алертов за первый месяц позволили предотвратить 3 потенциальных инцидента до того, как их заметили пользователи

Специалисты АйТи Фреш также провели обучающий семинар для команды разработки: 2 часа по LogQL и работе с дашбордами. Полная документация по инфраструктуре и runbook по типовым задачам были переданы клиенту.

«Теперь мы видим всё. Loki стал нашим вторым глазом. Инцидент, который раньше расследовали полдня, теперь решается за 15 минут прямо из Grafana» — CTO StreamVibe.

Если ваша команда тонет в логах микросервисов или тратит часы на ручной разбор инцидентов — обращайтесь в АйТи Фреш. Мы внедрим централизованный сбор и анализ логов, настроим алертинг и обучим вашу команду.

Часто задаваемые вопросы

Главное отличие — Loki индексирует только метаданные (labels), а не полное содержимое логов, как Elasticsearch. Это снижает требования к RAM в 5–8 раз и позволяет хранить логи в дешёвом объектном хранилище (S3/MinIO). ELK лучше для полнотекстового поиска по произвольным полям, Loki — для фильтрации по known labels с эпизодическим текстовым поиском.

Для нагрузки в 15 ГБ логов/сутки мы использовали: 2 write-реплики по 4 ГБ RAM, 2 read-реплики по 4 ГБ RAM, 1 backend на 2 ГБ RAM. Итого ~14 ГБ RAM. Для сравнения, аналогичный Elasticsearch-кластер потребовал бы 96–192 ГБ RAM.

Да. Loki работает на обычных серверах — в режиме single binary или через docker-compose. Для сбора логов вместо Promtail можно использовать Grafana Alloy, Fluentd или Fluent Bit с плагином Loki. Мы устанавливали Loki как на bare-metal, так и в Docker-окружениях.

Срок хранения настраивается параметром retention_period — от часов до лет. Loki 3.0 поддерживает per-stream retention: разные сроки для разных типов логов. Например, debug-логи — 3 дня, ошибки — 90 дней, логи биллинга — 180 дней для комплаенса.

Нет. Promtail использует WAL (Write-Ahead Log) и хранит позицию чтения в файле positions.yaml. При перезапуске агент продолжает чтение с того места, где остановился. Если Loki временно недоступен, Promtail буферизирует данные локально.

Нужна помощь с настройкой?

Специалисты АйТи Фреш помогут с внедрением и настройкой — 15+ лет опыта, обслуживание от 15 000 ₽/мес

📞 Связаться с нами
#Grafana Loki#сбор логов#Promtail настройка#LogQL запросы#агрегация логов микросервисов#Grafana дашборды логов#мониторинг контейнеров#Loki vs ELK