Initial commit: Message Gateway project

- FastAPI приложение для отправки мониторинговых алертов в мессенджеры
- Поддержка Telegram и MAX/VK
- Интеграция с Grafana, Zabbix, AlertManager
- Автоматическое создание тикетов в Jira
- Управление группами мессенджеров через API
- Декораторы для авторизации и скрытия эндпоинтов
- Подробная документация в папке docs/

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
2025-11-12 20:25:11 +03:00
commit b90def35ed
72 changed files with 10609 additions and 0 deletions

0
app/modules/__init__.py Normal file
View File

311
app/modules/alertmanager.py Normal file
View File

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

261
app/modules/grafana.py Normal file
View File

@@ -0,0 +1,261 @@
"""
Модуль для обработки алертов из Grafana.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import logging
from typing import Tuple, Optional, Dict, Any
from jinja2 import Environment, FileSystemLoader
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from app.models.grafana import GrafanaAlert
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(state: str) -> Tuple[str, str, str]:
"""
Получить иконки и название статуса в зависимости от состояния алерта.
Args:
state: Состояние алерта из Grafana.
Returns:
Кортеж (alert_icon, status_icon, status_name).
"""
status_map = {
"alerting": ("🔴", "💀", "Бросаем все и чиним"),
"ok": ("🟢", "", "Заработало"),
"paused": ("🟡", "🐢", "Пауза? Серьезно?!"),
"pending": ("🟠", "🤷", "Что-то начинается..."),
"no_data": ("🔵", "🥴", "Данные куда-то пропали"),
}
return status_map.get(state, ("🔸", "", state))
async def send(group_name: str, thread_id: int, alert: GrafanaAlert, messenger: Optional[str] = None) -> None:
"""
Отправить алерт из Grafana в мессенджер.
Args:
group_name: Имя группы из конфигурации.
thread_id: ID треда в группе (0 для основной группы).
alert: Данные алерта из Grafana.
messenger: Тип мессенджера (опционально, если не указан - используется из конфигурации группы).
Raises:
ValueError: Если группа не найдена в конфигурации.
"""
source = "grafana"
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.state == "alerting") or
(settings.jira_create_on_resolved and alert.state == "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.state == "alerting":
metrics.increment_firing_message(source, k8s_cluster, group_name, thread_id)
elif alert.state == "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: GrafanaAlert) -> Tuple[str, InlineKeyboardMarkup]:
"""
Сформировать сообщение и кнопки для мессенджера из алерта Grafana.
Args:
group_name: Имя группы.
thread_id: ID треда в группе.
alert: Данные алерта из Grafana.
Returns:
Кортеж (message, buttons).
"""
# Получаем иконки статуса
alert_icon, status_icon, status_name = _get_status_icons(alert.state)
# Формируем словарь для шаблона
message_dict = {
'state': alert.state,
'alert_icon': alert_icon,
'status_icon': status_icon,
'status_name': status_name,
'title': alert.title,
'message': alert.message or '',
'ruleid': alert.ruleId,
'rulename': alert.ruleName,
'orgid': alert.orgId,
'dashboardid': alert.dashboardId,
'panelid': alert.panelId,
}
# Обрабатываем evalMatches
labels_text = ""
for eval_match in alert.evalMatches:
labels_text += f"<b>{eval_match.metric}:</b> {eval_match.value}\n"
message_dict['labels'] = labels_text
# Обрабатываем теги
tags_text = ""
for key, value in alert.tags.items():
tags_text += f"<b>{key}:</b> {value}\n"
message_dict['tags'] = tags_text
# Получаем URL дашборда
dashboard_url = alert.ruleUrl.split("?")[0] if alert.ruleUrl else ""
rule_url = alert.ruleUrl
# Рендерим шаблон
from app.core.config import get_settings
settings = get_settings()
environment = Environment(loader=FileSystemLoader(settings.templates_path))
template = environment.get_template("grafana.tmpl")
message = template.render(message_dict)
# Формируем кнопки
buttons = render_buttons(dashboard_url, rule_url)
logger.info("Сообщение Grafana сформировано")
return message, buttons
def render_buttons(dashboard_url: str, rule_url: str) -> InlineKeyboardMarkup:
"""
Сформировать кнопки для сообщения мессенджера.
Args:
dashboard_url: URL дашборда Grafana.
rule_url: URL правила алерта.
Returns:
InlineKeyboardMarkup с кнопками.
"""
from app.core.config import get_settings
settings = get_settings()
buttons = []
# Главная кнопка Grafana
if settings.grafana_url:
buttons.append([InlineKeyboardButton("Графана", url=settings.grafana_url)])
# Кнопки дашборда и алерта
row_buttons = []
if dashboard_url:
row_buttons.append(InlineKeyboardButton("Дашборд", url=dashboard_url))
if rule_url:
row_buttons.append(InlineKeyboardButton("Алерт", url=rule_url))
if row_buttons:
buttons.append(row_buttons)
markup = InlineKeyboardMarkup(buttons) if buttons else None
logger.debug("Кнопки Grafana сгенерированы")
return markup

252
app/modules/zabbix.py Normal file
View File

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