Когда 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.

Парсинг: 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. Так проще выдерживать пики инжеста и хранение индекса.
Проверяем кардинальность до продакшена
Главная техника: не поднимайте поле в метки, пока не проверите его будущую кардинальность с помощью запросного парсинга. В 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 показывает тысячи уникальных значений, нормализуйте его. Аналогично можно проверять любые другие поля. Прелесть в том, что вы делаете это без изменения схемы хранения.

Типовые анти‑паттерны и их исправления
- Метка
request_idилиuser_id. Исправление: оставить в теле, при необходимости — нормализовать и использовать на запросе. - Сырой
pathкак метка. Исправление: шаблонизировать вtemplateили нормализовать на запросе, хранить сырой путь в теле. - Поднятие десятков имён меток из logfmt/JSON «на всякий случай». Исправление: белые списки в
labelsиrelabel_configs. - Отсутствие лимитов. Исправление: задать лимиты Loki и алерты на рост числа потоков.
Заключение
Loki отлично справляется с большими объёмами логов, если держать под контролем главное — кардинальность меток. Стройте пайплайны Promtail вокруг идеи «метки только для стабильных измерений», всё остальное — в теле. Нормализуйте, маскируйте, отбрасывайте шум до отправки. Проверяйте идею новой метки через запросный парсинг, и лишь затем поднимайте её в схему. Включайте ограничители в Loki, следите за количеством потоков и используйте запросные шаблоны для безопасных экспериментов. Так вы получите быстрые запросы, предсказуемые ресурсы и спокойные ночи админа.


