Grafana Loki: алерты с Ruler и labels из логов

Автор: | 15/12/2022

Пока пост по архитектуре и запуску Loki ещё в черновиках – посмотрим, как в Loki создавать алерты, за которые отвечает сервис ruler.

Документация по алертам в Loki – Rules and the Ruler.

Идея простая:

  • создаём файл с алертами в Prometheus-like формате
  • подключаем его к ruler
  • ruler парсит логи по заданным в конфиге выражениям, и пушит Alertmanager, передавая ему алерт

Алерты будем описывать в ConfigMap, который потом подключим к поду с Ruler.

Тестовый под для OOM-Killed

Мне хочется потестить на срабатывание OOM Killed, поэтому создадим под с явно заниженными лимитами, который будет убиваться “на взлёте”:

---
apiVersion: v1
kind: Pod
metadata:
  name: oom-test
  labels:
    test: "true"
spec:
  containers:
    - name: oom-test
      image: openjdk
      command: [ "/bin/bash", "-c", "--" ]
      args: [ "while true; do sleep 30; done;" ]
      resources:
        limits:
          memory: "1Mi"
  nodeSelector:
    kubernetes.io/hostname: eks-node-dev_data_services-i-081719890438d467f

В nodeSelector задаём имя ноды, что бы было проще искать в Локи.

При старте этого пода Kubernetes будет его убивать из-за превышения лимитов, а journald на WorkerNode будет записывать событие в системный журнал, который собирается promtail:

[simterm]

$ kk -n monitoring get cm logs-promtail -o yaml
...
    - job_name: journal
       journal:
        labels:
          job: systemd-journal
        max_age: 12h
        path: /var/log/journal
      relabel_configs:
      - source_labels:
        - __journal__systemd_unit
        target_label: unit
      - source_labels:
        - __journal__hostname
        target_label: hostname

[/simterm]

Запускаем наш под:

[simterm]

$ kk apply -f test-oom.yaml 
pod/oom-test created

[/simterm]

Проверяем:

[simterm]

$ kk describe pod oom-test
...
Events:
  Type     Reason                  Age                 From               Message
  ----     ------                  ----                ----               -------
  Normal   Scheduled               91s                 default-scheduler  Successfully assigned default/oom-test to ip-10-0-0-27.us-west-2.compute.internal
  Normal   SandboxChanged          79s (x12 over 90s)  kubelet            Pod sandbox changed, it will be killed and re-created.
  Warning  FailedCreatePodSandBox  78s (x13 over 90s)  kubelet            Failed to create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox container for pod "oom-test": Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: container init was OOM-killed (memory limit too low?): unknown

[/simterm]

И проверяем логи Локи:

Окей, теперь у нас есть oom-killed под для тестов – давайте формировать запрос для будущего алерта.

Формирование запроса в Loki

