Трюки для обробки метрик в Kapacitor

Швидше за все сьогодні вже ні в кого не виникає питання, навіщо потрібно збирати метрики сервісів. Наступний логічний крок - налаштувати алертинг на метрики, що збирається, який буде сповіщати про будь-які відхилення в даних у зручні вам канали (пошту, Slack, Telegram). У сервісі онлайн-бронювання готелів Ostrovok.ru всі метрики наших сервісів ллються в InfluxDB і відображаються в Grafana, там налаштований базовий алертинг. Для завдань типу "потрібно порахувати щось і порівняти з цим" ми використовуємо Kapacitor.

Трюки для обробки метрик в Kapacitor
Kapacitor – частина TICK-стеку, що вміє обробляти метрики з InfluxDB. Він може з'єднати кілька вимірювань між собою (join), з отриманих даних обчислити щось корисне, записати результат назад до InfluxDB, відправити алерт у Slack/Telegram/пошту.

Весь стек має круту та докладну документаціюАле завжди знайдуться корисні штуки, які в явному вигляді в мануалах не вказані. У цій статті я вирішив зібрати низку таких корисних неочевидних порад (основний синтаксис TICKscipt описаний тут) і показати, як їх можна застосовувати, на прикладі вирішення одного з наших завдань.

Поїхали!

float & int, помилки обчислень

Абсолютно стандартна проблема вирішується через каст:

var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))

Використання default()

Якщо тег/поле не заповнено, виникнуть помилки у обчисленнях:

|default()
        .tag('status', 'empty')
        .field('value', 0)

fill в join (inner vs outer)

За замовчуванням join відкине точки, де немає даних (inner).
При fill('null') буде виконано outer join, після якого потрібно зробити default() і заповнити порожні значення:

var data = res1
    |join(res2)
        .as('res1', 'res2)
        .fill('null')
    |default()
        .field('res1.value', 0.0)
        .field('res2.value', 100.0)

Тут все одно є нюанс. Якщо у прикладі вище одна із серій (res1 або res2) буде порожньою, підсумкова серія (data) також буде порожньою. На цю тему є кілька тикетів на гітхабі.1633, 1871, 6967) – чекаємо фіксів і трохи страждаємо.

Використання умов у обчисленнях (if у lambda)

|eval(lambda: if("value" > 0, true, false)

Останні п'ять хвилин із пайплайну за період

Наприклад, вам потрібно порівняти значення останніх п'яти хвилин із попереднім тижнем. Можна взяти дві пачки даних двома окремими batch'ами або витягнути частину даних із більшого періоду:

 |where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)

Альтернативою останніх п'яти хвилин може бути використання ноди BarrierNode, яка відсікає дані раніше зазначеного часу:

|barrier()
        .period(5m)

Приклади використання Go'шних шаблонів у message

Шаблони відповідають формату з пакета text.template, Нижче кілька часто зустрічаються завдань.

якщо ще

Наводимо лад, не тригерим людей текстом зайвий раз:

|alert()
    ...
    .message(
        '{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
    )

Дві цифри після коми у message

Поліпшуємо читабельність повідомлення:

|alert()
    ...
    .message(
        'now value is {{ index .Fields "value" | printf "%0.2f" }}'
    )

Розгортання змінних у message

Виводимо в повідомлення більше інформації для відповіді на питання «Чому репетує-то»?

var warnAlert = 10
  |alert()
    ...
    .message(
       'Today value less then '+string(warnAlert)+'%'
    )

Унікальний ідентифікатор алерту

Потрібна штука, коли в даних більше однієї групи, інакше генеруватиметься лише один алерт:

|alert()
      ...
      .id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')

Кастомні handler's

У великому списку хендлерів є exec, який дозволяє виконати свій скрипт з переданими параметрами (stdin) – творчість та й годі!

Один з наших кастомів – це невеликий скрипт для виправлення повідомлень у слак.
Спочатку нам захотілося відправляти у повідомленні картинку з графани, захищеною авторизацією. Після – писати OK в тред до попереднього алерту з тієї ж групи, а чи не окремим повідомленням. Ще трохи пізніше – дописувати у повідомлення найчастішу помилку за останні Х хвилин.

Окрема тема – зв'язок з іншими сервісами та будь-які дії, ініційовані алертом (тільки якщо ваш моніторинг працює досить добре).
Приклад опису хендлера, де slack_handler.py – наш самописний скрипт:

