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.

Пайплайн:

  1. docker: {} снимает обёртку container runtime, чтобы внутри осталась голая JSON-строка Traefik.
  2. json: вытаскивает из неё нужные поля.
  3. labels: поднимает три из них до лейблов стрима. По DownstreamStatus и RequestMethod фильтруют дашборды. Поднимать ClientHost (IP каждого посетителя) в Loki было бы преступлением против кардинальности — VictoriaLogs складывает высококардинальные лейблы в обычные поля вместо взрыва индекса, и только поэтому здесь это безопасно. Что показательно не поднято: RequestPath. Он остаётся в теле лога и при необходимости фильтруется LogsQL.
  4. 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-логов нормально