VictoriaLogs: створення Recording Rules з VMAlert

Автор |  08/01/2025
 

Продовжуємо міграцію з Grafana Loki на VictoriaLogs, і наступна задача – це перенести Recording Rules з Loki до VictoriaLogs, і оновити алерти.

Recording Rules та інтеграцію з VMAlert до VictoriaLogs завезли відносно недавно, і цю схему ще не тестував.

Тому спершу все зробимо руками, подивимось як це працює, які є нюанси, а потім будемо оновлювати Helm chart, яким деплоїться мій Monitoring Stack, і додавати туди нові Recording Rules.

Тож, що сьогодні:

  • встановимо VMAlert з Helm чарту в Kubernetes
  • перепишемо запит Loki LogQL на VictoriaLogs LogsQL
  • створимо VMAlert Recording Rule для генерації метрик з логів
  • протестуємо, як робити алерти з логів та Recording Rules
  • і подивимось, як цю схему можна інтегрувати в існуючий стек VictoriaMetrics

Попередні пости по VictoriaLogs:

Також див:

VictoriaLogs, Recording Rules та VMAlert

Отже, в чому полягає ідея:

  • VMAlert може робити запити до VictoriaLogs
  • в цих запитах він виконує якісь expr – як в звичайних алертах
  • по результатам цих запитів VMAlert або генерує метрику – якщо це Recording Rule – і записує її в VictoriaMetrics чи Prometheus, або генерує алерт – якщо це Alert

Тобто тут та ж сама схема, як і в Loki, і метрики з Recording Rule ми можемо використовувати не тільки для алертів, а і в Grafana dashboards.

Як завжди – у VictoriaMetrics є чудова документація:

Запуск VMAlert в Kubernetes з Helm чарту

В мене вже є повністю задеплоєний стек VictoriaMetrics і решта всього моніторингу власним чартом, але зараз VMAlert запустимо окремо від нього, бо є момент з тим, як VMAlert робить запити до VictoriaMetrics та VictoriaLogs – далі з цим розберемось.

Сам чарт тут – victoria-metrics-alert.

Для деплою нам знадобляться такі параметри:

  • datasource.url: адреса VictoriaLogs – до кого виконувати запити
  • notifier.url: адреса Alertmanager – куди слати алерти
  • remoteWrite.url: адреса VictoriaMetrics/Prometheus – куди записуємо метрики і стан алертів
  • remoteRead.url: адреса VictoriaMetrics/Prometheus – звідки читаємо стан алертів при рестарті VMAlert

Генеруємо values.yaml:

$ helm show values vm/victoria-metrics-alert > vmalert-test-values.yaml

Знаходимо потрібні Kubernetes Services:

$ kk -n ops-monitoring-ns get svc | grep 'alertmanager\|logs\|vmsingle'
atlas-victoriametrics-victoria-logs-single-server      ClusterIP   None             <none>        9428/TCP                     116d
vmalertmanager-vm-k8s-stack                            ClusterIP   None             <none>        9093/TCP,9094/TCP,9094/UDP   138d
vmsingle-vm-k8s-stack                                  ClusterIP   172.20.89.111    <none>        8429/TCP                     138d

Редагуємо vmalert-test-values.yaml:

...
  # VictoriaLogs Svc
  datasource:
    url: "http://atlas-victoriametrics-victoria-logs-single-server:9428"
...
  # Alertmanager Svc
  notifier:
    alertmanager:
      url: "http://vmalertmanager-vm-k8s-stack:9093"
...
  # VictoriaMetrics/Prometheus Svc
  remote:
    write:
      url: "http://vmsingle-vm-k8s-stack:8429"
...
    read:
      url: "http://vmsingle-vm-k8s-stack:8429"
...

Деплоїмо:

$ helm -n ops-monitoring-ns upgrade --install vmalert-test vm/victoria-metrics-alert -f vmalert-test-values.yaml

Перевіряємо Kubernetes Pod з VMalert:

