Files
MessageGateway/app/modules/alertmanager.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

312 lines
13 KiB
Python
Raw 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.

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