""" Модуль для обработки алертов из Zabbix. Автор: Сергей Антропов Сайт: https://devops.org.ru """ import logging from typing import Tuple, Optional from jinja2 import Environment, FileSystemLoader from telegram import InlineKeyboardButton, InlineKeyboardMarkup from app.models.zabbix import ZabbixAlert 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(severity: Optional[str], status: str) -> Tuple[str, str, str]: """ Получить иконки и название статуса в зависимости от серьезности и статуса алерта. Args: severity: Уровень серьезности (Disaster, High, Warning, Average, Information). status: Статус события (OK, PROBLEM). Returns: Кортеж (alert_icon, status_icon, status_name). """ severity = severity or "Information" status_map = { ("Disaster", "PROBLEM"): ("🔴", "💀", "Катастрофа. Бросаем все и чиним."), ("Disaster", "OK"): ("🟢", "💀", "Катастрофы избежали. Все работает. Пошли смотреть логи!"), ("High", "PROBLEM"): ("🟠", "😡", "Оперативно реагируем, диагностируем и чиним."), ("High", "OK"): ("🟢", "😡", "Отреагировали. Продиагностировали и починили."), ("Warning", "PROBLEM"): ("🟡", "🤔", "Ой. Что-то сломалось! Пойдем посмотрим?!"), ("Warning", "OK"): ("🟢", "🤔", "Посмотрели. Доламывать не стали. Починили."), ("Average", "PROBLEM"): ("🔵", "😒", "Ну такое себе. Но можно глянуть..."), ("Average", "OK"): ("🟢", "😒", "Пока ничего критичного. Само починилось"), ("Information", "PROBLEM"): ("🟣", "👻", "Ничего критичного. Просто информирую"), ("Information", "OK"): ("🟢", "👻", "Все молодцы. IT + DevOps = 🤝"), } return status_map.get((severity, status), ("🔸", "ℹ️", severity)) async def send(group_name: str, thread_id: int, alert: ZabbixAlert, messenger: Optional[str] = None) -> None: """ Отправить алерт из Zabbix в мессенджер. Args: group_name: Имя группы из конфигурации. thread_id: ID треда в группе (0 для основной группы). alert: Данные алерта из Zabbix. messenger: Тип мессенджера (опционально, если не указан - используется из конфигурации группы). Raises: ValueError: Если группа не найдена в конфигурации. """ source = "zabbix" 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.status == "PROBLEM") or (settings.jira_create_on_resolved and alert.status == "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.status == "PROBLEM": metrics.increment_firing_message(source, k8s_cluster, group_name, thread_id) elif alert.status == "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: ZabbixAlert) -> Tuple[str, InlineKeyboardMarkup]: """ Сформировать сообщение и кнопки для мессенджера из алерта Zabbix. Args: group_name: Имя группы. thread_id: ID треда в группе. alert: Данные алерта из Zabbix. Returns: Кортеж (message, buttons). """ # Получаем иконки статуса severity = alert.event_severity or "Information" alert_icon, status_icon, status_name = _get_status_icons(severity, alert.status) # Формируем словарь для шаблона message_dict = { 'state': severity, 'alert_icon': alert_icon, 'status_icon': status_icon, 'status_name': status_name, 'title': alert.event_name, 'subject': alert.alert_subject, 'message': alert.alert_message, 'message_data': alert.event_opdata or '', 'label_date': alert.event_recovery_date or '', 'label_time': alert.event_recovery_time or '', 'label_duration': alert.event_duration or '', 'label_host': alert.host_name, 'label_ip': alert.host_ip, 'label_port': alert.host_port, } # Рендерим шаблон from app.core.config import get_settings settings = get_settings() environment = Environment(loader=FileSystemLoader(settings.templates_path)) template = environment.get_template("zabbix.tmpl") message = template.render(message_dict) # Формируем кнопки alert_url_path = alert.link.split("/")[-1] if alert.link else "" buttons = render_buttons(alert_url_path) logger.info("Сообщение Zabbix сформировано") return message, buttons def render_buttons(alert_url_path: str) -> InlineKeyboardMarkup: """ Сформировать кнопки для сообщения мессенджера. Args: alert_url_path: Путь к событию в Zabbix. Returns: InlineKeyboardMarkup с кнопками. """ from app.core.config import get_settings settings = get_settings() buttons = [] if settings.zabbix_url: # Кнопка Zabbix buttons.append([InlineKeyboardButton("Заббикс", url=settings.zabbix_url)]) # Кнопка перехода к алерту if alert_url_path: alert_url = f"{settings.zabbix_url}/{alert_url_path}" buttons.append([InlineKeyboardButton("Перейти к алерту", url=alert_url)]) markup = InlineKeyboardMarkup(buttons) if buttons else None logger.debug("Кнопки Zabbix сгенерированы") return markup