$ kk -n ops-monitoring-ns get pod | grep vmalert-
vmalert-test-victoria-metrics-alert-server-6f485dc8b-tgcfd        1/1     Running     0             36s
vmalert-vm-k8s-stack-7d5bd6f955-dgx2r                             2/2     Running     0             47h

Тут vmalert-vm-k8s-stack-7d5bd6f955-dgx2r – це мій “дефолтний” VMAlert, а vmalert-test-victoria-metrics-alert-server – наш новий тестовий VMAlert.

Grafana Loki LogQL query => VictoriaLogs LogsQL query

В Grafana Loki в мене є такий Recording Rule:

kind: ConfigMap
apiVersion: v1
metadata:
  name: loki-alert-rules
data:
  rules.yaml: |-
    groups:
...
      - name: EKS-Pods-Metrics

        rules:

        - record: eks:pod:backend:api:path_duration:avg
          expr: |
            topk (10,
                avg_over_time (
                    {app="backend-api"} | json | regexp "https?://(?P<domain>([^/]+))" | line_format "{{.path}}: {{.duration}}"  | unwrap duration [5m]
                ) by (domain, path, node_name)
            )
...

Тут вичитуються логи з Kubernetes Pods нашого Backend API, з кожного запису створюється нове поле domain, і використовуються існуючі в логах поля path та duration.

А потім для кожного domain, path, node_name обчислюється average duration на виконання запиту.

Аби зробити аналогічний запит з VictoriaLogs LogsQL, нам потрібно:

  • вибрати логи з app:="backend-api"
  • створити поле domain
  • отримати значення path та duration
  • обчислити mean (average) за 5 хвилин по полю duration
  • згрупувати результат по полям domain, path, node_name

Знайдемо логи з VMLogs:

Далі:

  • додамо unpack_json, бо логи пишуться в JSON – парсимо його, і створюємо нові поля
  • додамо фільтр по полю http.url, бо частина записів в логах або не мають URL взагалі, або там адреса Kubernetes Pods у вигляді http://10.0.32.14:8080/ping – всякі Liveness && Readiness Probes, які нам не цікаві
  • використовуємо extract_regexp, аби з поля _msg створити нове поле domain
  • полів у нас тут забагато, всі вони нам не потрібні – використаємо fields pipe, і залишимо тільки ті, які будемо використовувати
  • можемо додати фільтр path:~".+", аби скіпнути всі записи з пустим path
app:="backend-api" | unpack_json | http.url:~"example.co" | extract_regexp "https?://(?P<domain>([^/]+))" | fields _time, path, duration, node_name, domain | path:~".+"

Замість фільтра http.url:~"example.co" можемо використати Sequence filter у формі http.url:seq("example.co") – але різниці у швидкості виконання запита не побачив:

Насправді для перформансу фільтр http.url:~"example.co" краще перенести на початок запиту, відразу за stream selector app:="backend-api", і спростити просто до Word filter "example.co" – але вже поробив скріни, тому ОК, тут нехай буде так, потім зробимо, як треба.

Тепер маємо потрібні записи, маємо потрібні поля – йдемо далі.

Далі нам потрібен stats pipe зі stats pipe function avg() за 5 хвилин зі значення в полі duration.

Додаємо в запит | stats by (_time:5m, path, node_name, domain) avg(duration) avg_duration.

Тут вже краще використати Time series візуалізацію в Grafana dashboard:

І давайте порівняємо результат з Loki.

Візьмемо якийсь домен, ноду, та URI, наприклад в Loki результат буде таким:

avg_over_time (
    {app="backend-api"} | json | regexp "https?://(?P<domain>([^/]+))" | line_format "{{.path}}: {{.duration}}" 
    | domain="api.challenge.example.co"
    | path="/coach/clients/{client_id}/accountability/groups"
    | node_name="ip-10-0-34-247.ec2.internal"
    | unwrap duration [5m]
) by (domain, path, node_name)

І в VictoriaLogs:

