From 0169bf5d6b8c6ff9b513bdba2f421334434b4c89 Mon Sep 17 00:00:00 2001 From: Vladislav Zverev Date: Tue, 17 Jun 2025 23:44:30 +0500 Subject: [PATCH] refactor(alerts): improve active problem fetching and message formatting Reworked the logic for retrieving data from Zabbix API to make it more efficient and filter-aware. Message generation for Telegram bot was refactored and decoupled from data retrieval logic to improve structure, readability, and reuse. Signed-off-by: UdoChudo --- app/bot/handlers/active_triggers.py | 19 ++- app/bot/keyboards/active_triggers.py | 7 -- app/bot/keyboards/groups.py | 23 ++++ .../processors/active_triggers_processor.py | 54 ++++---- app/bot/utils/zabbix.py | 119 ------------------ app/bot/utils/zabbix_alt.py | 103 +++++++++++++++ 6 files changed, 171 insertions(+), 154 deletions(-) create mode 100644 app/bot/keyboards/groups.py delete mode 100644 app/bot/utils/zabbix.py create mode 100644 app/bot/utils/zabbix_alt.py diff --git a/app/bot/handlers/active_triggers.py b/app/bot/handlers/active_triggers.py index f53eb17..db6deae 100644 --- a/app/bot/handlers/active_triggers.py +++ b/app/bot/handlers/active_triggers.py @@ -1,12 +1,12 @@ from telebot.types import Message, CallbackQuery from app.bot.keyboards.active_triggers import create_region_keyboard -from app.bot.utils.regions import get_sorted_regions_plain from app.bot.processors.active_triggers_processor import ( process_region_selection, process_group_selection, process_all_groups_request, ) +from app.bot.utils.regions import get_sorted_regions_plain def register_active_triggers(bot, app, state_manager): @@ -22,17 +22,28 @@ def register_callbacks_active_triggers(bot,app,state_manager): @bot.callback_query_handler(func=lambda c: c.data.startswith("region_")) def region_selected(callback_query: CallbackQuery): region_id = callback_query.data.split("_")[1] - process_region_selection(callback_query.message.chat.id,bot, region_id) + chat_id = callback_query.message.chat.id + bot.answer_callback_query(callback_query.id) + bot.delete_message(chat_id, callback_query.message.message_id) + process_region_selection(bot, chat_id, region_id) @bot.callback_query_handler(func=lambda c: c.data.startswith("group_")) def group_selected(callback_query: CallbackQuery): group_id = callback_query.data.split("_")[1] - process_group_selection(callback_query.message.chat.id,bot, group_id) + chat_id = callback_query.message.chat.id + bot.answer_callback_query(callback_query.id) + bot.delete_message(chat_id, callback_query.message.message_id) + bot.send_message(chat_id, f"Обработка...") + process_group_selection(bot, chat_id, group_id) @bot.callback_query_handler(func=lambda c: c.data.startswith("all_groups_")) def all_groups_selected(callback_query: CallbackQuery): region_id = callback_query.data.split("_")[2] - process_all_groups_request(callback_query.message.chat.id,bot, region_id) + chat_id = callback_query.message.chat.id + bot.answer_callback_query(callback_query.id) + bot.delete_message(chat_id, callback_query.message.message_id) + bot.send_message(chat_id, f"Обработка...") + process_all_groups_request(bot, chat_id, region_id) @bot.callback_query_handler(func=lambda c: c.data.startswith("regions_page_")) def regions_page_selected(callback_query: CallbackQuery): diff --git a/app/bot/keyboards/active_triggers.py b/app/bot/keyboards/active_triggers.py index d1879ff..6403fae 100644 --- a/app/bot/keyboards/active_triggers.py +++ b/app/bot/keyboards/active_triggers.py @@ -30,10 +30,3 @@ def create_region_keyboard(regions, page, page_size=REGIONS_PER_PAGE): markup.add(cancel_button) return markup - -def create_group_keyboard(groups, region_id): - markup = InlineKeyboardMarkup() - for group in groups: - markup.add(InlineKeyboardButton(text=group['name'], callback_data=f"group_{group['groupid']}")) - markup.add(InlineKeyboardButton(text="Все группы региона\n(Долгое выполнение)", callback_data=f"all_groups_{region_id}")) - return markup diff --git a/app/bot/keyboards/groups.py b/app/bot/keyboards/groups.py new file mode 100644 index 0000000..0a6a92c --- /dev/null +++ b/app/bot/keyboards/groups.py @@ -0,0 +1,23 @@ +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton + +def create_groups_keyboard(groups, region_id): + """ + Формирует InlineKeyboardMarkup для выбора группы хостов. + + :param groups: список словарей с группами, у каждой есть 'name' и 'groupid' + :param region_id: id региона, нужен для callback_data кнопки "Все группы региона" + :return: telebot.types.InlineKeyboardMarkup + """ + markup = InlineKeyboardMarkup() + for group in groups: + markup.add(InlineKeyboardButton( + text=group['name'], + callback_data=f"group_{group['groupid']}" + )) + markup.add(InlineKeyboardButton( + text="Все группы региона\n(Долгое выполнение)", + callback_data=f"all_groups_{region_id}" + )) + cancel_button = InlineKeyboardButton("Отмена", callback_data="cancel_input") + markup.add(cancel_button) + return markup diff --git a/app/bot/processors/active_triggers_processor.py b/app/bot/processors/active_triggers_processor.py index 676706c..8837182 100644 --- a/app/bot/processors/active_triggers_processor.py +++ b/app/bot/processors/active_triggers_processor.py @@ -1,19 +1,20 @@ -from telebot import types -from app.bot.keyboards.main_menu import get_main_menu -from app.bot.utils.zabbix import get_region_groups, get_all_groups_for_region, fetch_filtered_triggers -from app.bot.utils.tg_formatter import format_trigger_message # ⬅️ добавлено +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton -def process_region_selection(bot,chat_id, region_id): +from app.bot.keyboards.groups import create_groups_keyboard +from app.bot.keyboards.main_menu import get_main_menu +from app.bot.utils.tg_formatter import format_trigger_for_tg +from app.bot.utils.zabbix_alt import ( + get_region_groups, + get_all_groups_for_region, + fetch_triggers_data) + + +def process_region_selection(bot, chat_id, region_id): try: groups = get_region_groups(region_id) if not groups: return bot.send_message(chat_id, "Нет групп хостов для этого региона.") - - markup = types.InlineKeyboardMarkup() - for group in groups: - markup.add(types.InlineKeyboardButton(text=group['name'], callback_data=f"group_{group['groupid']}")) - markup.add(types.InlineKeyboardButton(text="Все группы региона\n(Долгое выполнение)", callback_data=f"all_groups_{region_id}")) - + markup = create_groups_keyboard(groups, region_id) bot.send_message(chat_id, "Выберите группу хостов:", reply_markup=markup) except Exception as e: bot.send_message(chat_id, f"Ошибка при получении групп: {str(e)}", reply_markup=get_main_menu()) @@ -21,11 +22,16 @@ def process_region_selection(bot,chat_id, region_id): def process_group_selection(bot, chat_id, group_id): try: - triggers = fetch_filtered_triggers(group_id) + triggers = fetch_triggers_data(group_id) if not triggers: bot.send_message(chat_id, "Нет активных событий.") - else: - send_trigger_messages(chat_id, triggers) + return + + for trigger in triggers: + text, url = format_trigger_for_tg(trigger) + markup = InlineKeyboardMarkup() + markup.add(InlineKeyboardButton(text="Открыть график", url=url)) + bot.send_message(chat_id, text, reply_markup=markup, parse_mode="HTML") except Exception as e: bot.send_message(chat_id, f"Ошибка при получении событий: {str(e)}") @@ -36,21 +42,21 @@ def process_all_groups_request(bot, chat_id, region_id): groups = get_all_groups_for_region(region_id) for group in groups: try: - triggers = fetch_filtered_triggers(group['groupid']) + triggers = fetch_triggers_data(group['groupid']) if triggers: all_triggers.extend(triggers) except Exception: continue - if all_triggers: - send_trigger_messages(chat_id, all_triggers) - else: + if not all_triggers: bot.send_message(chat_id, "Нет активных событий.") + return + + for trigger in all_triggers: + text, url = format_trigger_for_tg(trigger) + markup = InlineKeyboardMarkup() + markup.add(InlineKeyboardButton(text="Открыть график", url=url)) + bot.send_message(chat_id, text, reply_markup=markup, parse_mode="HTML") + except Exception as e: bot.send_message(chat_id, f"Ошибка при получении данных: {str(e)}") - - -def send_trigger_messages(chat_id, triggers): - for trigger in triggers: - text = format_trigger_message(trigger) - bot.send_message(chat_id, text, parse_mode="MarkdownV2") diff --git a/app/bot/utils/zabbix.py b/app/bot/utils/zabbix.py deleted file mode 100644 index 5a88f1e..0000000 --- a/app/bot/utils/zabbix.py +++ /dev/null @@ -1,119 +0,0 @@ -import time -from datetime import datetime -from pytz import timezone -from pyzabbix import ZabbixAPI, ZabbixAPIException -from telebot import logger - -from config import ZABBIX_URL, ZABBIX_API_TOKEN, ZABBIX_VERIFY_SSL, ZABBIX_TZ -from app.bot.utils.tg_escape_chars import escape_telegram_chars -TZ = timezone(ZABBIX_TZ) -verify_ssl = ZABBIX_VERIFY_SSL - -def get_region_groups(region_id: str): - """ - Получает список групп, имя которых содержит регион region_id, исключая 'test'. - """ - try: - zapi = ZabbixAPI(ZABBIX_URL) - zapi.login(api_token=ZABBIX_API_TOKEN) - zapi.session.verify = verify_ssl - - host_groups = zapi.hostgroup.get(output=["groupid", "name"], search={"name": region_id}) - filtered_groups = [group for group in host_groups if 'test' not in group['name'].lower()] - return filtered_groups - except Exception as e: - logger.error(f"Error getting region groups for '{region_id}': {e}") - return [] - -def get_all_groups_for_region(region_id: str): - """ - Аналогично get_region_groups, получение всех групп по региону. - """ - return get_region_groups(region_id) - -def fetch_filtered_triggers(group_id): - """ - Получение и фильтрация триггеров с severities 4 и 5, - формирование HTML сообщений для отправки в Telegram. - """ - try: - zapi = ZabbixAPI(ZABBIX_URL) - zapi.login(api_token=ZABBIX_API_TOKEN) - zapi.session.verify = verify_ssl - - problems = zapi.problem.get( - severities=[4, 5], - suppressed=0, - acknowledged=0, - groupids=group_id - ) - trigger_ids = [problem["objectid"] for problem in problems] - - if not trigger_ids: - return [] - - triggers = zapi.trigger.get( - triggerids=trigger_ids, - output=["triggerid", "description", "priority"], - selectHosts=["hostid", "name"], - monitored=1, - expandDescription=1, - expandComment=1, - selectItems=["itemid", "lastvalue"], - selectLastEvent=["clock", "eventid"] - ) - - events = zapi.event.get( - severities=[4, 5], - objectids=trigger_ids, - select_alerts="mediatype" - ) - - pnet_mediatypes = {"Pnet integration JS 2025", "Pnet integration JS 2024", "Pnet integration new2"} - - pnet_triggers = [] - event_dict = {event["objectid"]: event for event in events} - - for trigger in triggers: - event = event_dict.get(trigger["triggerid"]) - if event: - for alert in event["alerts"]: - if alert["mediatypes"] and alert["mediatypes"][0]["name"] in pnet_mediatypes: - pnet_triggers.append(trigger) - break - - triggers_sorted = sorted(pnet_triggers, key=lambda t: int(t['lastEvent']['clock'])) - - - priority_map = {'4': 'HIGH', '5': 'DISASTER'} - trigger_messages = [] - - for trigger in triggers_sorted: - event_time_epoch = int(trigger['lastEvent']['clock']) - event_time = datetime.fromtimestamp(event_time_epoch, tz=TZ) - description = escape_telegram_chars(trigger['description']) - host = trigger['hosts'][0]['name'] - priority = priority_map.get(trigger['priority'], 'Неизвестно') - item_ids = [item['itemid'] for item in trigger['items']] - batchgraph_link = f"{ZABBIX_URL}/history.php?action=batchgraph&" - batchgraph_link += "&".join([f"itemids[{item_id}]={item_id}" for item_id in item_ids]) - batchgraph_link += "&graphtype=0" - description = description.replace("{HOST.NAME}", host) - for i, item in enumerate(trigger['items']): - lastvalue_placeholder = f"{{ITEM.LASTVALUE{i + 1}}}" - if lastvalue_placeholder in description: - description = description.replace(lastvalue_placeholder, item['lastvalue']) - event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск') - message = (f"Host: {host}\n" - f"Описание: {description}\n" - f"Критичность: {priority}\n" - f"Время создания: {event_time_formatted}\n" - f'URL: Ссылка на график') - trigger_messages.append(message) - - logger.info(f"Fetched {len(triggers_sorted)} triggers for group {group_id}.") - return trigger_messages - - except Exception as e: - logger.error(f"Error fetching triggers for group {group_id}: {e}") - return [] diff --git a/app/bot/utils/zabbix_alt.py b/app/bot/utils/zabbix_alt.py new file mode 100644 index 0000000..ce3c1d0 --- /dev/null +++ b/app/bot/utils/zabbix_alt.py @@ -0,0 +1,103 @@ +import re +import time + +from pyzabbix import ZabbixAPI, ZabbixAPIException +from telebot import logger + +from config import ZABBIX_URL, ZABBIX_API_TOKEN, ZABBIX_VERIFY_SSL + + +def get_region_groups(region_id: str): + """ + Получает список групп, имя которых содержит регион region_id, исключая 'test' и группы, не соответствующие шаблону 'имя_число'. + """ + try: + zapi = ZabbixAPI(ZABBIX_URL) + zapi.login(api_token=ZABBIX_API_TOKEN) + zapi.session.verify = ZABBIX_VERIFY_SSL + + host_groups = zapi.hostgroup.get(output=["groupid", "name"], search={"name": region_id}) + + pattern = re.compile(r'.+_\d+$') # строка с нижним подчёркиванием перед числом в конце + + filtered_groups = [ + group for group in host_groups + if 'test' not in group['name'].lower() and pattern.match(group['name']) + ] + return filtered_groups + + except Exception as e: + logger.error(f"[Zabbix] Error getting region groups for '{region_id}': {e}") + return [] + +def get_all_groups_for_region(region_id: str): + """ + Аналогично get_region_groups, получение всех групп по региону. + """ + return get_region_groups(region_id) + + +def fetch_triggers_data(group_id): + """ + Возвращает список триггеров с необходимыми данными, + без форматирования сообщений. + """ + pnet_mediatypes = {"Pnet integration JS 2025", "Pnet integration JS 2024", "Pnet integration new2"} + start_time = time.time() + try: + zapi = ZabbixAPI(ZABBIX_URL) + zapi.login(api_token=ZABBIX_API_TOKEN) + + # Получаем проблемы с высокой и критической важностью + problems = zapi.problem.get( + severities=[4, 5], + suppressed=0, + acknowledged=0, + groupids=group_id + ) + trigger_ids = [problem["objectid"] for problem in problems] + + if not trigger_ids: + logger.info(f"No triggers found for group {group_id}") + return [] + + triggers = zapi.trigger.get( + triggerids=trigger_ids, + output=["triggerid", "description", "priority"], + selectHosts=["hostid", "name"], + monitored=1, + expandDescription=1, + expandComment=1, + selectItems=["itemid", "lastvalue"], + selectLastEvent=["clock", "eventid"] + ) + + events = zapi.event.get( + severities=[4, 5], + objectids=trigger_ids, + select_alerts="mediatype" + ) + + event_dict = {event["objectid"]: event for event in events} + + pnet_triggers = [] + for trigger in triggers: + event = event_dict.get(trigger["triggerid"]) + if event: + for alert in event["alerts"]: + if alert["mediatypes"] and alert["mediatypes"][0]["name"] in pnet_mediatypes: + pnet_triggers.append(trigger) + break + + triggers_sorted = sorted(pnet_triggers, key=lambda t: int(t['lastEvent']['clock'])) + logger.debug(f"Found {len(pnet_triggers)} pnet triggers for group {group_id}") + end_time = time.time() + logger.info(f"[Zabbix] Fetched {len(triggers_sorted)} triggers for group {group_id} in {end_time - start_time:.2f} seconds.") + return triggers_sorted + + except ZabbixAPIException as e: + logger.error(f"[Zabbix] Zabbix API error for group {group_id}: {e}") + return [] + except Exception as e: + logger.error(f"[Zabbix] Unexpected error fetching triggers for group {group_id}: {e}") + return []