""" Модуль для обработки алертов из AlertManager. Автор: Сергей Антропов Сайт: https://devops.org.ru """ import logging from typing import Tuple, Optional from jinja2 import Environment, FileSystemLoader from telegram import InlineKeyboardButton, InlineKeyboardMarkup from app.models.alertmanager import PrometheusAlert 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.jira_mapping import jira_mapping_manager from app.core.utils import check_stop_words, add_spaces_to_alert_name, 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(status: str, severity: Optional[str]) -> Tuple[str, str, str]: """ Получить иконки и название статуса в зависимости от статуса и серьезности алерта. Args: status: Статус алерта (firing, resolved, critical). severity: Уровень серьезности. Returns: Кортеж (alert_icon, status_icon, status_name). """ if status == "critical" or severity == "critical": return ("🔴", "💀", "Бросаем все и чиним") elif status == "firing" or severity == "firing" or severity == "warning": return ("🟡", "🔥", "Что-то сломалось") elif status == "resolved": return ("🟢", "✅", "Работает") else: return ("🔸", "ℹ️", status) async def send( k8s_cluster: str, group_name: str, thread_id: int, alert: PrometheusAlert, messenger: Optional[str] = None ) -> None: """ Отправить алерт из AlertManager в мессенджер. Args: k8s_cluster: Имя Kubernetes кластера. group_name: Имя группы из конфигурации. thread_id: ID треда в группе (0 для основной группы). alert: Данные алерта из AlertManager. messenger: Тип мессенджера (опционально, если не указан - используется из конфигурации группы). Raises: ValueError: Если группа не найдена в конфигурации или алерт заблокирован стоп-словами. """ source = "alertmanager" # Увеличиваем счетчик полученных сообщений metrics.increment_total_message(source, k8s_cluster, group_name, thread_id) # Проверяем стоп-слова alert_name = alert.commonLabels.get("alertname", "") if alert_name and check_stop_words(alert_name): logger.info(f"Алерт '{alert_name}' заблокирован стоп-словами") metrics.increment_reject_message(source, k8s_cluster, group_name, thread_id) return # Не отправляем сообщение # Получаем конфигурацию группы 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(k8s_cluster, 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 != "resolved") or (settings.jira_create_on_resolved and alert.status == "resolved") ) 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) # Увеличиваем счетчики в зависимости от статуса severity = alert.commonLabels.get("severity", "") if alert.status == "firing" or severity in ["firing", "warning"]: metrics.increment_firing_message(source, k8s_cluster, group_name, thread_id) elif alert.status == "critical" or severity == "critical": metrics.increment_critical_message(source, k8s_cluster, group_name, thread_id) elif alert.status == "resolved": 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( k8s_cluster: str, group_name: str, thread_id: int, alert: PrometheusAlert ) -> Tuple[str, Optional[InlineKeyboardMarkup]]: """ Сформировать сообщение и кнопки для мессенджера из алерта AlertManager. Args: k8s_cluster: Имя Kubernetes кластера. group_name: Имя группы. thread_id: ID треда в группе. alert: Данные алерта из AlertManager. Returns: Кортеж (message, buttons). """ message_dict = {} # Обрабатываем аннотации another_annotations = "" runbook_url = "" for key, value in alert.commonAnnotations.items(): if key == "summary": message_dict['summary'] = value.rstrip() elif key == "description": message_dict['description'] = value.rstrip() elif key == "runbook_url": message_dict['runbook_url'] = value runbook_url = value else: another_annotations += f"{key}: {value}\n" message_dict['another_annotations'] = another_annotations # Обрабатываем метки another_labels = "" severity = "" alertname = "" for key, value in alert.commonLabels.items(): if key == "alertname": alertname = add_spaces_to_alert_name(value) message_dict['alertname'] = alertname elif key == "severity": message_dict['severity'] = value severity = value elif key in [ "daemonset", "statefulset", "replicaset", "job_name", "To", "integration", "condition", "reason", "alertstate", "clustername", "namespace", "node", "persistentvolumeclaim", "service", "container", "endpoint", "instance", "job", "prometheus", "pod", "deployment", "metrics_path", "grpc_method", "grpc_service", "uid" ]: # Маппинг ключей для шаблона if key == "namespace": message_dict['ns'] = value else: message_dict[key] = value else: another_labels += f"{key}: {value}\n" message_dict['another_labels'] = another_labels # Получаем иконки статуса alert_icon, status_icon, status_name = _get_status_icons(alert.status, severity) message_dict['alert_icon'] = alert_icon message_dict['status_icon'] = status_icon message_dict['status_name'] = status_name # Рендерим шаблон from app.core.config import get_settings settings = get_settings() environment = Environment(loader=FileSystemLoader(settings.templates_path)) template = environment.get_template("alertmanager.tmpl") message = template.render(message_dict) # Формируем кнопки buttons = render_buttons(k8s_cluster, runbook_url, alert.status) logger.info("Сообщение AlertManager сформировано") return message, buttons def render_buttons( k8s_cluster: str, runbook_url: str, alert_status: str ) -> Optional[InlineKeyboardMarkup]: """ Сформировать кнопки для сообщения мессенджера. Args: k8s_cluster: Имя Kubernetes кластера. runbook_url: URL runbook с решением проблемы. alert_status: Статус алерта. Returns: InlineKeyboardMarkup с кнопками или None. """ from app.core.config import get_settings settings = get_settings() buttons = [] try: # Кнопки для мониторинга Kubernetes grafana_url = settings.get_k8s_grafana_url(k8s_cluster) prometheus_url = settings.get_k8s_prometheus_url(k8s_cluster) alertmanager_url = settings.get_k8s_alertmanager_url(k8s_cluster) buttons.append([ InlineKeyboardButton("Grafana", url=grafana_url), InlineKeyboardButton("Prometheus", url=prometheus_url), InlineKeyboardButton("Alertmanager", url=alertmanager_url) ]) except ValueError as e: logger.warning(f"Не удалось сформировать URL для Kubernetes: {e}") # Кнопка runbook (только для активных алертов) if runbook_url and alert_status != "resolved": buttons.append([InlineKeyboardButton("Вариант решения проблемы...", url=runbook_url)]) markup = InlineKeyboardMarkup(buttons) if buttons else None logger.debug("Кнопки AlertManager сгенерированы") return markup