Grafana Loki: можливості LogQL для роботи з логами та створення метрик для алертів

Автор |  30/12/2022
 

Добре – Loki запускати навчились – Grafana Loki: архітектура та запуск в Kubernetes з AWS S3 storage та boltdb-shipper, як налаштовувати алерти теж розібрались – Grafana Loki: алерти з Ruler та labels з логів.

Тепер час розібратися з тим, що взагалі ми можемо робити в Loki використовуючи її LogQL.

Підготовка

Далі для прикладів будемо використовувати два поди – один з nginxdemo/hello для звичайних логів nginx, а інший thorstenhans/fake-logger, який буде писати логи в JSON.

Для Nginx додамо Service, что б мати можливість слати запити з curl:

apiVersion: v1
kind: Pod
metadata:
  name: nginxdemo
  labels:
    app: nginxdemo
    logging: test
spec:
  containers:
  - name: nginxdemo
    image: nginxdemos/hello
    ports:
      - containerPort: 80
        name: nginxdemo-svc
---
apiVersion: v1
kind: Service
metadata:
  name: nginxdemo-service
  labels:
    app: nginxdemo
    logging: test
spec:
  selector:
    app: nginxdemo
  ports:
  - name: nginxdemo-svc-port
    protocol: TCP
    port: 80
    targetPort: nginxdemo-svc
---
apiVersion: v1
kind: Pod
metadata:
  name: fake-logger
  labels:
    app: fake-logger
    logging: test
spec:
  containers:
  - name: fake-logger
    image: thorstenhans/fake-logger:0.0.2

Деплоїмо та прокидуємо порт:

[simterm]

$ kk port-forward svc/nginxdemo-service 8080:80

[/simterm]

І запустимо curl зі звичайним GET в циклі:

[simterm]

$ watch -n 1 curl localhost:8080 2&>1 /dev/null &

[/simterm]

Та ще один – з POST:

[simterm]

$ watch -n 1 curl -X POST localhost:8080 2&>1 /dev/null

[/simterm]

Поїхали.

Grafana Explore: Loki – інтерфейс

Декілька слів про сам інтерфейс Grafana Explore > Loki.

Ви можете використовувати декілька запитів одночасно:

Також, є можливість розділити інтефейс на дві частини, і в кожній виконувати окремі запити:

Як і в звичайних дашбордах Grafana, є можливість вибрати період, за який ви хочете отримати дані, та задати інтервал для автооновлювання:

Або можете включити Live-режим – тоді дані будуть з’являтися як тільки вони потраплять до Loki:

Для створення запитів є два режими – Builder та Code.

В режимі Builder Loki видає список доступних тегів та фільтрів:

В режимі Code вони будуть підставлятися автоматично по мірі набору:

Функція Explain буде роз’яснювати що саме ваш запит робить:

А Inspector відобразить деталі про ваш запит – скільки часу і ресурсів було використано для формування відповіді – корисно для оптимізації запитів:

Крім того, завжди можна відкрити Loki Cheat Sheet, натиснувши (?) з правої сторони від поля для запиту:

LogQL: overview

В цілому, робота з Loki та її LogQL майже аналогічна роботі з Prometheus та його PromQL – майже всі тіж самі функції та загальний підхід, це навіть відображено в опису Loki: “Like Prometheus, but for logs”.

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

Типи запитів в Loki залежать від фінального результату:

  • Log queries: формують строки з лог-файлів
  • Metric queries: включають в себе Log queries, але в результаті формують числові значення, які можна використовувати для формування графіків в Grafana або для алертів в Ruler

В цілому, будь-який запит складається із трьох основних частин:

{Log Stream Selectors} <Log Pipeline "Log filter">

Тобто в запиті:

{app="nginxdemo"} |= "172.17.0.1"

{app="nginxdemo"} – це Log Stream Selector, в якому ми вибираємо конкретний стрім из Loki, |= – початок Log Pipeline, який включає в себе Log Filter Expression – "172.17.0.1".

Окрім Log filter, пайплайн може включати в себе Log або Tag formatting expression, який міняє отримані в пайплайн дані.

Обов’язковим є Log Stream Selector, тоді як Log Pipeline з його expressions являється опціональним, і використовуюється для уточненя або форматування результатів.

Log queries

Log Stream Selectors

Для селекторів викристовуються лейбли, які задаються агентом, який збирає логи – promtail, fluentd або іншими.

Log Stream Selector визначає скільки індексів та блоків данних будуть завантажені для повернення результату, тобто напряму впливає на швидкість роботи і ресурси CPU/RAM, задіяні для формування відповіді.