Значення “393” в обох випадках.

Гуд!

Тепер можемо власне переходити до Recording Rules.

Створення VictoriaLogs Recording Rules та Alerts

Для додавання Recording Rules в values чарту VMAlert є блок config.alerts.groups, в якому ми можемо з типом record описати або власне Recording Rule, або з типом alert – описати алерт.

Створення Recording Rule

Спочатку спробуємо Recording Rule.

Додаємо record: vmlogs:eks:pod:backend:api:path_duration:avg в наш файл vmalert-test-values.yaml:

...
  # -- VMAlert alert rules configuration.
  # Use existing configmap if specified
  configMap: ""
  # -- VMAlert configuration
  config:
    alerts:
      groups:
        - name: VmLogsEksPodsMetrics
          type: vlogs
          interval: 15s
          rules:
            - record: vmlogs:eks:pods:backend:api:path_duration:avg
              expr: |
                app:="backend-api" | unpack_json 
                | http.url:~"example.co" 
                | extract_regexp "https?://(?P<domain>([^/]+))" 
                | fields _time, path, duration, node_name, domain | path:~".+"
                | stats by (_time:5m, path, node_name, domain) avg(duration) avg_duration
...

Деплоїмо, глянемо логи тестового VMAlert:

$ ktail -n ops-monitoring-ns -l app.kubernetes.io/instance=vmalert-test
...
vmalert-test-victoria-metrics-alert-server-6469894c78-cmktk:vmalert {"ts":"2024-12-30T14:21:43.815Z","level":"info","caller":"VictoriaMetrics/app/vmalert/rule/group.go:486","msg":"group \"VmLogsEksPodsMetrics\" started; interval=15s; eval_offset=<nil>; concurrency=1"}
...

group \"VmLogsEksPodsMetrics\" started; – ОК.

Перевіряємо метрику vmlogs:eks:pods:backend:api:path_duration:avg в VMSingle:

Yay!

It works!

Створення Alert

Алерти можемо додати двома шляхами:

  • можемо описати новий алерт прямо в values чарту нового VMAlert, який буде виконувати запити напряму до VictoriaLogs
  • або, оскільки у нас є Recording Rule, який створює метрику – то ми можемо створити звичайний VMRule, який буде опрацьований оператором, і переданий до “дефолтного” VMAlert

Давайте спробуємо і так, і так.

Спочатку додамо алерт до файлу vmalert-test-values.yaml, поруч з нашим Recording Rule, в імені алерту вкажемо “Raw“:

...
  config:
    alerts:
      groups:
        - name: VmLogsEksPodsMetrics
          type: vlogs
          interval: 5s
          rules:

            - record: vmlogs:eks:pods:backend:api:path_duration:avg
              expr: |
                app:="backend-api" | unpack_json 
                | http.url:~"example.co" 
                | extract_regexp "https?://(?P<domain>([^/]+))" 
                | fields _time, path, duration, node_name, domain | path:~".+"
                | stats by (_time:5m, path, node_name, domain) avg(duration) avg_duration

            - alert: Test API Path duration Raw
              expr: |
                app:="backend-api" | unpack_json 
                | http.url:~"example.co" 
                | extract_regexp "https?://(?P<domain>([^/]+))" 
                | fields _time, path, duration, node_name, domain | path:~".+"
                | stats by (_time:5m, path, node_name, domain) avg(duration) as avg_duration
              for: 1s
              labels:
                severity: warning
                component: backend
                environment: dev
              annotations:
                summary: 'Test API Path duration Raw'
                description: |-
                  Request duration is too slow
                  *Domain Name*: `{{ $labels.domain }}`
                  *URI*: `{{ $labels.path }}`
                  *Duration*: `{{ $value | humanize }}`
                grafana_alb_overview_url: 'https://monitoring.ops.example.co/d/aws-alb-oveview/aws-alb-oveview?from=now-1h&to=now&var-domain={{ $labels.domain }}'
                tags: backend
