ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

Loki без кардинальности боли: пайплайны, парсинг и контроль меток

Кардинальность меток в Loki — ключевая боль: множатся потоки, тормозят запросы, растут счета. Разбираем на практике, как проектировать метки, собирать логи через Promtail, строить пайплайны парсинга и держать кардинальность под контролем.
Loki без кардинальности боли: пайплайны, парсинг и контроль меток

Когда Loki только внедряют, его любят за дешёвое хранение, быстрые запросы и знакомую философию меток. Но именно метки чаще всего «стреляют в ногу»: один невинный user_id или request_id в лейблах — и вы получаете взрыв кардинальности, миллионы потоков, длинные внутренние списки и медленные запросы. В этой статье разберёмся, как проектировать метки, какие поля держать в теле логов, как строить пайплайны Promtail для безопасного парсинга и редактирования, и как проверять кардинальность до того, как она станет проблемой.

Как Loki мыслит: потоки, метки и их цена

В Loki каждая уникальная комбинация меток определяет «поток» (stream). Слишком подробные метки множат потоки, повышают накладные расходы на индексацию и ухудшают производительность запросов. Поэтому основная стратегия — держать в метках только низкокардинальные и стабильные признаки, а всё изменчивое и уникальное оставлять в теле сообщения и разбирать на этапе запроса.

Безопасные примеры для меток: job, env, service, instance, region, укрупнённый status (например, HTTP-код), тип события. Опасные примеры: user_id, request_id, session, сырой URL, UUID, IP-адрес целиком, путь с динамическими сегментами.

Жёсткое правило: метки — это измерения для агрегации; всё, что уникально на каждую запись, — оставляйте в тексте и разбирайте на запросе.

Пайплайны Promtail: контур управления кардинальностью

Promtail — первый рубеж. Именно здесь удобно нормализовать сообщения, вырезать шум/секреты, извлекать поля и выборочно повышать их до меток. При этом важно «поднимать» в метки только те поля, которые действительно стабильны и полезны в агрегировании.

Минимальный скелет конфига Promtail с пайплайном:

