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, ниже несколько часто встречающихся задачек.
if-else
Наводим порядок, не триггерим людей текстом лишний раз:
|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.
- Берем блок dot.
- Вставляем в viewer, наслаждаемся.
Где ещё можно получить граблями
timestamp в influxdb при обратной записи
Например, мы настраиваем алерт на сумму запросов за час (groupBy(1h)) и хотим записать случившийся алерт в influxdb (чтобы красиво показать факт наличия проблемы на графике в grafana).
influxDBOut() запишет в timestamp значение time из алерта, соответственно, точка на графике будет записана раньше/позже, чем пришел алерт.
Когда требуется точность: обходим эту проблему через вызов кастомного handler'а, который запишет данные в influxdb с текущим timestamp'ом.
docker, сборка и деплой
При старте kapacitor может подгружать таски, шаблоны и хендлеры из директории, прописанной в конфиге, в блоке [load].
Для корректного создания таски нужны следующие вещи:
- Название файла – разворачивается в id/название скрипта
- Тип – stream/batch
- dbrp – кейворд для указания в какой базе + политике работает скрипт (dbrp «supplier».«autogen»)
Если в какой-то batch-таске не будет строки с dbrp, весь сервис откажется запускаться и честно напишет об этом в лог.
В chronograf’е же, напротив, этой строки быть не должно, через интерфейс она не принимается и выдаёт ошибку.
Хак при сборке контейнера: Dockerfile выходит с -1, если есть строки с //.+dbrp, что позволит сразу понять причину фейла при сборке билда.
join один ко многим
Задача-пример: нужно взять 95-й перцентиль времени работы сервиса за неделю, сравнить каждую минуту из 10 последних с этим значением.
Нельзя сделать join один ко многим, last/mean/median по группе точек превращают ноду в stream, вернется ошибка «cannot add child mismatched edges: batch -> stream».
Результат batch’а, как переменной в lambda-выражении, тоже не подставляется.
Есть вариант сохранять нужные цифры из первого батча в файл через udf и загружать этот файл через sideload.
Что мы этим решали?
У нас есть около 100 поставщиков отелей, к каждому из них может быть несколько подключений, назовем это каналом. Этих каналов примерно 300, каждый из каналов может отвалиться. Из всех записываемых метрик будем мониторить рейт ошибок (requests и errors).
Почему не графана?
Алерты по ошибкам, настроенные в графане, имеют несколько минусов. Какие-то критичные, на какие-то можно закрыть глаза, в зависимости от ситуации.
Графана не умеет вычисления между измерениями + алертинг, а нам же нужен рейт (requests-errors)/requests.
Ошибки выглядят злобно:
И менее злобно, если смотреть с успешными запросами:
Окей, мы можем предварительно посчитать рейт в сервисе до графаны, и в каких-то случаях это подойдет. Но не в нашем, т.к. для каждого канала свое соотношение считается «нормальным», а алерты работают по статичным значениям (ищем глазками, меняем, если часто алертит).
Это примеры «нормально» для разных каналов:
Пренебрегаем предыдущим пунктом и предположим, что у всех поставщиков «нормальная» картина похожа. Теперь-то все хорошо, и мы можем обойтись алертами в grafana?
Можем, но очень не хочется, потому что надо выбирать один из вариантов:
а) сделать множество графиков под каждый канал отдельно (и мучительно их сопровождать)
б) оставить один график со всеми каналами (и потеряться в цветастых линиях и настроенных алертах)
Как сделали?
Опять же, в документации есть хороший стартовый пример (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).
Порог вхождения не очень высокий – попробуйте его, если графана или другие инструменты не до конца удовлетворяют ваши хотелки.
Комментарии (6)
M0rdecay
05.12.2019 12:31Прекрасная статья, спасибо!
Отмечу следующее — в который уже раз встречаю утверждение "Графана не умеет вычисления между измерениями", но ведь дело тут не в графане!
Объясню по порядку (защищу графану :))
Во-первых, возможность вести вычисления между сериями данных это заслуга использования движка запросов Flux, а не Kapacitor. Дефолтный язык запросов InfluxDB в межсерийные вычисления не умеет, с чем вы и столкнулись при работе с графаной (для работы с Flux есть плагин, но он ещё сырой).
Из этого следует второе — визуализатор/алерт менеджер ограничен только возможностями при запросов, который предоставляет источник данных. Пример — если использовать в качестве источника Elasticsearch (коим я для целей хранения данных мониторинга пользуюсь уже давно, а вам советую попробовать), внезапно, возможность вычислений между сериями появляется, т.к. её предоставляет ES search api — https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html
Из минусов относительно InfluxDB — по-умолчанию в Elasticsearch не включен Index lifecycle management (нечто близкое к retention policy), эту вещь нужно настраивать ручками, но это не сложно.
Поработайте с эластиком в связке с графаной, очень мощный получается инструмент.
nsnoow Автор
05.12.2019 12:42Спасибо за комментарий! Вы правы, это моя ошибка формулировок.
В вычисления между измерениями не умеет дефолтный язык InfluxDB, конечно же.
Про flux читал ранее, на прошлой неделе вспомнил ещё раз про него, но потыкать пока не дошли руки. В теории, штука-то очень крутая и ожидаемая, на практике пока сказать не могу.
В InfluxDB льются только метрики, Elasticsearch естественно есть, используем для хранения логов (кстати про метрики и логи очень понравилась статья: whiteink.com/2019/logs-vs-metrics-a-false-dichotomy)
Maximuzzz
Спасибо за подборку хинтов, пригодится.
А вы полностью управляете инфаструктурой инфлюикса и пишите скрипты для алертеинга или юзеры тоже могут писать свои скрипты? Если последнее — как решаете вопрос с рзделением прав доступа?
nsnoow Автор
Тут было бы круто получить уточнение — кто есть юзер в данном случае?
В графане разработчики сами настраивают необходимые алерты
Если же этот алертинг не достаточен/не очень удобен — иногда приходят с задачками для капаситора: «посчитать первое и второе, записать сюда» или «хочу шикарный алерт по вот такой хитрой логике»
Maximuzzz
Получается, что юзер = разработчик в вашем случае, у нас в добавок ещё и админы классические.
Как я понял доступа самим «покрутить» скрипты в капаситоре у ваших разработчиков нет?
nsnoow Автор
Да, отдельно над задачей «раздать доступы» не думали, не было нужды.
Капаситор все же немногим сложнее алертилки в графане, дополнительный синтаксис не нужен -> все более очевидно.