В прикладі вище в селекторі {app="nginxdemo"} ми використовуємо оператор “=“, який може бути:

  • = : дорівнює
  • != : не дорівнює
  • =~ : regex
  • !~ : негативний regex

Отже, за запитом {app="nginxdemo"} ми отримаємо логи всіх подів, у яких є тег app зі значенням nginxdemo:

Можемо комбінувати декілька селекторів, наприклад отримати всі логи з logging=test, але без app=nginxdemo:

{logging="test", app!="nginxdemo"}

Або використати regex:

{app=~"nginx.+"}

Або просто вибрати взагалі всі логи (стріми), в яких є тег app:

Log Pipeline

Данні, отримані зі стриму можна передати в пайплайн для подальшого фільтрування або форматування. При цьому результат роботи одного пайплайну можна передати в наступний, і так далі.

Pipeline може включати в себе:

  • Log line filtering expressions – для фільтрування попередніх результатів
  • Parser expressions – для отримання тегів з логів, які можна передати в Tag filtering
  • Tag filtering expressions – для фільтрування даних по тегам
  • Log line formatting expressions – використовується для редагування отриманних результатів
  • Tag formatting expressions – редагування тегів/лейбл

Log Line filtering

Фільтри використовуються для… фільтрування)

Тобто, коли ми отримали дані із стриму, і хочемо з нього вибрати окремі строки – то використовуємо log filter.

Фільтр складається з оператора та строкового запиту, за яким робиться вибірка данних.

Операторами можуть буди:

  • |=: строка містить строковий запит
  • ! =: строка НЕ містить строковий запит
  • |~: строка дорівнює регулярному виразу
  • ! ~: строка НЕ дорівнює регулярному виразу

При використанні regex майте на увазі, что використовується синтаксис Golang RE2, і за замочуванням він є case-sensitive. Щоб переключити його на незалежний від регистру режим – додаємо (i?).

Окрім того, Log Line filtering краще використовувати на початку запиту, бо вони працють швидко, і позбавлять наступні пайплайни він зайвої роботи.

Прикладом log filter може бути вибірка за строкою:

{job=~".+"} |= "promtail"

Або декілька виразів, використовуючи регулярку:

Parser expressions

Парсери… парсять) (гвинтокрили гвинтять) вхідні дані, та отримують з них лейбли, які потім можна використати в подальших фільтрах або для формування Metric queries.

Наразі, LogQL підтримує json, logfmt, pattern, regexp та unpack для роботи з тегами.

json

Наприклад, json формує всі json-ключі в лейбли, тобто запит {app="fake-logger"} | json замість:

Сформує новий набір тегів:

Отримані через json теги можна далі використати для додаткових фільтрів, наприклад – вибрати тільки строки з level=debug:

logfmt

Для формування тегів з логів не в форматі JSON можно використати logfmt, який всі знайдені поля перетворить на лейбли.

Наприклад, job="monitoring/loki-read" має поля ключ=значення:

level=info ts=2022-12-28T14:31:11.645759285Z caller=metrics.go:170 component=frontend org_id=fake latency=fast

Які за допомогою logfmt перетворяться на лейбли:

regexp

Парсер regex приймає аргумент, в якому вказується regex-група, яка сформує тег із запиту.

Наприклад, зі строки:

10.0.44.12 – – [28/Dec/2022:14:42:58 +0000] 204 “POST /loki/api/v1/push HTTP/1.1” 0 “-” “promtail/” “-“

Ми можемо динамічно сформувати теги ip та status_code:

{container="nginx"} | regexp "^(?P<ip>[0-9]{1,3}.{3}[0-9]{1,3}).*(?P<status_code>[0-9]{3})"

pattern

pattern дозволяє сформувати лейбли за шаблонами лог-запису, тобто строка:

10.0.7.188 – – [28/Dec/2022:15:27:04 +0000] 204 “POST /loki/api/v1/push HTTP/1.1” 0 “-” “promtail/2.7.0” “-“

Може бути описана у вигляді:

{container="nginx"} | pattern `<ip> - - [<date>] <status_code> "<request> <uri> <_>" <size> "<_>" "<agent>" <_>`

де <_> ігнорить, тобто не створює тегу.

І в результаті отримаємо набір лейбл за цим шаблоном:

Див. більше тут – Introducing the pattern parser.

Tag filtering expressions

Як видно з назви, дозволяє створювати нові фільтри з тегів які вже є в запису, або які були створені за допомогою попереднього парсеру, наприклад logfmt.

Візьмемо строку:

level=info ts=2022-12-28T15:56:31.449162304Z caller=table_manager.go:252 msg=”query readiness setup completed” duration=1.965µs distinct_users_len=0

