import re import os import requests import time from datetime import datetime, date from collections import defaultdict import configparser import logging import schedule import threading import sys # Налаштування логів LOG_FILE_PATH = '/opt/olt_monitor/olt_monitor.log' LAN_EVENTS_LOG = '/opt/olt_monitor/lan_events.log' SENT_MESSAGES_FILE = '/opt/olt_monitor/sent_messages.txt' LAST_RESET_FILE = '/opt/olt_monitor/last_log_reset.txt' LAST_ACTIVATION_FILE = '/opt/olt_monitor/last_activation.txt' # Створення lan_events.log при старті, якщо не існує if not os.path.exists(LAN_EVENTS_LOG): try: with open(LAN_EVENTS_LOG, 'w', encoding='utf-8') as f: f.write('') logging.info(f"Створено файл {LAN_EVENTS_LOG}") except Exception as e: logging.error(f"Помилка створення {LAN_EVENTS_LOG}: {e}") raise # Налаштування логгера для lan_events.log lan_logger = logging.getLogger('lan_events') lan_handler = logging.FileHandler(LAN_EVENTS_LOG, encoding='utf-8') lan_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')) lan_logger.addHandler(lan_handler) lan_logger.setLevel(logging.INFO) # Змінна для відстеження часу попереджень last_size_warning = {} # Налаштування логування в консоль і olt_monitor.log logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[logging.StreamHandler(), logging.FileHandler(LOG_FILE_PATH, encoding='utf-8')] ) # Завантаження конфігурації config = configparser.ConfigParser() try: config.read('/opt/olt_monitor/config.ini') except Exception as e: logging.error(f"Помилка читання config.ini: {e}") raise # Конфігурація try: TELEGRAM_BOT_TOKEN = config.get('Settings', 'TelegramBotToken') TELEGRAM_CHAT_IDS = config.get('Settings', 'TelegramChatIDs').split(',') REPORT_TIME = config.get('Settings', 'ReportTime', fallback='06:00') LOG_FILE = config.get('Settings', 'LogFile', fallback='/var/log/zte.log') LAST_POSITION_FILE = config.get('Settings', 'LastPositionFile', fallback='/opt/olt_monitor/last_position.txt') CHECK_INTERVAL = config.getint('Settings', 'CheckInterval', fallback=5) except Exception as e: logging.error(f"Помилка парсингу конфігурації: {e}") raise # Завантажуємо відправлені повідомлення def load_sent_messages(): if not os.path.exists(SENT_MESSAGES_FILE): return set() try: with open(SENT_MESSAGES_FILE, 'r', encoding='utf-8') as f: return set(line.strip() for line in f if line.strip()) except Exception as e: logging.error(f"Помилка читання файлу {SENT_MESSAGES_FILE}: {e}") return set() # Зберігаємо нове повідомлення def save_sent_message(msg, sent_messages): try: with open(SENT_MESSAGES_FILE, 'a', encoding='utf-8') as f: f.write(msg + '\n') sent_messages.add(msg) except Exception as e: logging.error(f"Помилка збереження повідомлення в {SENT_MESSAGES_FILE}: {e}") # Перевірка часу останнього повідомлення про активацію def can_send_activation(): if not os.path.exists(LAST_ACTIVATION_FILE): return True try: with open(LAST_ACTIVATION_FILE, 'r') as f: last_time = float(f.read().strip()) return (time.time() - last_time) > 300 # 5 хвилин except Exception as e: logging.error(f"Помилка читання {LAST_ACTIVATION_FILE}: {e}") return True # Зберігаємо час активації def save_activation_time(): try: with open(LAST_ACTIVATION_FILE, 'w') as f: f.write(str(time.time())) except Exception as e: logging.error(f"Помилка збереження {LAST_ACTIVATION_FILE}: {e}") # Відправка повідомлення в Telegram def send_telegram(msg, sent_messages): if msg in sent_messages: logging.warning(f"Повідомлення вже відправлено, пропущено: {msg}") return url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage" for chat_id in TELEGRAM_CHAT_IDS: try: response = requests.post(url, data={"chat_id": chat_id.strip(), "text": msg}, timeout=10) response.raise_for_status() logging.info(f"Повідомлення успішно відправлено в Telegram (chat_id: {chat_id}): {msg}") save_sent_message(msg, sent_messages) except requests.RequestException as e: error_msg = f"Помилка відправки в Telegram для chat_id {chat_id}: {e}, response: {response.text if 'response' in locals() else 'немає відповіді'}" logging.error(error_msg) critical_msg = f"⚠️ Критична помилка в olt_monitor.py:\n{error_msg}" if critical_msg not in sent_messages: try: for critical_chat_id in TELEGRAM_CHAT_IDS: requests.post(url, data={"chat_id": critical_chat_id.strip(), "text": critical_msg}, timeout=10) save_sent_message(critical_msg, sent_messages) except Exception as critical_e: logging.error(f"Не вдалося відправити критичну помилку в Telegram: {critical_e}") # Відправка звіту про події LAN у Telegram (з очищенням lan_events.log) def send_lan_summary_to_telegram(sent_messages): logging.info("Початок створення звіту про події LAN") summary = defaultdict(lambda: {'los': 0, 'restore': 0}) today = str(date.today()) try: if not os.path.exists(LAN_EVENTS_LOG): logging.info(f"Файл {LAN_EVENTS_LOG} не існує") else: with open(LAN_EVENTS_LOG, 'r', encoding='utf-8') as f: lines_processed = 0 current_entry = [] for line in f: line = line.strip() if not line: continue # Начало новой записи определяется по временной метке (YYYY-MM-DD HH:MM:SS, с опциональными миллисекундами) if re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:,\d+)?", line): if current_entry: # Обрабатываем предыдущую запись lines_processed += 1 entry_text = '\n'.join(current_entry) logging.debug(f"Обробка запису у {LAN_EVENTS_LOG}: {entry_text}") # Извлекаем порт, OLT и номер ONU port_match = re.search(r"Порт: (gpon-onu_\d+/\d+/\d+(?::\d+)?)", entry_text) olt_match = re.search(r"OLT: (\S+) \((\d+\.\d+\.\d+\.\d+)\)", entry_text) onu_match = re.search(r"ONU №(\d+)", entry_text) if port_match and olt_match and onu_match: port = port_match.group(1) olt_name = olt_match.group(1) olt_ip = olt_match.group(2) onu_number = onu_match.group(1) key = (port, olt_name, olt_ip, onu_number) if "LAN LOS знайдено" in entry_text: summary[key]['los'] += 1 logging.debug(f"Знайдено LAN LOS для {key} у {LAN_EVENTS_LOG}") elif "LAN LOS Restore знайдено" in entry_text: summary[key]['restore'] += 1 logging.debug(f"Знайдено LAN LOS Restore для {key} у {LAN_EVENTS_LOG}") else: logging.debug(f"Запис не відповідає формату port/olt/onu: {entry_text}") current_entry = [] current_entry.append(line) else: current_entry.append(line) # Обработка последней записи if current_entry: lines_processed += 1 entry_text = '\n'.join(current_entry) logging.debug(f"Обробка останнього запису у {LAN_EVENTS_LOG}: {entry_text}") port_match = re.search(r"Порт: (gpon-onu_\d+/\d+/\d+(?::\d+)?)", entry_text) olt_match = re.search(r"OLT: (\S+) \((\d+\.\d+\.\d+\.\d+)\)", entry_text) onu_match = re.search(r"ONU №(\d+)", entry_text) if port_match and olt_match and onu_match: port = port_match.group(1) olt_name = olt_match.group(1) olt_ip = olt_match.group(2) onu_number = onu_match.group(1) key = (port, olt_name, olt_ip, onu_number) if "LAN LOS знайдено" in entry_text: summary[key]['los'] += 1 logging.debug(f"Знайдено LAN LOS для {key} у {LAN_EVENTS_LOG}") elif "LAN LOS Restore знайдено" in entry_text: summary[key]['restore'] += 1 logging.debug(f"Знайдено LAN LOS Restore для {key} у {LAN_EVENTS_LOG}") else: logging.debug(f"Останній запис не відповідає формату: {entry_text}") logging.info(f"Оброблено {lines_processed} записів у {LAN_EVENTS_LOG}") # Формирование отчета if summary: msg = f"📊 Звіт подій LAN за {today}:\n\n" for (port, olt_name, olt_ip, onu_number), counts in sorted(summary.items()): total = counts['los'] + counts['restore'] msg += ( f"🔢 ONU №{onu_number}\n" f"📍 Порт: {port}\n" f"🖥 OLT: {olt_name} ({olt_ip})\n" f"🔴 Втрата LAN: {counts['los']}\n" f"🟢 LAN відновлено: {counts['restore']}\n" f"🔢 Загалом: {total}\n\n" ) else: msg = f"📊 Звіт подій LAN за {today}:\n\n⚠️ Подій LAN LOS або Restore не знайдено" send_telegram(msg.strip(), sent_messages) logging.info("Звіт про події LAN відправлено в Telegram") # Очищаем lan_events.log после отправки try: with open(LAN_EVENTS_LOG, 'w', encoding='utf-8') as f: f.write('') logging.info(f"Файл {LAN_EVENTS_LOG} очищено після відправки звіту") except Exception as e: logging.error(f"Помилка очищення {LAN_EVENTS_LOG}: {e}") except Exception as e: logging.error(f"Помилка створення звіту LAN: {e}") # Ручна перевірка звіту без очищення lan_events.log def manual_lan_summary(): sent_messages = load_sent_messages() logging.info("Початок ручної перевірки звіту про події LAN") summary = defaultdict(lambda: {'los': 0, 'restore': 0}) today = str(date.today()) try: if not os.path.exists(LAN_EVENTS_LOG): logging.info(f"Файл {LAN_EVENTS_LOG} не існує") else: with open(LAN_EVENTS_LOG, 'r', encoding='utf-8') as f: lines_processed = 0 current_entry = [] for line in f: line = line.strip() if not line: continue # Начало новой записи определяется по временной метке if re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:,\d+)?", line): if current_entry: # Обрабатываем предыдущую запись lines_processed += 1 entry_text = '\n'.join(current_entry) logging.debug(f"Обробка запису у {LAN_EVENTS_LOG}: {entry_text}") port_match = re.search(r"Порт: (gpon-onu_\d+/\d+/\d+(?::\d+)?)", entry_text) olt_match = re.search(r"OLT: (\S+) \((\d+\.\d+\.\d+\.\d+)\)", entry_text) onu_match = re.search(r"ONU №(\d+)", entry_text) if port_match and olt_match and onu_match: port = port_match.group(1) olt_name = olt_match.group(1) olt_ip = olt_match.group(2) onu_number = onu_match.group(1) key = (port, olt_name, olt_ip, onu_number) if "LAN LOS знайдено" in entry_text: summary[key]['los'] += 1 logging.debug(f"Знайдено LAN LOS для {key} у {LAN_EVENTS_LOG}") elif "LAN LOS Restore знайдено" in entry_text: summary[key]['restore'] += 1 logging.debug(f"Знайдено LAN LOS Restore для {key} у {LAN_EVENTS_LOG}") else: logging.debug(f"Запис не відповідає формату port/olt/onu: {entry_text}") current_entry = [] current_entry.append(line) else: current_entry.append(line) # Обработка последней записи if current_entry: lines_processed += 1 entry_text = '\n'.join(current_entry) logging.debug(f"Обробка останнього запису у {LAN_EVENTS_LOG}: {entry_text}") port_match = re.search(r"Порт: (gpon-onu_\d+/\d+/\d+(?::\d+)?)", entry_text) olt_match = re.search(r"OLT: (\S+) \((\d+\.\d+\.\d+\.\d+)\)", entry_text) onu_match = re.search(r"ONU №(\d+)", entry_text) if port_match and olt_match and onu_match: port = port_match.group(1) olt_name = olt_match.group(1) olt_ip = olt_match.group(2) onu_number = onu_match.group(1) key = (port, olt_name, olt_ip, onu_number) if "LAN LOS знайдено" in entry_text: summary[key]['los'] += 1 logging.debug(f"Знайдено LAN LOS для {key} у {LAN_EVENTS_LOG}") elif "LAN LOS Restore знайдено" in entry_text: summary[key]['restore'] += 1 logging.debug(f"Знайдено LAN LOS Restore для {key} у {LAN_EVENTS_LOG}") else: logging.debug(f"Останній запис не відповідає формату: {entry_text}") logging.info(f"Оброблено {lines_processed} записів у {LAN_EVENTS_LOG}") # Формирование отчета if summary: msg = f"📊 Ручний звіт подій LAN за {today}:\n\n" for (port, olt_name, olt_ip, onu_number), counts in sorted(summary.items()): total = counts['los'] + counts['restore'] msg += ( f"🔢 ONU №{onu_number}\n" f"📍 Порт: {port}\n" f"🖥 OLT: {olt_name} ({olt_ip})\n" f"🔴 Втрата LAN: {counts['los']}\n" f"🟢 LAN відновлено: {counts['restore']}\n" f"🔢 Загалом: {total}\n\n" ) else: msg = f"📊 Ручний звіт подій LAN за {today}:\n\n⚠️ Подій LAN LOS або Restore не знайдено" send_telegram(msg.strip(), sent_messages) logging.info("Ручний звіт про події LAN відправлено в Telegram") except Exception as e: logging.error(f"Помилка створення ручного звіту LAN: {e}") # Налаштування логування та перезапису файлів def setup_logging_and_messages(): try: logging.info("Початок налаштування логування") last_reset_date = None if os.path.exists(LAST_RESET_FILE): with open(LAST_RESET_FILE, 'r') as f: last_reset_date = f.read().strip() logging.info(f"Прочитано дату останнього перезапису: {last_reset_date}") else: logging.info(f"Файл {LAST_RESET_FILE} не існує, буде створено") today = str(date.today()) logging.info(f"Поточна дата: {today}") # Перевірка розміру логу log_size_mb = os.path.getsize(LOG_FILE_PATH) / (1024 * 1024) if os.path.exists(LOG_FILE_PATH) else 0 logging.info("Розмір логу: " + str(round(log_size_mb, 2)) + " МБ") if last_reset_date != today: sent_messages = load_sent_messages() try: logging.basicConfig( filename=LOG_FILE_PATH, level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', filemode='w', force=True ) logging.info(f"Лог перезаписано для нової доби (size={log_size_mb:.2f} МБ)") with open(SENT_MESSAGES_FILE, 'w', encoding='utf-8') as f: f.write('') logging.info("Файл sent_messages.txt перезаписано для нової доби") with open(LAST_RESET_FILE, 'w') as f: f.write(today) logging.info(f"Оновлено {LAST_RESET_FILE} з датою {today}") except Exception as e: logging.error(f"Помилка при перезаписі файлів: {e}") raise else: logging.basicConfig( filename=LOG_FILE_PATH, level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', filemode='a', force=True ) logging.info("Дата не змінилася, використовується режим додавання") except Exception as e: logging.error(f"Критична помилка налаштування: {e}") print(f"Критична помилка налаштування: {e}") raise # Перевірка розміру файлів def check_file_size(file_path, file_name, max_size_mb=10): try: if os.path.exists(file_path): size_mb = os.path.getsize(file_path) / (1024 * 1024) if size_mb > max_size_mb: current_time = time.time() last_warning_time = last_size_warning.get(file_name, 0) if current_time - last_warning_time > 600: logging.warning(f"Розмір {file_name} перевищує {max_size_mb} МБ: {size_mb:.2f} МБ") last_size_warning[file_name] = current_time except Exception as e: logging.error(f"Помилка перевірки розміру {file_name}: {e}") # Глобальні змінні current_interface = None current_olt_name = None current_olt_ip = None current_description = None onu_add_buffer = defaultdict(dict) # Завантажуємо останню позицію def load_last_position(): if not os.path.exists(LAST_POSITION_FILE): return 0, os.stat(LOG_FILE).st_ino if os.path.exists(LOG_FILE) else 0 try: with open(LAST_POSITION_FILE, 'r') as f: pos = f.read().strip() if pos == '': return 0, os.stat(LOG_FILE).st_ino return int(pos), os.stat(LOG_FILE).st_ino except Exception as e: logging.error(f"Помилка читання позиції з {LAST_POSITION_FILE}: {e}") return 0, os.stat(LOG_FILE).st_ino if os.path.exists(LOG_FILE) else 0 # Зберігаємо останню позицію def save_last_position(position): try: with open(LAST_POSITION_FILE, 'w') as f: f.write(str(position)) except Exception as e: logging.error(f"Помилка збереження позиції в {LAST_POSITION_FILE}: {e}") # Парсинг логу def parse_log(file_obj, start_pos, sent_messages): global current_interface, current_olt_name, current_olt_ip, current_description try: file_obj.seek(start_pos) file_size = os.path.getsize(LOG_FILE) if start_pos > file_size: logging.warning(f"start_pos ({start_pos}) більше file_size ({file_size}), скидаємо на 0") start_pos = 0 file_obj.seek(0) lines = file_obj.readlines() end_pos = file_obj.tell() except Exception as e: logging.error(f"Помилка читання файлу логу: {e}") return start_pos for line in lines: line = line.strip().replace('#012', '').replace('#015', '') if not line: continue # Витягуємо ім'я та IP OLT m_olt = re.search(r"\[(\S+) (\d+\.\d+\.\d+\.\d+)\]", line) if m_olt: current_olt_name = m_olt.group(1) current_olt_ip = m_olt.group(2) # Витягуємо час time_match = re.match(r"^\w+\s+\d+\s+(\d{2}:\d{2}:\d{2})", line) time_str = time_match.group(1) if time_match else datetime.now().strftime("%H:%M:%S") try: event_time = datetime.strptime(time_str, "%H:%M:%S") event_time = event_time.replace(year=datetime.now().year, month=datetime.now().month, day=datetime.now().day) except ValueError as e: logging.error(f"Помилка формату часу {time_str}: {e}") continue # Перевіряємо OLT перед обробкою подій if not current_olt_name or not current_olt_ip: logging.warning(f"Пропущено подію через відсутність OLT: {line}") continue # Витягуємо інтерфейс m_intf = re.search(r"(?:interface\s+)(gpon-olt_\d+/\d+/\d+|gpon-onu_\d+/\d+/\d+:\d+)", line, re.IGNORECASE) if m_intf: current_interface = m_intf.group(1) continue # Витягуємо description m_desc = re.search(r"description\s+(\S+)", line, re.IGNORECASE) if m_desc: current_description = m_desc.group(1) for key, data in list(onu_add_buffer.items()): if key[2] == current_interface or key[2].replace("gpon-olt_", "gpon-onu_") + f":{key[3]}" == current_interface: data['description'] = current_description desc_str = f"\n🏷 Опис: {data['description']}" if data['description'] else "" msg = ( f"✅ ONU додано\n\n" f"🕒 Час: {data['time_str']}\n" f"🔢 ONU №{data['onu_number']}\n" f"📍 Порт: {data['onu_iface']}{desc_str}\n" f"🖥 OLT: {key[0]} ({key[1]})" ) send_telegram(msg, sent_messages) del onu_add_buffer[key] continue # LAN LOS Alarm m_lan_los = re.search(r"(?:GponRm notify:.*)?\s*SubType:\d+\s*Pos:\d+\s*ONU Uni lan los\. alarm", line, re.IGNORECASE) if m_lan_los: m_iface_num = re.search(r"<(gpon-onu_\d+/\d+/\d+):(\d+)>", line) if m_iface_num: onu_iface = m_iface_num.group(1) onu_num = m_iface_num.group(2) desc_str = f"\n🏷 Опис: {current_description}" if current_description and current_interface == onu_iface else "" msg = ( f"🔴 Втрата LAN-з'єднання\n\n" f"🕒 Час: {time_str}\n" f"🔢 ONU №{onu_num}\n" f"📍 Порт: {onu_iface}{desc_str}\n" f"🖥 OLT: {current_olt_name} ({current_olt_ip})" ) lan_logger.warning(f"LAN LOS знайдено: {msg}") # Записываем только в lan_events.log current_description = None continue # LAN LOS Restore m_lan_restore = re.search(r"(?:GponRm notify:.*)?\s*SubType:\d+\s*Pos:\d+\s*ONU Uni lan los\. restore", line, re.IGNORECASE) if m_lan_restore: m_iface_num = re.search(r"<(gpon-onu_\d+/\d+/\d+):(\d+)>", line) if m_iface_num: onu_iface = m_iface_num.group(1) onu_num = m_iface_num.group(2) desc_str = f"\n🏷 Опис: {current_description}" if current_description and current_interface == onu_iface else "" msg = ( f"🟢 LAN-з'єднання відновлено\n\n" f"🕒 Час: {time_str}\n" f"🔢 ONU №{onu_num}\n" f"📍 Порт: {onu_iface}{desc_str}\n" f"🖥 OLT: {current_olt_name} ({current_olt_ip})" ) lan_logger.warning(f"LAN LOS Restore знайдено: {msg}") # Записываем только в lan_events.log current_description = None continue # Видалення ONU m_no_onu = re.search(r"\b(?:no\s+onu|ont delete)\s+(\d+)(?:\s+\d+)?|ont delete\s+(\d+/\d+/\d+)\s+(\d+)", line, re.IGNORECASE) if m_no_onu: onu_iface = m_no_onu.group(2) or current_interface or "невідомий" onu_number = m_no_onu.group(1) or m_no_onu.group(3) desc_str = f"\n🏷 Опис: {current_description}" if current_description and current_interface == onu_iface else "" msg = ( f"❌ ONU видалено\n\n" f"🕒 Час: {time_str}\n" f"🔢 ONU №{onu_number}\n" f"📍 Порт: {onu_iface}{desc_str}\n" f"🖥 OLT: {current_olt_name} ({current_olt_ip})" ) send_telegram(msg, sent_messages) current_description = None key = (current_olt_name, current_olt_ip, onu_iface, onu_number) if key in onu_add_buffer: del onu_add_buffer[key] continue # Додавання ONU m_add_onu = re.search(r"\b(?:onu\s+add|ont add)\s+(\d+)\s+(\d+)|ont add\s+(\d+/\d+/\d+)\s+(\d+)|onu\s+(\d+)\s+type\s+\S+\s+sn\s+\S+", line, re.IGNORECASE) if m_add_onu: onu_iface = m_add_onu.group(1) or m_add_onu.group(3) or current_interface or "невідомий" onu_number = m_add_onu.group(2) or m_add_onu.group(4) or m_add_onu.group(5) key = (current_olt_name, current_olt_ip, onu_iface, onu_number) onu_add_buffer[key] = { 'time_str': time_str, 'onu_iface': onu_iface, 'onu_number': onu_number, 'description': current_description if current_interface == onu_iface else None, 'timestamp': datetime.now() } current_description = None continue # Обробка відкладених подій ONU current_time = datetime.now() for key, data in list(onu_add_buffer.items()): if (current_time - data['timestamp']).seconds >= 5: desc_str = f"\n🏷 Опис: {data['description']}" if data['description'] else "" msg = ( f"✅ ONU додано\n\n" f"🕒 Час: {data['time_str']}\n" f"🔢 ONU №{data['onu_number']}\n" f"📍 Порт: {data['onu_iface']}{desc_str}\n" f"🖥 OLT: {key[0]} ({key[1]})" ) send_telegram(msg, sent_messages) del onu_add_buffer[key] return end_pos # Моніторинг логу def monitor_log(): sent_messages = load_sent_messages() if can_send_activation(): test_msg = f"🔔 Моніторинг OLT активовано\n\n🕒 Час: {datetime.now().strftime('%H:%M:%S')}" send_telegram(test_msg, sent_messages) save_activation_time() last_pos, last_inode = load_last_position() while True: try: check_file_size(LOG_FILE_PATH, "olt_monitor.log", max_size_mb=10) check_file_size(SENT_MESSAGES_FILE, "sent_messages.txt", max_size_mb=10) check_file_size(LAN_EVENTS_LOG, "lan_events.log", max_size_mb=10) if not os.path.exists(LOG_FILE): logging.error(f"Файл логу {LOG_FILE} не існує. Очікування 60 секунд") time.sleep(60) continue current_inode = os.stat(LOG_FILE).st_ino if current_inode != last_inode: logging.info(f"Виявлено новий inode для {LOG_FILE}, скидання позиції") last_pos, last_inode = 0, current_inode with open(LOG_FILE, "r", encoding="utf-8") as f: new_pos = parse_log(f, last_pos, sent_messages) if new_pos != last_pos: save_last_position(new_pos) last_pos = new_pos time.sleep(CHECK_INTERVAL) except Exception as e: logging.error(f"Критична помилка при обробці логу: {e}") time.sleep(60) # Планувальник для щоденного звіту def run_scheduler(): sent_messages = load_sent_messages() schedule.every().day.at(REPORT_TIME).do(send_lan_summary_to_telegram, sent_messages) while True: schedule.run_pending() time.sleep(60) if __name__ == "__main__": try: setup_logging_and_messages() # Перевіряємо аргумент командного рядка if len(sys.argv) > 1 and sys.argv[1] == "--manual-report": manual_lan_summary() else: # Запускаємо планувальник у окремому потоці scheduler_thread = threading.Thread(target=run_scheduler, daemon=True) scheduler_thread.start() monitor_log() except Exception as e: logging.error(f"Помилка запуску програми: {e}") raise