scrape_configs:
- job_name: apps
  static_configs:
  - targets: [localhost]
    labels:
      job: apps
      env: prod
      __path__: /var/log/apps/*.log
  pipeline_stages:
  - json:
      expressions:
        ts: time
        level: level
        msg: message
        path: http.path
        status: http.status
        ip: client_ip
  - labels:
      level:
      status:
  - template:
      source: path
      template: '{{ RegexReplaceAll "[0-9a-fA-F-]{8,}" ":id" .Value }}'
  - output:
      source: msg
  • json извлекает поля из структурированного сообщения.
  • labels поднимает только низкокардинальные метки — уровень и HTTP-статус.
  • template нормализует путь, заменяя любые похожие на ID сегменты на :id. Это оставит сырой уникальный путь в тексте безопасным для дальнейшего разбора на запросе.
  • output назначает «чистое» поле сообщения, которое попадёт в Loki.

Пример пайплайна Promtail с нормализацией и безопасными метками

Парсинг: JSON, logfmt и регулярки без боли

Если ваши логи уже структурированы (JSON, logfmt), используйте соответствующие стадии. Это быстрее и стабильнее, чем универсальные регулярки.

JSON

pipeline_stages:
- json:
    expressions:
      level: level
      msg: message
      status: http.status
      route: http.route
- labels:
    level:
    status:

Храните в метках только level и укрупнённый status. Поле http.route часто содержит динамику — оставьте его в сообщении или нормализуйте.

logfmt

pipeline_stages:
- logfmt:
- labels:
    level:
    status:

Стадия logfmt сама распознаёт пары ключ=значение. Выбирайте, что поднимать в метки. Избыточное количество имён в метках увеличит «ширину» потока.

Regex: только для нормализации и извлечения

Регулярки используйте точечно: вырезать/обезличить, выделить несколько ключей. Не пытайтесь ими «разобрать всё» — это и медленнее, и ломается чаще при изменениях формата.

pipeline_stages:
- regex:
    expression: 'path=(?P<path>\S+) status=(?P<status>\d+)'
- template:
    source: path
    template: '{{ RegexReplaceAll "/[0-9]+" "/:num" .Value }}'
- labels:
    status:

Если у вас много Nginx-доступов, подумайте и про IO профилирование логов и кешей — см. разбор временных каталогов Nginx и tmpfs для IO.

Ветвления, отбрасывание шума и редактирование

Условные пайплайны позволяют чисто разделять обработку разных типов сообщений в одном файле.

pipeline_stages:
- match:
    selector: '{job="apps"}'
    stages:
    - match:
        selector: '{level="debug"}'
        action: drop
    - json:
        expressions:
          level: level
          msg: message
          status: http.status
    - labels:
        level:
        status:
    - drop:
        source: msg
        expression: "^healthcheck$"

Стадия drop полезна для шумных healthcheck-строк, а ветвление по селектору — чтобы применять разные парсеры к разным источникам.

Для маскировки секретов используйте replace:

pipeline_stages:
- replace:
    expression: 'password=\S+'
    replace: 'password=***'

Контроль меток до отправки: relabel_configs

Помимо пайплайнов, в Promtail есть relabel_configs — мощный слой для манипуляций с метками ещё до попадания в пайплайн или после него (в зависимости от места определения). Это лучший способ отрезать лишнее, переименовать и привести набор меток к «бюджету» кардинальности.

scrape_configs:
- job_name: apps
  static_configs:
  - targets: [localhost]
    labels:
      job: apps
      env: prod
      __path__: /var/log/apps/*.log
  relabel_configs:
  - action: labeldrop
    regex: (filename|stream|container_id)
  - action: replace
    source_labels: [env]
    target_label: environment
  - action: labelkeep
    regex: (job|environment|instance)

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

Ограничения на стороне Loki

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

limits_config:
  max_streams_per_user: 50000
  ingestion_rate_mb: 4
  ingestion_burst_size_mb: 8
  max_line_size: 256kb
  reject_old_samples: true
  reject_old_samples_max_age: 168h

Ключевая настройка — max_streams_per_user: если кто-то начнёт генерировать уникальные метки на каждый запрос, инжест сразу остановится с ошибкой, вместо того чтобы тихо «съесть» всё место и ресурсы. max_line_size помогает отсекать случайно зашедшие дампы или трейс-лупы.

Если разворачиваете Loki самостоятельно, удобнее разместить его на изолированном узле с гарантированным CPU/IO — подойдёт VDS с SSD и достаточным IOPS. Так проще выдерживать пики инжеста и хранение индекса.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Проверяем кардинальность до продакшена

Главная техника: не поднимайте поле в метки, пока не проверите его будущую кардинальность с помощью запросного парсинга. В LogQL можно парсить на лету и агрегировать как будто это метка — без изменения хранения.

JSON-пример

sum by (status) (
  count_over_time(( {job="apps"} | json | status!="" )[5m])
)

Так вы увидите распределение по status без поднятия этого поля в лейблы. Если оно стабильное (например, 200/404/500), можно смело делать из него метку.

Парсинг Nginx-доступов

sum by (status) (
  count_over_time(( {job="nginx"}
    | pattern `<ip> - - [<ts>] "<method> <path> HTTP/<proto>" <status> <size>`
  )[1m])
)

Статус — хорошая метка. А вот path почти всегда слишком детализирован. Можно заранее нормализовать путь в пайплайне или нормализовать в шаблоне запроса и агрегировать по укрупнённой форме:

sum by (norm_path) (
  count_over_time(( {job="nginx"}
    | pattern `<ip> - - [<ts>] "<method> <path> HTTP/<proto>" <status> <size>`
    | line_format "{{ RegexReplaceAll \\"/v[0-9]+\\" \\"/vX\\" .path }}"
    | regexp `(?P<norm_path>[^?]+)`
  )[5m])
)

Здесь мы вместо метки используем временные поля на стороне запроса. Если распределение по norm_path устойчивое и ограниченное, можно рассмотреть поднятие в постоянную метку (но только если это действительно нужно для запросов).

Многострочные логи и трейс-дампы

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

pipeline_stages:
- multiline:
    firstline: '^[0-9T:.+-]+'
    max_lines: 500
    max_wait_time: 3s

Сигнатуру firstline подберите под формат вашего времени/префикса. Ограничивайте max_lines и max_wait_time, чтобы не блокировать пайплайн при «обрывах».

Производительность Promtail: очереди и батчи

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

clients:
- url: http://loki:3100/loki/api/v1/push
  batchwait: 1s
  batchsize: 1048576
  max_retries: 10
  backoff_config:
    min_period: 500ms
    max_period: 5s
    max_retries: 8

Слишком маленькие батчи породят лишние HTTP-запросы, слишком большие — задержки и буферизацию. Балансируйте под свой поток логов.

systemd, syslog и контейнеры

Journald

scrape_configs:
- job_name: systemd
  journal:
    json: true
    max_age: 12h
    labels:
      job: systemd
  relabel_configs:
  - action: labelkeep
    regex: (job|env|unit|priority)

В journald много служебных полей, не все стоит поднимать. Оставляйте только действительно полезные для агрегации. Детали по inotify и путям удобно сверить со статьёй inotify и пути systemd в CI.

Контейнеры

Автообнаружение в Kubernetes или Docker часто приносит десятки технических меток. Настройте белые списки и нормализацию: применяйте labelkeep, переименовывайте в один общий service, отбрасывайте container_id, если он не нужен для агрегаций.

Чек-лист проектирования меток и пайплайнов

  • Определите «бюджет» кардинальности: какие метки точно нужны для основных дашбордов и оповещений.
  • Не поднимайте в метки поля, чья кардинальность > O(10^2)–O(10^3) для вашего масштаба.
  • Сначала используйте запросный парсинг (| json, | logfmt, | regexp, | pattern) и агрегируйте; только потом — постоянные метки.
  • Нормализуйте пути и ID в пайплайнах (template с RegexReplaceAll), оставляйте сырьё в теле.
  • Отбрасывайте шум (drop по уровню, healthcheck-пути, тестовые префиксы).
  • Используйте relabel_configs для белых списков меток и удаления лишнего.
  • Включите защитные лимиты в Loki: max_streams_per_user, max_line_size, лимиты инжеста.
  • Проверяйте пайплайны на стейджинге с нагрузкой, смотрите число потоков и распределение меток.

Диагностика: какие метки «распухают»

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

topk(20, count by (route) (
  count_over_time(( {job="apps"} | json | route!="" )[15m])
))

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

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

Типовые анти‑паттерны и их исправления

  • Метка request_id или user_id. Исправление: оставить в теле, при необходимости — нормализовать и использовать на запросе.
  • Сырой path как метка. Исправление: шаблонизировать в template или нормализовать на запросе, хранить сырой путь в теле.
  • Поднятие десятков имён меток из logfmt/JSON «на всякий случай». Исправление: белые списки в labels и relabel_configs.
  • Отсутствие лимитов. Исправление: задать лимиты Loki и алерты на рост числа потоков.

Заключение

Loki отлично справляется с большими объёмами логов, если держать под контролем главное — кардинальность меток. Стройте пайплайны Promtail вокруг идеи «метки только для стабильных измерений», всё остальное — в теле. Нормализуйте, маскируйте, отбрасывайте шум до отправки. Проверяйте идею новой метки через запросный парсинг, и лишь затем поднимайте её в схему. Включайте ограничители в Loki, следите за количеством потоков и используйте запросные шаблоны для безопасных экспериментов. Так вы получите быстрые запросы, предсказуемые ресурсы и спокойные ночи админа.

Поделиться статьей

Вам будет интересно

Let’s Encrypt wildcard через DNS-01: acme.sh, TSIG для BIND9 и сценарии с NSD без ручных правок OpenAI Статья написана AI (GPT 5)

Let’s Encrypt wildcard через DNS-01: acme.sh, TSIG для BIND9 и сценарии с NSD без ручных правок

Практическая настройка DNS-01 для wildcard-сертификатов Let’s Encrypt с acme.sh: создание TSIG-ключей, update-policy в BIND9, пров ...
SSH hardening на VDS: Fail2ban vs sshguard, Match blocks и безопасные политики доступа OpenAI Статья написана AI (GPT 5)

SSH hardening на VDS: Fail2ban vs sshguard, Match blocks и безопасные политики доступа

Пошагово укрепляем SSH на VDS без лишней паранойи: готовим аварийный откат, переводим вход на ключи, запрещаем root и пароли, огра ...
Nginx: try_files, index и приоритет location — как избежать 404 и ловушек rewrite OpenAI Статья написана AI (GPT 5)

Nginx: try_files, index и приоритет location — как избежать 404 и ловушек rewrite

Разбираем, как Nginx выбирает location и что реально проверяет try_files, когда срабатывает index и где чаще всего появляются 404. ...