Якщо пропустимо її через парсер logrmt, то отримаємо теги caller, msg, durarion та distinct_users_len:

Далі, можемо створити фільтр за цими тегами:

Доступні оператори тут ==, =, !=, >, >=, <, <=.

Також, можемо використати оператори and або or:

{job="monitoring/loki-read"} | logfmt | caller="table_manager.go:252" or org_id="fake" and caller!~"metrics.go.+"

Log line formatting expressions

Далі, можемо формувати те, які саме дані нам будуть відображені в записі.

Наприклад, візьмемо той же loki-read, в якому маємо теги:

Серед них нам цікаво відобразити тільки component та duration – використовуємо форматування:

{job="monitoring/loki-read"} | logfmt | line_format "{{.component}} {{.duration}}"

Label format expressions

За допомогою label_format можемо перейменувати, змінити чи додати нові лейбли.

Для цього, аргументом передаємо ім’я лейбли з оператором =, за яким йде потрібне значення.

Наприклад, маємо лейблу app:

Яку хочемо перейменувати в application – використовуємо label_format application=app:

Або можемо використати значення існуючого тегу для створення нового, для цього використовуємо шаблонізатор у вигляді {{.field_name}}, де можемо комбінувати декілька полів.

Тобто, якщо хочемо створити тег error_message в якому будуть значення полів level та msg – формуємо такий запит:

{job="default/fake-logger"} | json | label_format error_message="{{.level}} {{.msg}}"

Log Metrics

І розглянемо як із логів можна створювати метрики, які можна використовувати для формування графіків або алертів (див. Grafana Loki: алерти з Ruler та labels з логів).

Interval vectors

Для роботи з векторами за часом наразі є чотири доступні функції, які в принципі вже знайомі по Prometheus:

  • rate: кількість логів в секунду
  • count_over_time: підрахувати кількість записів стріму за заданий проміжок часу
  • bytes_rate: кількість байт в секунду
  • bytes_over_time: підрахувати кількість байт стріму за заданий проміжок часу

Наприклад, отримати queries per second для джоби fake-logger:

rate({job="default/fake-logger"}[5m])

Може бути корисно, щоб створити алерт на випадок як якийсь сервіс почав писати багато логів, що може бути ознакою того, що “щось пішло не так”.

Отримати кількість записів з рівнем warning за останні 5 хвилин можно за допомогою такого запиту:

count_over_time({job="default/fake-logger"} | json | level="warning" [5m])

Aggregation Functions

Також, можемо використовувати функції агрегації для об’єднання вихідних даних, всі також знайомі по PromQL:

  • sum: сумма за лейблою
  • min, max та avg: мінімальне, максимальне да середнє значення
  • stdev, stdvar: стандартне відхилення та розбіжність
  • count: кількість елементів у векторі
  • bottomk та topk: мінімальний та максимальний елементи

Синтаксис функцій агрегації:

<aggr-op>([parameter,] <vector expression>) [without|by (<label list>)]

Наприклад, отримати кількість записів в секунду від джоби fake-logger, та поділити їх по тегу label:

sum(rate({job="default/fake-logger"} | json [5m])) by (level)

Або з прикладів вище:

  • отримати записи з подів loki-read
  • із результату створити дві нові лейбли – component та duration
  • отримати кількість записів в секунду
  • прибрати записи без компоненту
  • та відобразити суму по кожному компоненту
sum(rate({job="monitoring/loki-read"} | logfmt | line_format "{{.component}} {{.duration}}" | component != "" [10s])) by (component)

Інші оператори

І зовсім вже коротко про інші можливості.

Математичні оператори:

  • + – додавання
  • - – віднімання
  • * – множення
  • / – ділення
  • % – коефіцієнт
  • ^ – зведення у ступінь

Логічні оператори:

  • and: та
  • or: або
  • unless: за виключенням

Оператори порівняння:

  • ==: дорівнює
  • !=: не дорівнює
  • >: більше ніж
  • >=: більше ніж або дорівнює
  • <: менше ніж
  • <=: менше ніж або дорівнює

Знов-таки з прикладів, які використовували раньше.

Створюємо лейблу request:

{container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>`

Отримаємо рейт запросів POST в секунду за останні 5 хвилин:

sum(rate({container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>` | request="POST" [5m]))

Спочатку перевіримо на графіку кількість запитів GET та POST:

sum(rate({container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>` [5m]))  by (request)

А теперь отримаємо процент з типом POST від загальної кількості запитів:

  • всі запити POST ділимо на загальну кількість запитів
  • результат множимо на 100
sum(rate({container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>` | request="POST" [5m])) / sum(rate({container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>` [5m])) * 100

На цьому все.

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