...

Деплоїмо Helm з цим новим алертом:

$ helm -n ops-monitoring-ns upgrade --install vmalert-test vm/victoria-metrics-alert -f vmalert-test-values.yaml

Тепер створимо файл з VMRule з аналогічним алертом, але з метрики, яка створюється нашим Recording Rule – в ім’я алерту додаємо “VMSingle“:

apiVersion: operator.victoriametrics.com/v1beta1
kind: VMRule
metadata:
  name: alerts-vmlogs-test
spec:

  groups:

    - name: VMAlertVMlogsTest
      rules:
        - alert: Test API Path duration VMSingle
          expr: vmlogs:eks:pods:backend:api:path_duration:avg > 0
          for: 1s
          labels:
            severity: warning
            component: backend
            environment: dev
          annotations:
            summary: 'Test API Path duration VMSigle'
            description: |-
              Request duration is too slow
              *Domain Name*: `{{ $labels.domain }}`
              *URI*: `{{ $labels.path }}`
              *Duration*: `{{ $value | humanize }}`
            grafana_alb_overview_url: 'https://monitoring.ops.example.co/d/aws-alb-oveview/aws-alb-oveview?from=now-1h&to=now&var-domain={{ $labels.domain }}'
            tags: backend

Деплоїмо його:

$ kk -n ops-monitoring-ns apply -f test-alert.yaml
vmrule.operator.victoriametrics.com/alerts-vmlogs-test created

І чекаємо повідомлення від Alertmanager в Slack:

Гуд!

Працює.

Тепер можемо переносити цей конфіг до загального Helm-чарту нашого моніторингу.

VictoriaLogs, VMAlert, та чарт victoria-metrics-k8s-stack

Отже, в моєму проекті є наш власний чарт, в якому через Helm dependencies встановлюються такі чарти:

apiVersion: v2
name: atlas-victoriametrics
description: A Helm chart for Atlas Victoria Metrics Kubernetes monitoring stack
type: application
version: 0.1.1
appVersion: "1.17.0"
dependencies:
- name: victoria-metrics-k8s-stack
  version: ~0.31.0
  repository: https://victoriametrics.github.io/helm-charts
- name: victoria-metrics-auth
  version: ~0.8.0
  repository: https://victoriametrics.github.io/helm-charts
  condition: victoria-metrics-auth.enabled
- name: victoria-logs-single
  version: ~0.8.0
  repository: https://victoriametrics.github.io/helm-charts
...

А далі в values.yaml для кожного сабчарту задаються параметри.

VMAlert: datasource.url, VictoriaMetrics та VictoriaLogs

Що нам треба – це додати інтеграцію VMAlert з VictoriaLogs сюди, але є нюанс: VMAlert може мати тільки один параметр datasource.url, в якому зараз заданий Kubernetes Service з VMSingle – звідки VMAlert бере метрики для обчислення умов існуючих алертів:

$ kk -n ops-monitoring-ns describe pod vmalert-vm-k8s-stack-7d5bd6f955-m6mz4
...
Containers:
  vmalert:
    ...
    Args:
      -datasource.url=http://vmsingle-vm-k8s-stack.ops-monitoring-ns.svc.cluster.local.:8429
...

Але ж нам треба задати адресу VictoriaLogs, і при цьому залишити можливість запитів до VMSingle.

В документації VictoriaLogs How to use one vmalert for VictoriaLogs and VictoriaMetrics rules in the same time? описуються два варіанти рішення:

  • або просто мати два окремих інстанси VMAlert – один для метрик з VictoriaLogs, другий – для роботи з VictoriaLogs
  • або використати VMAuth, і в залежності від URI запиту від VMAlert роутити запити на потрібний бекенд – або VictoriaMetrics/VMSingle, або VictoriaLogs

Опція 1: два інстанси VMAlert

Перший варіант – запускати два VMAlert, і кожному передати власний datasource.url.

Але є питання – як в різні VMAlert передавати Recording Rules та власне Алерти?

