Статья рассказывает о проекте внутри компании на стыке цифрового суверенитета, которому суждено быть погребенным в архиве корпоративного репозитория кода. Возможно, эта информация спасет кого-то в поисках решения проблем, стоящих перед ними или натолкнет на нечто большое и красивое.

Привет, я – Женя, работал в команде обеспечения надежности компании Ситимобил. Последние полгода занимался чем-то средним между 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) Уведомление дежурных о начале смены

  1. Отслеживаем вывод API schedules, затем пришло на ум два подхода:

  • создавать инцидент с текстом «Ура, начало вашей смены!»

  • отсылать пуши, когда видим что дежурный поменялся

  1. Используем стандартную функциональность OnCall: интеграцию с календарем в формате iCal, тогда уведомления будут приходить в корпоративный календарь.

Мне нравится идея с созданием инцидента, так как проверяется вся цепочка: создание алерта, отправка сообщений о нем, эскалация если оповещение не дошло и не было погашено.

Если кто пойдет по этим стопам и придумает другой подход к проблеме, напишите потом в личку, пожалуйста  – мне почему-то важно знать как тут можно было сделать еще.

Итоги

MVP-версия была реализована, запущена и успешно протестирована, можно даже сказать что внедрена в использование. Grafana OnCall подхватывает нашу реализацию как родную, успешно расширяет свою функциональность, отправляет оповещения об алертах в нужном нам формате по нужным каналам, без необходимости приобретать подписку каких-либо иностранных сервисов.

Схема взаимодействия сервисов получилась примерно такая:

Да, здесь есть над чем работать в отношении количества потенциальных точек отказа.

Я бы в дальнейшем интегрировал в Эмулятор отсылку пушей и клиент к Asterisk + TTS, или подключил сервисы через MQ-очередь, а не HTTP API как это сделано сейчас.

Но текущая схема проста, её можно было быстро собрать из тех кубиков которые уже имелись, а с таким мощным фундаментом в дальнейшем уже можно продолжать работу над стабильностью и расширением функционала.

На экране телефона выглядело так:

На первом скрине видны изменения в тексте, которые невозможно достичь в базовой версии OnCall. К сожалению, логика поиска FQDN сервиса с помощью регулярного выражения срабатывала не всегда. Как и здесь :)

Добились полного контроля над продуктом, который можем хостить в удобном нам месте и делать с ним все что требуется для покрытия нужд дежурных инженеров. Нет боязни остаться без инцидент-менеджмента в любой момент, когда узнают про белорусский аккаунт и его отключат. Кодовая база получилось небольшая и понятная, ее можно было легко поддерживать как прослойку и передавать в другие команды в дальнейшем.

Расширение функциональности тоже не было бы проблемой, следующей идеей на очереди выступала поддержка мобильного приложения Grafana OnCall. На проработке пара идей – руки уже не дошли, сокращение пришло быстрее :)

Из проблем могу назвать только что UI Grafana OnCall не готов к тому что облачный инстанс может не отвечать, и если так случается - показывает ошибку что ситуация ой, работать не буду. Я нахожу это странным, ведь для работы UI постоянное подключение к облаку не нужно. Думаю что это можно поправить простым PR в их репозитарий, у меня, к сожалению, руки не дошли.

Комментарии (6)


  1. punilki
    13.06.2023 11:35

    а что мешало поставить локально OSS OnCall и ним экспериментировать?

    Для дежурств мы используем гугл-календарь, OnCall следит за ним и делает ротацию сам, оповещая дежурных.

    ну и основная проблема алертов экосистемы Prometheus - мешанина из информации и отвратительный UX без напильника, судя по скринам, так и не решена


    1. ebogdanov Автор
      13.06.2023 11:35

      Считаю что это хорошо если у вас есть время попереживать про UX в таком деле :) У меня в OnCall только создание расписания доставляет боль чуть ниже спины.

      Про Google Calendar отличная идея, – спасибо.

      1) OSS OnCall не умеет дозваниваться и отправлять СМС/Пуши. В Телегу/Слак/Email - пожалуйста прямо из коробки. Нужная же команде функциональность работает только если есть подключение к Grafana OnCall Cloud, или к Twilio - поэтому и пришлось изучить связку и сделать эмулятор, так как API у OnCall проще в реализации.

      2) В таком наборе можно решать что угодно по информационному наполнению сообщения - у нас сокращение штата прошло раньше чем до этого руки дошли. Но по тому что прилетает в нашем конкретном случае порядок навели и внедрили общий стандарт.


      1. punilki
        13.06.2023 11:35

        1. Тут соглашусь, для OSS нужен сторонний провайдер звонков-пушей, у нас как раз Twilio

        2. Увы, нам в процессе эксплуатации функционала наполнения показалось маловато - ввиду отсутствия команды эксплуатации и решения инцидентов силами Dev(Ops) информативность и точность алертов оказалась достаточно важной и пришлось делать свой вариант AlertManager, умеющий "обогащать" алерты дополнительной информацией, облегчающей разбор и анализ алертов, ну и плюс умеющий группировать алерты более сложным способом, чем умеет AlertManager


        1. ebogdanov Автор
          13.06.2023 11:35

          Мне было бы интересно про ваш опыт почитать или послушать более детально :)


  1. 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 или пользоваться именно нашим мобильным приложением, которое шлет пуши через наш клауд (какой-то облачный сервис для этого нужен, мы запилили свой). Контрибьютеры позитивно относятся к расширению функционала в ту сторону, которая обеспечит вам большую независимость.


  1. Matvey-Kuk
    13.06.2023 11:35

    А еще, у нас есть русскоязычное коммьюнити, присоединяйтесь: https://t.me/amixr_ru