- FastAPI приложение для отправки мониторинговых алертов в мессенджеры - Поддержка Telegram и MAX/VK - Интеграция с Grafana, Zabbix, AlertManager - Автоматическое создание тикетов в Jira - Управление группами мессенджеров через API - Декораторы для авторизации и скрытия эндпоинтов - Подробная документация в папке docs/ Автор: Сергей Антропов Сайт: https://devops.org.ru
262 lines
10 KiB
Python
262 lines
10 KiB
Python
"""
|
||
Модуль для обработки алертов из Grafana.
|
||
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
import logging
|
||
from typing import Tuple, Optional, Dict, Any
|
||
from jinja2 import Environment, FileSystemLoader
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||
|
||
from app.models.grafana import GrafanaAlert
|
||
from app.core.groups import groups_config
|
||
from app.core.metrics import metrics
|
||
from app.core.jira_utils import create_jira_ticket_from_alert
|
||
from app.core.utils import truncate_message
|
||
from app.core.messenger_factory import MessengerFactory
|
||
from app.core.button_utils import convert_telegram_buttons_to_dict
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _get_status_icons(state: str) -> Tuple[str, str, str]:
|
||
"""
|
||
Получить иконки и название статуса в зависимости от состояния алерта.
|
||
|
||
Args:
|
||
state: Состояние алерта из Grafana.
|
||
|
||
Returns:
|
||
Кортеж (alert_icon, status_icon, status_name).
|
||
"""
|
||
status_map = {
|
||
"alerting": ("🔴", "💀", "Бросаем все и чиним"),
|
||
"ok": ("🟢", "✅", "Заработало"),
|
||
"paused": ("🟡", "🐢", "Пауза? Серьезно?!"),
|
||
"pending": ("🟠", "🤷", "Что-то начинается..."),
|
||
"no_data": ("🔵", "🥴", "Данные куда-то пропали"),
|
||
}
|
||
|
||
return status_map.get(state, ("🔸", "ℹ️", state))
|
||
|
||
|
||
async def send(group_name: str, thread_id: int, alert: GrafanaAlert, messenger: Optional[str] = None) -> None:
|
||
"""
|
||
Отправить алерт из Grafana в мессенджер.
|
||
|
||
Args:
|
||
group_name: Имя группы из конфигурации.
|
||
thread_id: ID треда в группе (0 для основной группы).
|
||
alert: Данные алерта из Grafana.
|
||
messenger: Тип мессенджера (опционально, если не указан - используется из конфигурации группы).
|
||
|
||
Raises:
|
||
ValueError: Если группа не найдена в конфигурации.
|
||
"""
|
||
source = "grafana"
|
||
k8s_cluster = ""
|
||
|
||
# Увеличиваем счетчик полученных сообщений
|
||
metrics.increment_total_message(source, k8s_cluster, group_name, thread_id)
|
||
|
||
# Получаем конфигурацию группы
|
||
group_config = await groups_config.get_group_config(group_name, messenger)
|
||
if group_config is None:
|
||
raise ValueError(f"Группа '{group_name}' не найдена в конфигурации")
|
||
|
||
messenger_type = group_config.get("messenger", "telegram")
|
||
chat_id = group_config.get("chat_id")
|
||
group_thread_id = group_config.get("thread_id", 0)
|
||
|
||
# Используем thread_id из параметра, если указан, иначе из конфигурации группы
|
||
final_thread_id = thread_id if thread_id > 0 else group_thread_id
|
||
|
||
# Создаем клиент мессенджера
|
||
messenger_client = MessengerFactory.create_from_config(group_config)
|
||
|
||
# Проверяем поддержку тредов
|
||
if not messenger_client.supports_threads and final_thread_id > 0:
|
||
logger.warning(f"Мессенджер '{messenger_type}' не поддерживает треды, thread_id будет проигнорирован")
|
||
final_thread_id = None
|
||
elif final_thread_id == 0:
|
||
final_thread_id = None
|
||
|
||
# Формируем сообщение
|
||
message, buttons = render_message(group_name, thread_id, alert)
|
||
|
||
# Обрезаем сообщение если оно слишком длинное
|
||
message = truncate_message(message)
|
||
|
||
# Создаем Jira тикет, если включено и нужно
|
||
jira_issue_key = None
|
||
from app.core.config import get_settings
|
||
settings = get_settings()
|
||
|
||
if settings.jira_enabled:
|
||
should_create_ticket = (
|
||
(settings.jira_create_on_alert and alert.state == "alerting") or
|
||
(settings.jira_create_on_resolved and alert.state == "ok")
|
||
)
|
||
|
||
if should_create_ticket:
|
||
try:
|
||
jira_issue_key = await create_jira_ticket_from_alert(
|
||
alert=alert,
|
||
source=source,
|
||
k8s_cluster=k8s_cluster
|
||
)
|
||
if jira_issue_key:
|
||
# Добавляем кнопку Jira в сообщение
|
||
jira_button = _create_jira_button(jira_issue_key, settings)
|
||
if jira_button:
|
||
if buttons:
|
||
# Добавляем кнопку к существующим кнопкам
|
||
new_buttons = buttons.inline_keyboard.copy()
|
||
new_buttons.append([jira_button])
|
||
buttons = InlineKeyboardMarkup(new_buttons)
|
||
else:
|
||
# Если кнопок еще нет, создаем новую клавиатуру
|
||
buttons = InlineKeyboardMarkup([[jira_button]])
|
||
except Exception as e:
|
||
logger.error(f"Ошибка создания Jira тикета: {e}")
|
||
|
||
# Преобразуем кнопки в универсальный формат
|
||
buttons_dict = convert_telegram_buttons_to_dict(buttons)
|
||
|
||
# Отправляем сообщение
|
||
success = await messenger_client.send_message(
|
||
chat_id=chat_id,
|
||
text=message,
|
||
thread_id=final_thread_id,
|
||
reply_markup=buttons_dict,
|
||
disable_web_page_preview=True,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
if success:
|
||
metrics.increment_sent_message(source, k8s_cluster, group_name, thread_id)
|
||
|
||
# Увеличиваем счетчики в зависимости от состояния
|
||
if alert.state == "alerting":
|
||
metrics.increment_firing_message(source, k8s_cluster, group_name, thread_id)
|
||
elif alert.state == "ok":
|
||
metrics.increment_resolved_message(source, k8s_cluster, group_name, thread_id)
|
||
else:
|
||
metrics.increment_error_message(source, k8s_cluster, group_name, thread_id)
|
||
raise Exception(f"Ошибка отправки сообщения в {group_config.get('messenger', 'unknown')}")
|
||
|
||
|
||
def _create_jira_button(issue_key: str, settings) -> Optional[InlineKeyboardButton]:
|
||
"""
|
||
Создать кнопку для ссылки на Jira тикет.
|
||
|
||
Args:
|
||
issue_key: Ключ тикета Jira (например, "MON-123").
|
||
settings: Настройки приложения.
|
||
|
||
Returns:
|
||
InlineKeyboardButton с ссылкой на тикет или None.
|
||
"""
|
||
if not issue_key or not settings.jira_url:
|
||
return None
|
||
|
||
jira_url = f"{settings.jira_url}/browse/{issue_key}"
|
||
return InlineKeyboardButton(f"📋 Jira: {issue_key}", url=jira_url)
|
||
|
||
|
||
def render_message(group_name: str, thread_id: int, alert: GrafanaAlert) -> Tuple[str, InlineKeyboardMarkup]:
|
||
"""
|
||
Сформировать сообщение и кнопки для мессенджера из алерта Grafana.
|
||
|
||
Args:
|
||
group_name: Имя группы.
|
||
thread_id: ID треда в группе.
|
||
alert: Данные алерта из Grafana.
|
||
|
||
Returns:
|
||
Кортеж (message, buttons).
|
||
"""
|
||
# Получаем иконки статуса
|
||
alert_icon, status_icon, status_name = _get_status_icons(alert.state)
|
||
|
||
# Формируем словарь для шаблона
|
||
message_dict = {
|
||
'state': alert.state,
|
||
'alert_icon': alert_icon,
|
||
'status_icon': status_icon,
|
||
'status_name': status_name,
|
||
'title': alert.title,
|
||
'message': alert.message or '',
|
||
'ruleid': alert.ruleId,
|
||
'rulename': alert.ruleName,
|
||
'orgid': alert.orgId,
|
||
'dashboardid': alert.dashboardId,
|
||
'panelid': alert.panelId,
|
||
}
|
||
|
||
# Обрабатываем evalMatches
|
||
labels_text = ""
|
||
for eval_match in alert.evalMatches:
|
||
labels_text += f"<b>{eval_match.metric}:</b> {eval_match.value}\n"
|
||
message_dict['labels'] = labels_text
|
||
|
||
# Обрабатываем теги
|
||
tags_text = ""
|
||
for key, value in alert.tags.items():
|
||
tags_text += f"<b>{key}:</b> {value}\n"
|
||
message_dict['tags'] = tags_text
|
||
|
||
# Получаем URL дашборда
|
||
dashboard_url = alert.ruleUrl.split("?")[0] if alert.ruleUrl else ""
|
||
rule_url = alert.ruleUrl
|
||
|
||
# Рендерим шаблон
|
||
from app.core.config import get_settings
|
||
settings = get_settings()
|
||
|
||
environment = Environment(loader=FileSystemLoader(settings.templates_path))
|
||
template = environment.get_template("grafana.tmpl")
|
||
message = template.render(message_dict)
|
||
|
||
# Формируем кнопки
|
||
buttons = render_buttons(dashboard_url, rule_url)
|
||
|
||
logger.info("Сообщение Grafana сформировано")
|
||
return message, buttons
|
||
|
||
|
||
def render_buttons(dashboard_url: str, rule_url: str) -> InlineKeyboardMarkup:
|
||
"""
|
||
Сформировать кнопки для сообщения мессенджера.
|
||
|
||
Args:
|
||
dashboard_url: URL дашборда Grafana.
|
||
rule_url: URL правила алерта.
|
||
|
||
Returns:
|
||
InlineKeyboardMarkup с кнопками.
|
||
"""
|
||
from app.core.config import get_settings
|
||
settings = get_settings()
|
||
|
||
buttons = []
|
||
|
||
# Главная кнопка Grafana
|
||
if settings.grafana_url:
|
||
buttons.append([InlineKeyboardButton("Графана", url=settings.grafana_url)])
|
||
|
||
# Кнопки дашборда и алерта
|
||
row_buttons = []
|
||
if dashboard_url:
|
||
row_buttons.append(InlineKeyboardButton("Дашборд", url=dashboard_url))
|
||
if rule_url:
|
||
row_buttons.append(InlineKeyboardButton("Алерт", url=rule_url))
|
||
|
||
if row_buttons:
|
||
buttons.append(row_buttons)
|
||
|
||
markup = InlineKeyboardMarkup(buttons) if buttons else None
|
||
logger.debug("Кнопки Grafana сгенерированы")
|
||
return markup
|