Бо в мене Алерти описуються через ресурси VMRules, які з VictoriaMetrics Operator записуються в ConfigMap, який потім підключається до мого “дефолтного” VMAlert:

$ kk -n ops-monitoring-ns describe pod vmalert-vm-k8s-stack-7d5bd6f955-m6mz4
...
Volumes:
  ...
  vm-vm-k8s-stack-rulefiles-0:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      vm-vm-k8s-stack-rulefiles-0
...

І цей ConfigMap містить в собі всі алерти:

$ kk get cm vm-vm-k8s-stack-rulefiles-0 -o yaml | head -n 30
apiVersion: v1
data:
  ops-monitoring-ns-alerts-alertmanager.yaml: |
    groups:
    - name: VM.Alertmanager.rules
      rules:
      - alert: Alertmanager Failed To Send Alerts
        annotations:
          description: |-
            Alertmanager failed to send {{ $value | humanizePercentage }} of notifications
            *Kubernetes cluster*: `{{ $labels.cluster }}`
            *Pod*: `{{ $labels.pod }}`
            *Integration*:  `{{ $labels.integration }}`
          summary: Alertmanager Failed To Send Alerts
          tags: devops
        expr: |-
          sum(
            rate(alertmanager_notifications_failed_total [5m])
            /
            rate(alertmanager_notifications_total [5m])
          ) by (cluster, integration, pod)
          > 0.01
        for: 1m
        labels:
          component: devops
          environment: ops
          severity: warning
  ops-monitoring-ns-alerts-aws-alb.yaml: |
    groups:
    - name: AWS.ALB.Logs.rules

Якщо робити схему з двома інстансами VMAlert з різними datasource.url – то для інстансу, який буде робити запити до VictoriaLogs нам потрібно створювати власний ConfigMap, і маунтити його з вальюсів цього інстансу VMAlert, без VMRules і участі VM Operator.

Хоча технічно, мабуть, можливо мати VMRules з Recording Rules та Alerts і два інстанси VMAlert, де в кожен інстанс будуть мапитись один і той самий ConfigMap і з RecordingRules, і з Alerts – але тоді один VMAlert буде постійно писати про помилки запитів до VictroriaMetrcis, а другий – про помилки запитів до VictoriaLogs.

Тому тут бачу тільки варіант з окремим ConfgiMap для RecordingRules, і окремо мати VMRules для алертів, як воно є зараз.

Мені така схема якось не дуже подобається, бо я хотів би і RecordingRules, і Алерти описувати через VMRules.

ОК, тоді розглянемо інший варіант – з VMAuth.

Опція 2: VMAuth і src_paths

Другий варіант – редіректити запити від єдиного інстансу VMAlert до VictoriaLogs та VictoriaMetrics/VMSingle через VMAuth.

В мене VMAuth вже є, писав про нього в пості VictoriaMetrics: VMAuth – проксі, аутентифікація та авторизація, де налаштована аутентифікація і вже є роути – я ним користуюсь для доступу до деяких внутрішніх ресурсів, коли мені ліньки робити kubectl port-forward.

Що нам треба – це додати ще пару src_paths:

  • /api/v1/query.* – для запитів до VictoriaMetrics/VMSingle
  • /select/logsql/.* – для запитів до VictoriaLogs

Тоді в моєму випадку все разом буде виглядати так:

apiVersion: v1
kind: Secret
metadata:
  name: vmauth-config-secret
stringData:
  auth.yml: |-
    users:
    - username: vmadmin
      password: {{ .Values.vmauth_password }}
      url_map:
      - src_paths:
        - /alertmanager.*
        url_prefix: http://vmalertmanager-vm-k8s-stack.ops-monitoring-ns.svc:9093
      - src_paths:
        - /vmui.*
        url_prefix: http://vmsingle-vm-k8s-stack.ops-monitoring-ns.svc:8429
      - src_paths:
        - /prometheus.*
        url_prefix: http://vmsingle-vm-k8s-stack.ops-monitoring-ns.svc:8429
      - src_paths:
        - /api/v1/query.*
        url_prefix: http://vmsingle-vm-k8s-stack:8429
      - src_paths:
        - /select/logsql/.*
        url_prefix: http://atlas-victoriametrics-victoria-logs-single-server:9428
      default_url:
        - http://vmalertmanager-vm-k8s-stack.ops-monitoring-ns.svc:9093