topic: slack_graph
id: slack_graph.alert
match: level() != INFO AND changed() == TRUE
kind: exec
options:
  prog: /sbin/slack_handler.py
  args: ["-c", "CHANNELID", "--graph", "--search"]

Як бешкетувати?

Варіант із виведенням у лог

|log()
      .level("error")
      .prefix("something")

Дивитись (cli): kapacitor -url host-or-ip:9092 logs lvl=error

Варіант із httpOut

Показує дані у поточному пайплайні:

|httpOut('something')

Дивитись (get): host-or-ip:9092/kapacitor/v1/tasks/task_name/something

Схема виконання

  • Кожна тяга повертає дерево виконання з корисними цифрами у форматі graphviz.
  • Беремо блок точка.
  • Вставляємо у viewer, насолоджуємося.

Де ще можна отримати граблями

timestamp в influxdb при зворотному записі

Наприклад, ми налаштовуємо алерт на суму запитів за годину (groupBy(1h)) і хочемо записати алерт в influxdb (щоб красиво показати факт наявності проблеми на графіку в grafana).

influxDBOut() запише в timestamp значення time з алерту, відповідно, точка на графіку буде записана раніше/пізніше, ніж прийшов алерт.

Коли потрібна точність: обходимо цю проблему через виклик кастомного handler'а, який запише дані в influxdb з поточним timestamp'ом.

docker, складання та деплой

При старті kapacitor може підвантажувати таски, шаблони і хендлер з директорії, прописаної в конфізі, в блоці [load].

Для коректного створення таски потрібні такі речі:

  1. Назва файлу – розгортається в id/назва скрипта
  2. Тип – stream/batch
  3. dbrp - кейворд для вказівки в якій базі + політиці працює скрипт (dbrp "supplier". "autogen")

Якщо в якомусь batch-тягу не буде рядка з dbrp, весь сервіс відмовиться запускатися і чесно напише про це в балку.

У chronograf'е ж, навпаки, цього рядка не повинно бути, через інтерфейс вона не приймається і видає помилку.

Хак при складанні контейнера: Dockerfile виходить з -1, якщо є рядки з //.+dbrp, що дозволить відразу зрозуміти причину фейлу при складанні білда.

join один до багатьох

Завдання: потрібно взяти 95-й перцентиль часу роботи сервісу за тиждень, порівняти кожну хвилину з 10 останніх з цим значенням.

Не можна зробити один до багатьох, last/mean/median по групі точок перетворюють ноду в stream, повернеться помилка «cannot add child mismatched edges: batch -> stream».

Результат batch'а, як змінної в lambda-вираженні, теж не підставляється.

Є варіант зберігати потрібні цифри з першого батча до файлу через udf і завантажувати цей файл через sideload.

Що ми вирішували цим?

Ми маємо близько 100 постачальників готелів, до кожного з них може бути кілька підключень, назвемо це каналом. Цих каналів приблизно 300, кожен із каналів може відвалитися. З усіх записуваних метрик моніторитимемо рейт помилок (requests і errors).

Чому не графана?

Алерти помилково, налаштовані в графані, мають кілька мінусів. Якісь критичні, на якісь можна заплющити очі, залежно від ситуації.

Графана не вміє обчислення між вимірами + алертинг, а нам потрібен рейт (requests-errors)/requests.

Помилки виглядають злісно:

Трюки для обробки метрик в Kapacitor

І менш зло, якщо дивитися з успішними запитами:

Трюки для обробки метрик в Kapacitor

Окей, ми можемо заздалегідь порахувати рейт у сервісі до графани, і в якихось випадках це підійде. Але над нашому, т.к. для кожного каналу своє співвідношення вважається «нормальним», а алерти працюють за статичними значеннями (шукаємо очима, міняємо, якщо часто алертит).

Це приклади «нормально» для різних каналів:

Трюки для обробки метрик в Kapacitor

Трюки для обробки метрик в Kapacitor

Нехтуємо попереднім пунктом та припустимо, що у всіх постачальників «нормальна» картина схожа. Тепер все добре, і ми можемо обійтися алертами в grafana?
Можемо, але дуже не хочеться, бо треба вибирати один із варіантів:
а) зробити безліч графіків під кожен канал окремо (і болісно їх супроводжувати)
б) залишити один графік з усіма каналами (і загубитися в квітчастих лініях та налаштованих алертах)

