- FastAPI приложение для отправки мониторинговых алертов в мессенджеры - Поддержка Telegram и MAX/VK - Интеграция с Grafana, Zabbix, AlertManager - Автоматическое создание тикетов в Jira - Управление группами мессенджеров через API - Декораторы для авторизации и скрытия эндпоинтов - Подробная документация в папке docs/ Автор: Сергей Антропов Сайт: https://devops.org.ru
253 lines
11 KiB
Python
253 lines
11 KiB
Python
"""
|
||
Модуль для обработки алертов из 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
|