Статья рассказывает о проекте внутри компании на стыке цифрового суверенитета, которому суждено быть погребенным в архиве корпоративного репозитория кода. Возможно, эта информация спасет кого-то в поисках решения проблем, стоящих перед ними или натолкнет на нечто большое и красивое.
Привет, я – Женя, работал в команде обеспечения надежности компании Ситимобил. Последние полгода занимался чем-то средним между DevOps, SRE и разработкой. Теперь, вместо того чтобы готовиться к собеседованиям, саморефлексирую. Сейчас я хочу рассказать о нашем решении замены облачного инструмента для дежурных инженеров эксплуатации.
Введение
Возможно вы слышали что сервис городской мобильности Ситимобил больше не работает. Это не так – компания вместо коллапса об стену и за неделю до дедлайна, продана холдингу People2People и продолжает работать, но в новой реальности.
Для ротации дежурств и уведомления об инцидентах команда эксплуатации пользовалась сервисом Opsgenie от Atlassian. И это отличный продукт – с радостью платили за него, пока однажды не пришло уведомление об отключении нашего аккаунта. В причинах, насколько я смог перевести с английского, было указано что-то вроде: “извините, вы русские, а значит тоже виноватые. Спасибо за деньги, было классно – но больше так продолжаться не может”.
Уход с Opsgenie в один день был бы катастрофой. Было очень удобно роутить в него все алерты как продуктовые так и инфраструктурные. Не так давно устаканились их описания и процессы, поэтому моментальный переход вызвал бы риски для аптайма и ментального здоровья команды эксплуатации.
Намек поняли и занялись поиском альтернативы – оставаться без полноценного алертинга слишком дорого для бизнеса.
Облачный сервис компания боится покупать, на балансе NewRelic было больше 25K $ , Slack - 15K $. Аккаунты заморозили, деньги зависли – веры зарубежным провайдерам услуг больше нет.
Разработка своего решения – риск, в компании весьма ограничены ресурcы фронтенд-программистов.
Сравнение функциональности альтернатив Opsgenie
Быстро провели исследование: а что есть на рынке с открытым исходным кодом на Golang или Python. В поле зрения попали плагин Grafana OnCall и проект GoAlert.
Таблица сравнения по функциональности для инженеров:
Требование |
Opsgenie |
Goalert |
Grafana OnCall |
Комментарий |
Ротации дежурных |
+ |
+ |
+ |
GoAlert: неудобно быстро увидеть текущего дежурного |
Эскалации |
+ |
+ |
+ |
|
Звонки дежурным |
+ |
? |
? |
GoAlert: voip через добавление персонального вебхука для походов в API или покупать twilio OnCall: существует возможность использовать webhook, но требует исследования |
Push-уведомления |
+ |
- |
- |
OnCall: на момент сравнения не было |
SMS |
+ |
- |
? |
OnCall: только через Cloud версию |
Роутинг алертов по командам на основании лейблов и метаданных |
+ |
- |
+ |
|
Обновление статусов алертов (ack/close/assign) через API/звонок/приложение |
+ |
- |
- |
OnCall требует исследования, по коду Cloud умеет через звонок |
Размещение за пределами внутреннего контура |
+ |
? |
? |
GoAlert: нужно искать внешнюю ВМ, настраивать мониторинг и т.п. OnCall: умеет в k8s из коробки |
API (для поддержки работы внутренних сервисов) |
+ |
- |
+ |
OnCall: https://grafana.com/docs/oncall/latest/oncall-api-reference/ |
Назначение алерта на другую команду/пользователя |
+ |
- |
? |
GoAlert: переход на следующий шаг эскалации, проставление ack в UI или закрытие алерта |
Оповещение о начале дежурства |
+ |
- |
+ |
OnCall: Telegram, Slack |
Простота и понятность настройки |
+ |
- |
+ |
|
Возможность импорта существующих корпоративных аккаунтов |
- |
+ |
+ |
GoAlert: Реализовали создание учетной записи через наш локальный OAuth-провайдер |
OpenSource |
- |
+ |
+ |
|
Детализация сообщений |
+ |
+ |
- |
|
Мобильное приложение |
+ |
- |
- |
OnCall: на момент сравнения не было |
В целом сложились такие впечатления:
Grafana OnCall – классный быстро развивающийся проект. Для закрытия базовых потребностей наших инженеров требует серьезного допила кода, а также покупки Grafana Cloud или Twilio. Код открытый, понятный – воспользовавшись путем допиливания однажды попадем в ситуацию когда поддержание в актуальном состоянии внутреннего форка с сильными изменениями потребовало бы больших ресурсов, которых нет, да и вряд ли появятся в будущем;
GoAlert – реализация базовых сценариев требует серьезного допила кодовой базы нашими руками. Это не проблема пока в наличии ресурсы, но выделять их на поддержку этого решения странно и неразумно.
Копаем глубже
Решили более пристально посмотреть на Grafana OnCall в части кода и взаимодействия с облаком. По факту заказчиков сильно не устраивает невозможность звонка дежурному, и отсылка сообщения об ошибке в Slack или Telegram, вместо смс или пуша в приложение.
Нужно отметить тот факт что проект быстро развивается и на момент написания статьи уже реализована поддержка собственного мобильного приложения, и двух типов пуш-сообщений, в т.ч. с пробоем режима “не беспокоить”.
Оказалось – Grafana OnCall Cloud инстанс это плагин Grafana OnCall, который крутится в облаке Google Cloud Engine и имеет настройку интеграции с аккаунтом Twilio.
Вот такая вот рекурсия. Не то чтобы это скрывали, но я нахожу это решение интересным с технической точки зрения.
Бросили попытки сделать дозвон дежурным через URL Webhook в GoAlert, т.к. эта схема хоть и работала на тестах, показала свою несостоятельность в части масштабируемости на команды. Так как нужно было делать какую-то автоматизацию по поддержанию в актуальном состоянии номера текущего дежурного.
Да и это открытие того факта что всё работает на одной платформе сулило простые решения с достижением большего результата.
Пришла идея: изучить работу этой связки на бесплатном тарифе, посмотреть какие URL вызываются, параметры, проанализировать полученные данные это с бутылочкой лимонада.
На самом деле документация присутствует в репозитории, но угадывать что нужно дольше.
После создания тестового аккаунта Grafana Cloud настраиваем Charles Proxy на захват соединений из Docker-контейнера и получаем следующую картину:
Запросы уходят в JSON-формате или в POST-переменных, ответы приходят в JSON.
Красные и желтые статусы на картинке – проверки с заведомо неверными данными, сделаны чтобы лучше понимать протокол оповещения об ошибке, а что – нет. Разработчики OnCall придерживаются парадигмы стандартных коды ошибок HTTP. Нет маскировок через HTTP 200 + error в теле ответа и т.д.
Каждый запрос содержит в себе заголовок Authorization: <API ключ из настроек>.
Описание методов API
Описание каждого пункта что это и зачем используется:
1. api/v1/users?page=1&short=true&roles=0&roles=1
Синхронизация пользователей между облачным инстансом Grafanа и локальной копией. В нашем случае это дает статус “номер телефона верифицирован”, без чего нельзя указывать шаг “позвони дежурному”. Для этого нужно чтобы в ответе users было указано: is_phone_number_verified: true. Облачная Grafana отдает это поле если у нее в БД существует номер телефона для этого пользователя.
{
"count": 1,
"next": null,
"previous": null,
"results": [{
"id": "U4MEY7XPUAB74",
"email": "admin@localhost",
"username": "admin",
"role": "admin",
"is_phone_number_verified": true
}]
}
2. api/v1/info/
Информация об инстансе Grafana Cloud OnCall.
Формат ответа:
{
"url": "http://localhost:8080/"
}
Обратите внимание на то что этот URL будет использоваться в UI OnCall, например, для перехода в облачный профиль пользователя.
3. api/v1/integrations/
Этот endpoint используется для создания Heartbeat, формат следующий:
type=formatted_webhook&name=OnCall+Cloud+Heartbeat+http%3A%2F%2Flocalhost%3A8080
Если вам не знакомо понятие Heartbeat логика следующая: OnCall создает интеграцию в облаке к которому подключен: проверка того что он из этого облака доступен. Если условия не будут выполняться – откроется алерт. В нашем случае на это есть отдельные проверки, так что я просто приделал заглушки по ответам.
4. api/v1/make_call
Звонок дежурному, формат POST-запроса:
email=admin%40localhost&message=You+are+invited+to+check+an+incident+from+Grafana+OnCall.+Alert+via+Formatted+Webhook+%3Ablush%3A+with+title+TestAlert%3A+The+whole+system+is+down+triggered+1+times
Формат ответа: HTTP 200
{}
5. api/v1/make_call
СМС дежурному, формат POST- запроса:
email=admin%40localhost&message=You+are+invited+to+check+an+incident+%234+with+title+%22TestAlert%3A+The+whole+system+is+down%22+in+Grafana+OnCall+organization%3A+%22self_hosted_stack%22%2C+alert+channel%3A+Formatted+Webhook+%3Ablush%3A%2C+alerts+registered%3A+1%2C+http%3A%2F%2Fgrafana%3A3000%2Fa%2Fgrafana-oncall-app%2F%3Fpage%3Dincident%26id%3DIGXB2VDQ6L15C%0AYour+Grafana+OnCall+%3C3
В ответ HTTP 200
{}
Эта информация упростила понимание того что OnCall ожидает и как работает связка с облаком.
Теперь есть необходимая информация для реализации коннектора Grafana OnCall через REST API
Указать на его URI можно через переменную окружения GRAFANA_CLOUD_ONCALL_API_URL, которая используется в коде проекта.
Со стороны логики выглядит просто: к нам приходит сообщение в фиксированном формате, содержащее email пользователя – ищем запись в нашей БД и отсылаем пуш/смс/звонок в зависимости от ожидаемого и настроек. Из дополнительных плюсов появляется возможно реализовать контроль текста сообщения. Slack нам не нужен - потому что бан. Telegram опциональный канал оповещения, использование которого по соглашению внутри компании не принуждает к быстрому времени реагирования.
После анализа требований стало ясно что в нашей инфраструктуре есть то что нужно для реализации этого:
VOIP-сервис с экспертизой в нем;
Text-to-Speech engine c простой кодовой базой на Python;
Подключение к СМС-сервисам.
Прикинув бюджеты на СМС, решили доставлять пуши в клиентское мобильное приложение. Для этого уже есть: клиентское приложение и механизм доставки в него сообщений.
Этого набора функциональности хватит для MVP: сообщения приходят, дежурный получает голосовые звонки и текстовые уведомления.
Нам нужно немного больше
Погружаясь в реализацию в качестве полноценной замены Opsgenie нужно реализовать следующее:
Уметь ставить флаг Acknowledgement инциденту при звонке на телефон;
Давать чуть больше информации чем приходит в стадартной схеме. You’re Invited to check incident #11111 - не устраивает отсутствием деталей;
Уведомления дежурных инженеров о начале и конце смены.
Варианты как этого можно достичь:
1) Поставить флаг Acknowledged можно через запрос к internal-api:
curl -H "Authorization: Basic $encoded_base64" -X POST \
"https://grafana:3000/api/plugin-proxy/grafana-oncall-app/api/internal/v1/alertgroups/ID57VB23YIEFQ/acknowledge/"
Также можно писать лог в alert groups через другой HTTP-запрос, так будет понятнее что происходило и очень близко к тому как работает оригинальная интеграция.
Запрос:
curl -H "Authorization: Basic $encoded_base64" \
"https://grafana:3000/api/plugin-proxy/grafana-oncall-app/api/internal/v1/resolution_notes/" \
--data-raw '{"alert_group":"ID57VB23YIEFQ","text":"Acknowledged via phone +79010001020 by username"}'
2) Тут немного сложнее, при получении сообщения в наш сервис, нужно парсить сообщение и выполнить запрос к API, будет следующая структура из которой уже нужно вытягивать нужную информацию.
Запрос:
curl -H "Authorization: Basic $encoded_base64" \
"http://grafana:3000/api/plugin-proxy/grafana-oncall-app/api/internal/v1/alertgroups?search=111"
111 - это incident id из сообщения об инциденте.
Ответ прийдет в следующем формате, и с ним уже можно работать в зависимости задачи:
{
"next": null,
"previous": null,
"results": [
{
"pk": "IBPCHC5GUQA9J",
"alerts_count": 1,
"inside_organization_number": 1,
"alert_receive_channel": {
"id": "CURP5A67EWMXQ",
"integration": "formatted_webhook",
"verbal_name": "Formatted Webhook :blush:",
"deleted": false
},
"resolved": false,
"resolved_by": 2,
"resolved_by_user": null,
"resolved_at": null,
"acknowledged_at": "2023-04-04T10:29:27.480594Z",
"acknowledged": true,
"acknowledged_on_source": false,
"acknowledged_by_user": {
"pk": "U14Y3FBHKIFZ7",
"username": "admin"
},
"silenced": false,
"silenced_by_user": null,
"silenced_at": null,
"silenced_until": null,
"started_at": "2023-03-23T13:34:17.957545Z",
"related_users": [
{
"username": "admin",
"pk": "U14Y3FBHKIFZ7",
"avatar": "/avatar/46d229b033af06a191ff2267bca9ae56"
}
],
"render_for_web": {
"title": "TestAlert: The whole system is down",
"message": "<p>This alert was sent by user for the demonstration purposes<br/>Smth happened. Oh no!</p>",
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg",
"source_link": "https://en.wikipedia.org/wiki/Downtime"
},
"render_for_classic_markdown": {
"title": "TestAlert: The whole system is down",
"message": "This alert was sent by user for the demonstration purposes\nSmth happened. Oh no!",
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg",
"source_link": "https://en.wikipedia.org/wiki/Downtime"
},
"dependent_alert_groups": [],
"root_alert_group": null,
"status": 1,
"declare_incident_link": "http://grafana:3000/a/grafana-incident-app/incidents/declare/?caption=OnCall+Alert+Group&url=http%3A%2F%2Fgrafana%3A3000%2Fa%2Fgrafana-oncall-app%2Falert-groups%2FIBPCHC5GUQA9J&title=TestAlert%3A+The+whole+system+is+down",
"team": null,
"is_restricted": false
}
]
}
Нужно отметить что я потерял много времени с RBAC разрешениями в Grafana, в итоге плюнул и создал системного пользователя. В примерах команд предполагается, что данные этого пользователя указаны в формате "логин:пароль" и кодировке BASE64. Если вы побороли RBAC – замените Basic на Bearer.
3) Уведомление дежурных о начале смены
Отслеживаем вывод API schedules, затем пришло на ум два подхода:
создавать инцидент с текстом «Ура, начало вашей смены!»
отсылать пуши, когда видим что дежурный поменялся
Используем стандартную функциональность OnCall: интеграцию с календарем в формате iCal, тогда уведомления будут приходить в корпоративный календарь.
Мне нравится идея с созданием инцидента, так как проверяется вся цепочка: создание алерта, отправка сообщений о нем, эскалация если оповещение не дошло и не было погашено.
Если кто пойдет по этим стопам и придумает другой подход к проблеме, напишите потом в личку, пожалуйста – мне почему-то важно знать как тут можно было сделать еще.
Итоги
MVP-версия была реализована, запущена и успешно протестирована, можно даже сказать что внедрена в использование. Grafana OnCall подхватывает нашу реализацию как родную, успешно расширяет свою функциональность, отправляет оповещения об алертах в нужном нам формате по нужным каналам, без необходимости приобретать подписку каких-либо иностранных сервисов.
Схема взаимодействия сервисов получилась примерно такая:
Да, здесь есть над чем работать в отношении количества потенциальных точек отказа.
Я бы в дальнейшем интегрировал в Эмулятор отсылку пушей и клиент к Asterisk + TTS, или подключил сервисы через MQ-очередь, а не HTTP API как это сделано сейчас.
Но текущая схема проста, её можно было быстро собрать из тех кубиков которые уже имелись, а с таким мощным фундаментом в дальнейшем уже можно продолжать работу над стабильностью и расширением функционала.
На экране телефона выглядело так:
На первом скрине видны изменения в тексте, которые невозможно достичь в базовой версии OnCall. К сожалению, логика поиска FQDN сервиса с помощью регулярного выражения срабатывала не всегда. Как и здесь :)
Добились полного контроля над продуктом, который можем хостить в удобном нам месте и делать с ним все что требуется для покрытия нужд дежурных инженеров. Нет боязни остаться без инцидент-менеджмента в любой момент, когда узнают про белорусский аккаунт и его отключат. Кодовая база получилось небольшая и понятная, ее можно было легко поддерживать как прослойку и передавать в другие команды в дальнейшем.
Расширение функциональности тоже не было бы проблемой, следующей идеей на очереди выступала поддержка мобильного приложения Grafana OnCall. На проработке пара идей – руки уже не дошли, сокращение пришло быстрее :)
Из проблем могу назвать только что UI Grafana OnCall не готов к тому что облачный инстанс может не отвечать, и если так случается - показывает ошибку что ситуация ой, работать не буду. Я нахожу это странным, ведь для работы UI постоянное подключение к облаку не нужно. Думаю что это можно поправить простым PR в их репозитарий, у меня, к сожалению, руки не дошли.
Комментарии (6)
Matvey-Kuk
13.06.2023 11:35+2Я снимаю шляпу перед усилиями, которые вы провернули вокруг OnCall, и не могу не позвать коммитить изменения в апстрим :)
Например, недавно у нас появился достаточно качественный интерфейс для написания собственных звонилок и комьюнити сразу же привинтило к нему znonok: https://github.com/grafana/oncall/pull/2137/files, контрибьютор астериска уже переписывает свой PR на новый интерфейс и, будем надеется, увидим его в апстриме: https://github.com/grafana/oncall/pull/1282
В общем, у нас нет никакого хитрого замысла заставить всех платить Twilio или пользоваться именно нашим мобильным приложением, которое шлет пуши через наш клауд (какой-то облачный сервис для этого нужен, мы запилили свой). Контрибьютеры позитивно относятся к расширению функционала в ту сторону, которая обеспечит вам большую независимость.
Matvey-Kuk
13.06.2023 11:35А еще, у нас есть русскоязычное коммьюнити, присоединяйтесь: https://t.me/amixr_ru
punilki
а что мешало поставить локально OSS OnCall и ним экспериментировать?
Для дежурств мы используем гугл-календарь, OnCall следит за ним и делает ротацию сам, оповещая дежурных.
ну и основная проблема алертов экосистемы Prometheus - мешанина из информации и отвратительный UX без напильника, судя по скринам, так и не решена
ebogdanov Автор
Считаю что это хорошо если у вас есть время попереживать про UX в таком деле :) У меня в OnCall только создание расписания доставляет боль чуть ниже спины.
Про Google Calendar отличная идея, – спасибо.
1) OSS OnCall не умеет дозваниваться и отправлять СМС/Пуши. В Телегу/Слак/Email - пожалуйста прямо из коробки. Нужная же команде функциональность работает только если есть подключение к Grafana OnCall Cloud, или к Twilio - поэтому и пришлось изучить связку и сделать эмулятор, так как API у OnCall проще в реализации.
2) В таком наборе можно решать что угодно по информационному наполнению сообщения - у нас сокращение штата прошло раньше чем до этого руки дошли. Но по тому что прилетает в нашем конкретном случае порядок навели и внедрили общий стандарт.
punilki
Тут соглашусь, для OSS нужен сторонний провайдер звонков-пушей, у нас как раз Twilio
Увы, нам в процессе эксплуатации функционала наполнения показалось маловато - ввиду отсутствия команды эксплуатации и решения инцидентов силами Dev(Ops) информативность и точность алертов оказалась достаточно важной и пришлось делать свой вариант AlertManager, умеющий "обогащать" алерты дополнительной информацией, облегчающей разбор и анализ алертов, ну и плюс умеющий группировать алерты более сложным способом, чем умеет AlertManager
ebogdanov Автор
Мне было бы интересно про ваш опыт почитать или послушать более детально :)