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

Автор |  28/12/2022
 

Загальну інформацію по Grafana Loki див. у Grafana Loki: архітектура та запуск в Kubernetes з AWS S3 storage та boltdb-shipper.

Серед інших сервісів, які складають собою Loki, є окремий сервіс ruler, який відповідає за роботу з алертами, які можно генерити прямо з логів.

Ідея дуже проста:

  • створюємо файл з алертами в  Prometheus-like форматі
  • підключаємо його до поду ruler (loki-read у випадку simple-scalable deployment)
  • ruler парсить логи по заданним в файлі конфігурації правилам, і якщо якийсь expression спрацьовує – то Ruler пушить Alertmanager, передаючи йому алерт

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

Документація – Rules and the 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]

І перевіряємо логі Loki:

Окей, тепер у нас є 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, якщо використовується:

...
        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
...

Загалом, на цьому поки що все.

Посилання по темі