""" Модуль для обработки алертов из 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"{eval_match.metric}: {eval_match.value}\n" message_dict['labels'] = labels_text # Обрабатываем теги tags_text = "" for key, value in alert.tags.items(): tags_text += f"{key}: {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