Цей Secret передається в values для VMAuth:

...
victoria-metrics-auth:
  ingress:
    enabled: true
  ...
  secretName: vmauth-config-secret
...

Якщо у вас VMAuth не використовується, або працює без паролю – то простіше, бо для VMAlert просто можна задати datasource.url.

Якщо ж потрібна аутентифікація – то додамо ще один Kubernetes Secret з логіном та паролем:

apiVersion: v1
kind: Secret
metadata:
  name: vmauth-password
stringData:
  username: vmadmin
  password: {{ .Values.vmauth_password }}

Далі в вальюсах для VMAlert додаємо datasource.url та datasource.basicAuth:

...
  vmalert:
    annotations: {}
    enabled: true
    spec:
      datasource:
        basicAuth:
          username:
            name: vmauth-password
            key: username
          password:
            name: vmauth-password
            key: password
        url: http://atlas-victoriametrics-victoria-metrics-auth:8427    
...

Тут:

  • поле spec для VMAlert описується в VMAlertSpec і має поле datasource
    • поле datasource описується в VMAlertDatasourceSpec і має поля basicAuth та url
      • поле basicAuth описується в basicauth і має два поля – password та username
        • поля password та username описуються в SecretKeySelector, і мають два поля – name та key
          • поле name: ім’я Kubernetes Secret
          • поле key: ключ в цьому сікреті

Деплоїмо, і тепер наш VMAlert відправляє запити для алертів на VMAuth, а VMAuth редіректить їх до url_prefix: http://vmsingle-vm-k8s-stack:8429.

Додавання VMRule з RecordingRule

Тепер додамо новий VMRule, в якому опишемо RecordingRule, в якому будемо генерити метрику vmlogs:eks:pods:backend:api:path_duration:avg:

apiVersion: operator.victoriametrics.com/v1beta1
kind: VMRule
metadata:
  name: vmlogs-alert-rules
spec:

  groups:

    - name: VM-Logs-Backend-Pods-Logs
      # an expressions for the VictoriaLogs datasource
      type: vlogs
      rules:
        - record: vmlogs:eks:pods:backend:api:path_duration:avg
          expr: |
            app:="backend-api" "example.co" | unpack_json 
            | extract_regexp "https?://(?P<domain>([^/]+))" 
            | fields _time, path, duration, node_name, domain | path:~".+"
            | stats by (_time:5m, path, node_name, domain) avg(duration) avg_duration

Деплоїмо, перевіряємо новий VMRule:

$ kk get vmrule | grep vmlogs
vmlogs-alert-rules                                      4s              

Логи VMAlert – нова група створена:

$ ktail -l app.kubernetes.io/name=vmalert
...
vmalert-vm-k8s-stack-6c5cb6d76d-dxpbf:vmalert 2025-01-08T13:30:43.609Z  info    VictoriaMetrics/app/vmalert/rule/group.go:486   group "VM-Logs-Backend-Pods-Logs" will start in 1.540718685s; interval=15s; eval_offset=<nil>; concurrency=1
vmalert-vm-k8s-stack-6c5cb6d76d-dxpbf:vmalert 2025-01-08T13:30:45.151Z  info    VictoriaMetrics/app/vmalert/rule/group.go:486   group "VM-Logs-Backend-Pods-Logs" started; interval=15s; eval_offset=<nil>; concurrency=1
...

І перевіряємо нову метрику в VMSingle:

Готово.

Тепер можна мігрувати решту Recording Rules з Loki до VictoriaLogs.