В логах мы смотрели по {hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" – используем его же для тестового алерта.

Сначала проверим что нам нарисует сама Локи – используем rate() и sum(), см. Log range aggregations:

sum(rate({hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" [5m])) by (hostname)

Гуд!

С этим уже можно работать – создавать тестовый алерт.

Создание алерта для Loki Ruler

Создаём файл с ConfigMap:

kind: ConfigMap
apiVersion: v1
metadata:
  name: rules-alerts
  namespace: monitoring
data:
  rules.yaml: |-
    groups:
      - name: systemd-alerts
        rules:
          - alert: TESTLokiRuler Systemd journal
            expr: |
              sum(rate({hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" [5m])) by (hostname) > 1
            for: 1s
            labels:
                severity: info
            annotations:
                summary: Test Loki OOM Killer Alert

Деплоим его:

[simterm]

$ kk apply -f rule-cm.yaml 
configmap/rules-alerts created

[/simterm]

Ruler и ConfigMap volume

Далее, нам надо подключить этот ConfigMap в под с ruler в каталог, который указан в конфиге Loki для компонента ruler:

...
    ruler:
      storage:
        local:
          directory: /var/loki/rules
...

Ruler у нас работает в loki-read подах – открываем их StatefulSet:

[simterm]

$ kk -n monitoring edit sts loki-read

[/simterm]

Описываем новый volume:

...
      volumes:
      - configMap:
          defaultMode: 420
          name: rules-alerts
        name: rules
...

И его маппинг в под по пути /var/loki/rules/fake/rules.yaml, где fake – это tenant_id, если используется: (расскажу в посте про запуск Loki):

...
        volumeMounts:
        - mountPath: /etc/loki/config
          name: config
        - mountPath: /tmp
          name: tmp
        - mountPath: /var/loki
          name: data
        - mountPath: /var/loki/rules/fake/rules.yaml
          name: rules
          subPath: rules.yaml
...

В subPath указываем key из ConfigMap, что бы подключить именно как файл.

Настройка Ruler alerting

Находим Alertmanager URL:

[simterm]

$ kk -n monitoring get svc
NAME                                             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
...
prometheus-kube-prometheus-alertmanager          ClusterIP   172.20.240.159   <none>        9093/TCP                     110d
...

[/simterm]

В ConfigMap Loki для ruler указываем этот адрес:

...
    ruler:
      storage:
        local:
          directory: /var/loki/rules
        type: local
      alertmanager_url: http://prometheus-kube-prometheus-alertmanager:9093
...

Все параметры для rulerтут>>>.

Открываем себе доступ к Alertmanager, что бы проверять алерты:

[simterm]

$ kk -n monitoring port-forward svc/prometheus-kube-prometheus-alertmanager 9093:9093

[/simterm]

Рестартим поды loki-read, можно просто через kubectl delete pod, и проверяем их логи:

[simterm]

$ kk -n monitoring  logs -f loki-read-0
...
level=info ts=2022-12-13T16:37:33.837173256Z caller=metrics.go:133 component=ruler org_id=fake latency=fast query="(sum by(hostname)(rate({hostname=\"eks-node-dev_data_services-i-081719890438d467f\"} |~ \".*OOM-killed.*\"[5m])) > 1)" query_type=metric range_type=instant length=0s step=0s duration=120.505858ms status=200 limit=0 returned_lines=0 throughput=48MB total_bytes=5.8MB total_entries=1 queue_time=0s subqueries=1
...

[/simterm]

Проверяем Алерты в Алертменеджере – http://localhost:9093:

Loki и дополнительные labels

В алертах хочется выводить немного больше информации, чем просто сообщение “Test Loki OOM Killer Alert”, к примеру – отобразить имя пода, который был убит.

Добавление labels в Promtail

Первый вариант – это создавать новые лейблы ещё на этапе сбора логов, в самом Promtail через pipeline_stages, см. Grafana: Loki – Prometheus-like счётчики и функции агрегации в LogQL и графики DNS запросов к dnsmasq, например так:

- job_name: journal
  pipeline_stages:
  - match:
      selector: '{job="systemd-journal"}'
      stages:
      - regex:
          expression: '.*level=(?P<level>[a-zA-Z]+).*'
      - labels:
          level:
      - regex:
          expression: '.*source="(?P<source>[a-zA-Z]+)".*'
      - labels:
          source:
  journal:
    labels:
      job: systemd-journal
    max_age: 12h
    path: /var/log/journal
  relabel_configs:
  - source_labels:
    - __journal__systemd_unit
    target_label: unit
  - source_labels:
    - __journal__hostname
    target_label: hostname

Тут я для тестов создавал нове новые лейблы, которые подключались к логам – source и level.

Другой вариант с Promtail – используя static_labels.

Но тут есть проблема: так как Loki на каждый набор лейбл создаёт отдельный стрим, для которого создаются отдельные индексы и блоки данных, то в результате получим во-первых проблемы с производительностью, во-вторых – со стоимостью, т.к. на каждый индекс и блок данных будут выполняться запросы чтения-записи в shared store, в конкретно нашем случае это AWS S3, где за каждый запрос приходится платить деньги.

См. отличный разбор на эту тему тут – Grafana Loki and what can go wrong with label cardinality.

Добавление labels из запросов в Loki

Вместо этого, мы можем создавать новый лейблы прямо из запроса с помощью самой Loki.

Возьмём запись из лога, в которой говорится о срабатывании OOM Killer:

E1213 16:52:25.879626 3382 pod_workers.go:951] “Error syncing pod, skipping” err=”failed to \”CreatePodSandbox\” for \”oom-test_default(f02523a9-43a7-4370-85dd-1da7554496e6)\” with CreatePodSandboxError: \”Failed to create sandbox for pod \\\”oom-test_default(f02523a9-43a7-4370-85dd-1da7554496e6)\\\”: rpc error: code = Unknown desc = failed to start sandbox container for pod \\\”oom-test\\\”: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: container init was OOM-killed (memory limit too low?): unknown\”” pod=”default/oom-test” podUID=f02523a9-43a7-4370-85dd-1da7554496e6

Тут у нас есть поле pod с именем пода, который был убит – pod="default/oom-test".

Используем regex в виде pod=".*/(?P<pod>[a-zA-Z].*)".*, что бы создать Named Capturing Group, проверяем например на https://regex101.com:

Дополняем выборку в Loki:

{hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" | regexp `pod=".*/(?P<pod>[a-zA-Z].*)".*`

И в логе получаем лейблу pod  со значением “oom-test“:

Проверяем запрос алерта с sum() и rate():

sum(rate({hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" | regexp `pod=".*/(?P<pod>[a-zA-Z].*)".*` | pod!="" [5m])) by (pod)

Результат:

Обновляем алерт – добавим description в котором используем {{ $labels.pod }}:

- alert: TESTLokiRuler Systemd journal
  expr: |
    sum(rate({hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" | regexp `.*pod=".*/(?P<pod>[a-zA-Z].*)".*` | pod!="" [15m])) by (pod) > 1
  for: 1s
  labels:
      severity: info
  annotations:
      summary: Test Loki OOM Killer Alert
      description: "Killed pod: `{{ $labels.pod }}`"

Ждём его срабатывание:

И в Слаке:

Grafana Loki и ошибки 502 и 504

Сейчас не получается зарепродьюсить, но иногда Grafana не может дождаться ответа от Loki, и выполнение запроса падает с ошибками 502 или 504.

Есть тред в Girthub, мне помогло увеличение таймаутов HTTP в Loki ConfigMap:

...
server:
  http_server_read_timeout: 600s
  http_server_write_timeout: 600s
...

В целом, на этом пока всё.

Ссылки по теме