#!/usr/bin/env python3 # -*- coding: utf-8 -*- import argparse import datetime import os import re from pathlib import Path from collections import defaultdict, deque from typing import Dict, List, Optional, Tuple import requests BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DEFAULT_PATTERNS = ["*.log", "*_log.txt"] TOKEN_FILE_HOME = os.path.expanduser("~/.telegram_dpc_bot_token") TOKEN_FILE_ETC = "/etc/telegram_dpc_bot_token" TS_RE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})") CATEGORIES = { "open_meteo_timeout": re.compile( r"timeout|timed out|Read timed out|Gateway Time-out|504", re.IGNORECASE ), "ssl_handshake": re.compile(r"handshake", re.IGNORECASE), "permission_error": re.compile(r"PermissionError|permesso negato|Errno 13", re.IGNORECASE), "telegram_error": re.compile(r"Telegram error|Bad Request|chat not found|can't parse entities", re.IGNORECASE), "traceback": re.compile(r"Traceback", re.IGNORECASE), "exception": re.compile(r"\bERROR\b|Exception", re.IGNORECASE), "token_missing": re.compile(r"token missing|Token Telegram assente", re.IGNORECASE), } def load_text_file(path: str) -> str: try: with open(path, "r", encoding="utf-8") as f: return f.read().strip() except Exception: return "" def load_bot_token() -> str: tok = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip() if tok: return tok tok = load_text_file(TOKEN_FILE_HOME) if tok: return tok tok = load_text_file(TOKEN_FILE_ETC) return tok.strip() if tok else "" def send_telegram(text: str, chat_ids: Optional[List[str]]) -> bool: token = load_bot_token() if not token or not chat_ids: return False url = f"https://api.telegram.org/bot{token}/sendMessage" base_payload = { "text": text, "disable_web_page_preview": True, } ok = False with requests.Session() as s: for chat_id in chat_ids: payload = dict(base_payload) payload["chat_id"] = chat_id try: resp = s.post(url, json=payload, timeout=15) if resp.status_code == 200: ok = True except Exception: continue return ok def tail_lines(path: str, max_lines: int) -> List[str]: dq: deque[str] = deque(maxlen=max_lines) with open(path, "r", encoding="utf-8", errors="ignore") as f: for line in f: dq.append(line.rstrip("\n")) return list(dq) def parse_ts(line: str) -> Optional[datetime.datetime]: m = TS_RE.search(line) if not m: return None try: return datetime.datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") except Exception: return None def analyze_logs(files: List[str], since: datetime.datetime, max_lines: int) -> Tuple[Dict, Dict, Dict, Dict]: category_hits = defaultdict(list) per_file_counts = defaultdict(lambda: defaultdict(int)) timeout_minutes = defaultdict(int) stale_logs = {} # path -> (last_ts, hours_since_update) now = datetime.datetime.now() for path in files: if not os.path.isfile(path): continue last_ts = None for line in tail_lines(path, max_lines): ts = parse_ts(line) if ts: last_ts = ts if not last_ts or last_ts < since: continue for cat, regex in CATEGORIES.items(): if regex.search(line): category_hits[cat].append((last_ts, path, line)) per_file_counts[path][cat] += 1 if cat == "open_meteo_timeout": timeout_minutes[last_ts.strftime("%H:%M")] += 1 break # Verifica se il log è "stale" (non aggiornato da più di 24 ore) if last_ts: hours_since = (now - last_ts).total_seconds() / 3600.0 if hours_since > 24: stale_logs[path] = (last_ts, hours_since) else: # Se non ha timestamp, verifica data di modifica del file try: mtime = os.path.getmtime(path) file_mtime = datetime.datetime.fromtimestamp(mtime) hours_since = (now - file_mtime).total_seconds() / 3600.0 if hours_since > 24: stale_logs[path] = (file_mtime, hours_since) except Exception: pass return category_hits, per_file_counts, timeout_minutes, stale_logs def format_report( days: int, files: List[str], category_hits: Dict, per_file_counts: Dict, timeout_minutes: Dict, stale_logs: Dict, ) -> str: now = datetime.datetime.now() since = now - datetime.timedelta(days=days) lines = [] lines.append(f"🧾 Log Monitor - ultimi {days} giorni") lines.append(f"Intervallo: {since.strftime('%Y-%m-%d %H:%M')} → {now.strftime('%Y-%m-%d %H:%M')}") lines.append(f"File analizzati: {len(files)}") lines.append("") # Sezione log non aggiornati if stale_logs: lines.append("⚠️ Log non aggiornati (>24h):") for path, (last_ts, hours_since) in sorted(stale_logs.items(), key=lambda x: x[1][1], reverse=True): short_path = os.path.basename(path) days_ago = hours_since / 24.0 if days_ago >= 1: lines.append(f" • {short_path}: {days_ago:.1f} giorni fa ({last_ts.strftime('%Y-%m-%d %H:%M')})") else: lines.append(f" • {short_path}: {hours_since:.1f} ore fa ({last_ts.strftime('%Y-%m-%d %H:%M')})") lines.append("") total_issues = sum(len(v) for v in category_hits.values()) if total_issues == 0 and not stale_logs: lines.append("✅ Nessun problema rilevato nelle ultime 72 ore.") return "\n".join(lines) lines.append(f"Problemi rilevati: {total_issues}") lines.append("") for cat, items in sorted(category_hits.items(), key=lambda x: len(x[1]), reverse=True): lines.append(f"- {cat}: {len(items)}") # show up to 3 latest samples for ts, path, msg in sorted(items, key=lambda x: x[0], reverse=True)[:3]: short_path = os.path.basename(path) lines.append(f" • {ts.strftime('%Y-%m-%d %H:%M:%S')} | {short_path} | {msg[:180]}") lines.append("") if timeout_minutes: lines.append("Timeout: minuti più frequenti") for minute, count in sorted(timeout_minutes.items(), key=lambda x: x[1], reverse=True)[:6]: lines.append(f" • {minute} -> {count}") lines.append("") # Per-file summary (top 6 files) lines.append("File più problematici") file_totals = [] for path, cats in per_file_counts.items(): file_totals.append((sum(cats.values()), path)) for total, path in sorted(file_totals, reverse=True)[:6]: short_path = os.path.basename(path) lines.append(f" • {short_path}: {total}") return "\\n".join(lines) def main(): parser = argparse.ArgumentParser() parser.add_argument("--days", type=int, default=3, help="Numero di giorni da analizzare") parser.add_argument("--max-lines", type=int, default=5000, help="Limite righe per file") parser.add_argument("--chat_id", help="Chat ID Telegram (separati da virgola)") parser.add_argument("--log", action="append", help="Aggiungi un file log specifico") args = parser.parse_args() if args.log: files = [p for p in args.log if os.path.exists(p)] else: from pathlib import Path files = [] for pat in DEFAULT_PATTERNS: files.extend(sorted([str(p) for p in Path(BASE_DIR).glob(pat)])) files = sorted(set(files)) since = datetime.datetime.now() - datetime.timedelta(days=args.days) category_hits, per_file_counts, timeout_minutes, stale_logs = analyze_logs(files, since, args.max_lines) report = format_report(args.days, files, category_hits, per_file_counts, timeout_minutes, stale_logs) if args.chat_id: chat_ids = [c.strip() for c in args.chat_id.split(",") if c.strip()] send_telegram(report, chat_ids) else: print(report) if __name__ == "__main__": main()