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

253 lines
11 KiB
Python
Raw Permalink 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.

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