Promtail: доставка access-логов Traefik в VictoriaLogs
Published: 2026-06-12
VictoriaLogs запущена и ждёт данных — теперь что-то должно читать access-логи Traefik с ноды и пушить их в хранилище. Это «что-то» — Promtail: DaemonSet, один ConfigMap и пайплайн, который превращает JSON-лог Traefik в размеченные, запрашиваемые стримы. Заодно расскажу про крюк через Kubernetes service discovery, который я в итоге выкинул в пользу глоба в одну строку.
Шаг 0: заставить Traefik писать access-логи
По умолчанию Traefik не логирует почти ничего. Access-лог включается в Helm values (в k0s это chart extension), и ключевое здесь — формат:
yamllogs:
general:
level: INFO
access:
enabled: true
format: json
format: json — это весь фокус. CLF-формат, используемый по умолчанию, пришлось бы разбирать регулярками; JSON бесплатно даёт именованные поля: ClientHost, RequestMethod, RequestPath, DownstreamStatus, Duration, RouterName, ServiceName. Лог идёт в stdout, kubelet складывает его в /var/log/pods/, и подобрать его может всё, что умеет тейлить файлы.
DaemonSet Promtail
Promtail монтирует каталог логов ноды read-only и работает на каждом узле (узел тут один, но DaemonSet ничего не стоит и переживёт день, когда узлов станет два):
yamlapiVersion: apps/v1
kind: DaemonSet
metadata:
name: promtail
namespace: logging
spec:
selector:
matchLabels:
app: promtail
template:
metadata:
labels:
app: promtail
spec:
serviceAccountName: promtail
tolerations:
- operator: Exists
containers:
- name: promtail
image: grafana/promtail:3.0.0
args:
- -config.file=/etc/promtail/promtail.yaml
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: config
mountPath: /etc/promtail
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: config
configMap:
name: promtail-config
- name: varlog
hostPath:
path: /var/log
Лимит 64 МБ. Promtail с одним лог-стримом использует малую долю этого.
Конфиг: клиент и positions
yamlserver:
http_listen_port: 9080
grpc_listen_port: 0
log_level: warn
positions:
filename: /tmp/positions.yaml
clients:
- url: http://victoria-logs.logging.svc.cluster.local:9428/insert/loki/api/v1/push
URL клиента — Loki-совместимый эндпоинт VictoriaLogs. Promtail понятия не имеет, что говорит не с Loki.
positions.filename: /tmp/positions.yaml — осознанное упрощение. Positions хранят, докуда дочитан каждый файл; в /tmp они умирают вместе с подом, и после рестарта Promtail перечитывает ещё существующие файлы и шлёт дубли. Для access-логов это приемлемо — задвоенная строка в дашборде не инцидент. Если когда-нибудь надоест — лечится hostPath-маунтом под positions, а не PVC.
Scrape-конфиг: версия, которая работает
yamlscrape_configs:
- job_name: traefik-access
static_configs:
- targets: [localhost]
labels:
job: traefik-access
namespace: traefik
__path__: /var/log/pods/traefik_traefik-*/*/*.log
pipeline_stages:
- docker: {}
- json:
expressions:
ClientHost: ClientHost
RequestMethod: RequestMethod
RequestPath: RequestPath
DownstreamStatus: DownstreamStatus
Duration: Duration
RouterName: RouterName
ServiceName: ServiceName
level: level
- labels:
ClientHost:
RequestMethod:
DownstreamStatus:
- drop:
expression: '.*middleware.*does not exist.*'
Раскладка kubelet'а — /var/log/pods/<namespace>_<pod-name>_<pod-uid>/<container>/*.log, так что глоб traefik_traefik-* находит под Traefik в неймспейсе traefik независимо от текущего UID и числа рестартов. Одна строка пути заменяет целый механизм discovery.
Пайплайн:
docker: {}снимает обёртку container runtime, чтобы внутри осталась голая JSON-строка Traefik.json:вытаскивает из неё нужные поля.labels:поднимает три из них до лейблов стрима. ПоDownstreamStatusиRequestMethodфильтруют дашборды. ПодниматьClientHost(IP каждого посетителя) в Loki было бы преступлением против кардинальности — VictoriaLogs складывает высококардинальные лейблы в обычные поля вместо взрыва индекса, и только поэтому здесь это безопасно. Что показательно не поднято:RequestPath. Он остаётся в теле лога и при необходимости фильтруется LogsQL.drop:выбрасывает конфигурационный шум — ошибку, которую Traefik пишет на каждый запрос к роуту с протухшей ссылкой на middleware: один кривой редирект давал тысячи одинаковых строк в день. Дроп на агенте не пускает их в хранилище вообще.
Версия, которая не выжила: kubernetes_sd
Первая итерация находила под Traefik через Kubernetes API — так, как учит каждый туториал по Promtail:
yamlkubernetes_sd_configs:
- role: pod
namespaces:
names: [traefik]
relabel_configs:
- source_labels: [__meta_kubernetes_pod_container_name]
regex: traefik
action: keep
- source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]
separator: /
replacement: /var/log/pods/*$1*/$2/*.log
target_label: __path__
Это работало — и заодно это причина, по которой у ServiceAccount есть ClusterRole на pods, nodes, services и endpoints. Но ради ровно одного пода с предсказуемым именем это watch-соединение к API-серверу, четыре relabel-правила и кэш discovery — всё чтобы вычислить путь, который я могу написать руками. Когда я переписал джоб на статический глоб, хуже не стало ничего, а сорок строк конфига исчезли. Service discovery оправдывает себя, когда таргеты приходят и уходят; одиночный ingress-контроллер — не тот случай.
RBAC, впрочем, остаётся — следующему scrape-джобу (логи приложений по неймспейсам) discovery понадобится по-настоящему.
Замечание о сроке жизни Promtail
Grafana объявила Promtail устаревшим в пользу Alloy; ветка 3.x в режиме поддержки, окончание LTS-поддержки анонсировано на начало 2026 года. Я всё равно взял его: конфиг выше — двадцать строк, прозрачных насквозь; Alloy умеет писать в тот же Loki-эндпоинт, когда дойдёт до миграции; а принимающей стороне — VictoriaLogs — всё равно, кто пушит. Перенос этого джоба на Alloy — час работы в тот день, когда станет необходим.
Что может пойти не так
В VictoriaLogs пусто, в логах Promtail — «no such file». Глоб не совпадает с реальным каталогом пода. Смотрите, что там на самом деле: ls /var/log/pods/ | grep traefik. Под в другом неймспейсе или Helm-релиз с именем не traefik меняют префикс <namespace>_<pod-name>.
Строки приходят, но поля не парсятся. Обёртка рантайма и парсер не совпали. Docker пишет JSON-конверты ({"log": "...", "stream": ...}), containerd/CRI — текстовый префикс (2026-06-12T10:00:00.000Z stdout F {...}). Стадия docker: у Promtail разбирает первое, стадия cri: — второе. Если json: ничего не извлекает — посмотрите сырую строку из файла и проверьте, какой конверт у вас на самом деле.
Дубли строк после рестарта Promtail. Это positions в /tmp (см. выше) — здесь ожидаемо. Если дубли есть без рестартов — один и тот же путь тейлят два пода Promtail; ищите лишний под DaemonSet'а на ноде.
Drop-стадия съедает лишнее. drop.expression применяется ко всей строке на этом этапе пайплайна. Слишком широкая регулярка молча выбрасывает реальный трафик — после правки сверяйте объём count-запросом в VMUI: {job="traefik-access"} _time:15m | stats count().
Итого
- Traefik пишет JSON access-логи в stdout; раскладка kubelet'а
/var/log/pods/<ns>_<pod>-*/позволяет тейлить их одним глобом - Promtail пушит в VictoriaLogs по протоколу Loki — без плагинов и без знания, что это не Loki
kubernetes_sd_configsбыл первой версией; для одного известного пода статический глоб__path__делает то же на сорок строк короче- В лейблы поднимаются только низкокардинальные поля;
RequestPathостаётся в теле, аClientHostбезопасен только потому, что VictoriaLogs терпит высокую кардинальность - Известный шум отбрасывайте на агенте, а не на этапе запросов
- Positions в
/tmp— размен «дубли после рестарта» на ноль конфигурации хранения; для access-логов нормально