Files
MessageGateway/app/modules/grafana.py
Sergey Antropov b90def35ed Initial commit: Message Gateway project
- FastAPI приложение для отправки мониторинговых алертов в мессенджеры
- Поддержка Telegram и MAX/VK
- Интеграция с Grafana, Zabbix, AlertManager
- Автоматическое создание тикетов в Jira
- Управление группами мессенджеров через API
- Декораторы для авторизации и скрытия эндпоинтов
- Подробная документация в папке docs/

Автор: Сергей Антропов
Сайт: https://devops.org.ru
2025-11-12 20:25:11 +03:00

262 lines
10 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Модуль для обработки алертов из 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