Трюки для обробки метрик в Kapacitor

Як зробили?

Знову ж таки, у документації є хороший стартовий приклад (Calculating rates across joined series), можна піддивитися або взяти за основу в аналогічних завданнях.

Що зробили в результаті:

  • join двох серій за кілька годин, угруповання каналами;
  • заповнюємо серії за групами, якщо даних не було;
  • порівнюємо медіану останніх 10 хвилин із попередніми даними;
  • кричимо, якщо щось виявили;
  • пишемо пораховані рейти і алерти в influxdb;
  • відправляємо корисне повідомлення у slack.

На мою думку, нам максимально красиво вдалося все, що хотіли б отримати на виході (і навіть трохи більше з кастомними хендлерами).

На github.com можна переглянути приклад коду и мінімальну схему (graphviz) одержаного скрипта.

Приклад коду, що вийшов:

dbrp "supplier"."autogen"
var name = 'requests.rate'
var grafana_dash = 'pczpmYZWU/mydashboard'
var grafana_panel = '26'
var period = 8h
var todayPeriod = 10m
var every = 1m
var warnAlert = 15
var warnReset = 5
var reqQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."requests"'
var errQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."errors"'

var prevErr = batch
    |query(errQuery)
        .period(period)
        .every(every)
        .groupBy(1m, 'channel', 'supplier')

var prevReq = batch
    |query(reqQuery)
        .period(period)
        .every(every)
        .groupBy(1m, 'channel', 'supplier')

var rates = prevReq
    |join(prevErr)
        .as('req', 'err')
        .tolerance(1m)
        .fill('null')
    // заполняем значения нулями, если их не было
    |default()
        .field('err.value', 0.0)
        .field('req.value', 0.0)
    // if в lambda: считаем рейт, только если ошибки были
    |eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
        .as('rate')

// записываем посчитанные значения в инфлюкс
rates
    |influxDBOut()
        .quiet()
        .create()
        .database('kapacitor')
        .retentionPolicy('autogen')
        .measurement('rates')

// выбираем данные за последние 10 минут, считаем медиану
var todayRate = rates
    |where(lambda: duration((unixNano(now()) - unixNano("time")) / 1000, 1u) < todayPeriod)
    |median('rate')
        .as('median')

var prevRate = rates
    |median('rate')
        .as('median')

var joined = todayRate
    |join(prevRate)
        .as('today', 'prev')
    |httpOut('join')

var trigger = joined
    |alert()
        .warn(lambda: ("prev.median" - "today.median") > warnAlert)
        .warnReset(lambda: ("prev.median" - "today.median") < warnReset)
        .flapping(0.25, 0.5)
        .stateChangesOnly()
        // собираем в message ссылку на график дашборда графаны
        .message(
            '{{ .Level }}: {{ index .Tags "channel" }} err/req ratio ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}It is ok now{{ else }}
'+string(todayPeriod)+' median is {{ index .Fields "today.median" | printf "%0.2f" }}%, by previous '+string(period)+' is {{ index .Fields "prev.median" | printf "%0.2f" }}%{{ end }}
http://grafana.ostrovok.in/d/'+string(grafana_dash)+
'?var-supplier={{ index .Tags "supplier" }}&var-channel={{ index .Tags "channel" }}&panelId='+string(grafana_panel)+'&fullscreen&tz=UTC%2B03%3A00'
        )
        .id('{{ index .Tags "name" }}/{{ index .Tags "channel" }}')
        .levelTag('level')
        .messageField('message')
        .durationField('duration')
        .topic('slack_graph')

// "today.median" дублируем как "value", также пишем в инфлюкс остальные филды алерта (keep)
trigger
    |eval(lambda: "today.median")
        .as('value')
        .keep()
    |influxDBOut()
        .quiet()
        .create()
        .database('kapacitor')
        .retentionPolicy('autogen')
        .measurement('alerts')
        .tag('alertName', name)

А висновок який?

Kapacitor чудово вміє виконувати моніторинг-алертинг із купою угруповань, проводити додаткові обчислення за вже записаними метриками, виконувати кастомні дії та запускати скрипти (udf).

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

Джерело: habr.com

Додати коментар або відгук