From 9dbe0cfa9338df9289e8bc4d250a3b3f51686bd2 Mon Sep 17 00:00:00 2001 From: daniele Date: Sun, 18 Jan 2026 07:00:02 +0100 Subject: [PATCH] Backup automatico script del 2026-01-18 07:00 --- services/telegram-bot/bot.py | 20 +- services/telegram-bot/civil_protection.py | 8 +- services/telegram-bot/freeze_alert.py | 58 ++--- services/telegram-bot/log_monitor.py | 231 ++++++++++++++++++ services/telegram-bot/meteo.py | 32 ++- services/telegram-bot/nowcast_120m_alert.py | 12 +- .../telegram-bot/smart_irrigation_advisor.py | 33 ++- services/telegram-bot/student_alert.py | 2 +- 8 files changed, 339 insertions(+), 57 deletions(-) create mode 100644 services/telegram-bot/log_monitor.py diff --git a/services/telegram-bot/bot.py b/services/telegram-bot/bot.py index 7d0ef18..a2a0acd 100644 --- a/services/telegram-bot/bot.py +++ b/services/telegram-bot/bot.py @@ -199,8 +199,12 @@ def call_meteo_script(args_list): try: # Esegui: python3 meteo.py --arg1 val1 ... cmd = ["python3", METEO_SCRIPT] + args_list - result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + # Timeout aumentato a 90s per gestire retry e chiamate API multiple + # (get_forecast può fare retry + fallback, get_visibility_forecast può fare 2 chiamate) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=90) return result.stdout if result.returncode == 0 else f"Errore Script: {result.stderr}" + except subprocess.TimeoutExpired: + return f"Errore esecuzione script: Timeout dopo 90 secondi (script troppo lento)" except Exception as e: return f"Errore esecuzione script: {e}" @@ -270,7 +274,7 @@ async def meteo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N f"✈️ **Report Meteo - {viaggio_attivo['name']}**\n\n{report_viaggio}", parse_mode="Markdown" ) - else: + else: # Nessun viaggio attivo: invia report per Casa await update.message.reply_text("🔄 Generazione report meteo per Casa...", parse_mode="Markdown") report_casa = call_meteo_script(["--home"]) @@ -661,7 +665,7 @@ async def meteo_viaggio_command(update: Update, context: ContextTypes.DEFAULT_TY f"📊 **Report Meteo - {name}**\n\n{report_meteo}", parse_mode="Markdown" ) - else: + else: await update.message.reply_text( f"⚠️ Errore nella generazione del report meteo:\n{report_meteo}", parse_mode="Markdown" @@ -729,11 +733,13 @@ async def meteo_viaggio_command(update: Update, context: ContextTypes.DEFAULT_TY await update.message.reply_text(f"❌ Errore durante l'elaborazione: {str(e)}", parse_mode="Markdown") async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None: - # LANCIAMO LO SCRIPT ESTERNO PER CASA + # Esegui una sola chiamata e invia il report a tutti i chat_id report = call_meteo_script(["--home"]) for uid in ALLOWED_IDS: - try: await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown") - except: pass + try: + await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown") + except Exception: + pass @restricted async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -838,7 +844,7 @@ def main(): application.add_handler(CallbackQueryHandler(button_handler)) job_queue = application.job_queue - job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=8, minute=0, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6)) + job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=7, minute=15, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6)) application.run_polling() diff --git a/services/telegram-bot/civil_protection.py b/services/telegram-bot/civil_protection.py index 187e5dc..272866d 100644 --- a/services/telegram-bot/civil_protection.py +++ b/services/telegram-bot/civil_protection.py @@ -138,6 +138,10 @@ def telegram_send_html(message_html: str, chat_ids: Optional[List[str]] = None) message_html: Messaggio HTML da inviare chat_ids: Lista di chat IDs (default: TELEGRAM_CHAT_IDS) """ + # Sanitize unsupported HTML tags (Telegram non supporta
) + if message_html: + message_html = re.sub(r"<\s*br\s*/?\s*>", "\n", message_html, flags=re.IGNORECASE) + token = load_bot_token() if not token: LOGGER.warning("Token Telegram assente. Nessun invio effettuato.") @@ -363,8 +367,8 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False): else: LOGGER.warning("Invio debug non riuscito (token mancante o errore Telegram).") else: - LOGGER.info("Nessuna allerta nelle zone monitorate. Nessuna notifica inviata.") - return + LOGGER.info("Nessuna allerta nelle zone monitorate. Nessuna notifica inviata.") + return sig = build_signature(parsed) state = load_state() diff --git a/services/telegram-bot/freeze_alert.py b/services/telegram-bot/freeze_alert.py index cabd820..1ee32c6 100644 --- a/services/telegram-bot/freeze_alert.py +++ b/services/telegram-bot/freeze_alert.py @@ -476,41 +476,41 @@ def analyze_freeze(chat_ids: Optional[List[str]] = None, debug_mode: bool = Fals LOGGER.info(" Nuova fascia %d: %s - %s", i+1, start.strftime("%d/%m %H:%M"), end.strftime("%d/%m %H:%M")) if should_notify: - # Costruisci messaggio con dettagli sulle nuove fasce orarie - period_details = [] - if has_new_periods: - for start, end in new_periods[:3]: # Max 3 fasce nel messaggio - if start.date() == end.date(): - period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%H:%M')}") - else: - period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%d/%m %H:%M')}") - - msg_parts = [ - "❄️ ALLERTA GELO\n", - f"📍 {html.escape(LOCATION_NAME)}\n\n", - f"Minima prevista (entro {HOURS_AHEAD}h): {min_temp_val:.1f}°C\n", - f"📅 Quando: {html.escape(fmt_dt(min_temp_time))}", - ] - if period_details: - msg_parts.append("\n🕒 Fasce orarie: " + ", ".join(period_details)) - msg_parts.append("\n\nProteggere piante e tubature esterne.") - - msg = "".join(msg_parts) + # Costruisci messaggio con dettagli sulle nuove fasce orarie + period_details = [] + if has_new_periods: + for start, end in new_periods[:3]: # Max 3 fasce nel messaggio + if start.date() == end.date(): + period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%H:%M')}") + else: + period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%d/%m %H:%M')}") + + msg_parts = [ + "❄️ ALLERTA GELO\n", + f"📍 {html.escape(LOCATION_NAME)}\n\n", + f"Minima prevista (entro {HOURS_AHEAD}h): {min_temp_val:.1f}°C\n", + f"📅 Quando: {html.escape(fmt_dt(min_temp_time))}", + ] + if period_details: + msg_parts.append("\n🕒 Fasce orarie: " + ", ".join(period_details)) + msg_parts.append("\n\nProteggere piante e tubature esterne.") + + msg = "".join(msg_parts) ok = telegram_send_html(msg, chat_ids=chat_ids) if ok: - LOGGER.info("Allerta gelo inviata. Tmin=%.1f°C at %s, nuove fasce: %d", - min_temp_val, min_temp_time.isoformat(), len(new_periods)) - # Aggiorna le fasce notificate - for start, end in new_periods: - notified_periods.append({ - "start": start.isoformat(), - "end": end.isoformat(), - }) + LOGGER.info("Allerta gelo inviata. Tmin=%.1f°C at %s, nuove fasce: %d", + min_temp_val, min_temp_time.isoformat(), len(new_periods)) + # Aggiorna le fasce notificate + for start, end in new_periods: + notified_periods.append({ + "start": start.isoformat(), + "end": end.isoformat(), + }) else: LOGGER.warning("Allerta gelo NON inviata (token mancante o errore Telegram).") else: - LOGGER.info("Gelo già notificato (nessuna nuova fascia oraria, peggioramento < 2°C). Tmin=%.1f°C", min_temp_val) + LOGGER.info("Gelo già notificato (nessuna nuova fascia oraria, peggioramento < 2°C). Tmin=%.1f°C", min_temp_val) state.update({ "alert_active": True, diff --git a/services/telegram-bot/log_monitor.py b/services/telegram-bot/log_monitor.py new file mode 100644 index 0000000..74cf910 --- /dev/null +++ b/services/telegram-bot/log_monitor.py @@ -0,0 +1,231 @@ +#!/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() diff --git a/services/telegram-bot/meteo.py b/services/telegram-bot/meteo.py index 97b4daf..36e7e1e 100644 --- a/services/telegram-bot/meteo.py +++ b/services/telegram-bot/meteo.py @@ -221,7 +221,9 @@ def get_forecast(lat, lon, model=None, is_home=False, timezone=None, retry_after # Nota: minutely_15 non è usato in meteo.py (solo per script di allerta) try: - r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25) + t0 = time.time() + # Timeout ridotto a 20s per fallire più velocemente in caso di problemi + r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=20) if r.status_code != 200: # Dettagli errore più specifici error_details = f"Status {r.status_code}" @@ -238,22 +240,23 @@ def get_forecast(lat, lon, model=None, is_home=False, timezone=None, retry_after logger.error(f"API Error {error_details}") return None, error_details # Restituisce anche i dettagli dell'errore response_data = r.json() + logger.info("get_forecast ok model=%s points=5 elapsed=%.2fs", model or "best_match", time.time() - t0) # Open-Meteo per multiple locations (lat/lon separati da virgola) restituisce # direttamente un dict con "hourly", "daily", etc. che contiene liste di valori # per ogni location. Per semplicità, restituiamo il dict così com'è # e lo gestiamo nel codice chiamante return response_data, None except requests.exceptions.Timeout as e: - error_details = f"Timeout dopo 25s: {str(e)}" - logger.error(f"Request timeout: {error_details}") + error_details = f"Timeout dopo 20s: {str(e)}" + logger.error("Request timeout: %s elapsed=%.2fs", error_details, time.time() - t0) return None, error_details except requests.exceptions.ConnectionError as e: error_details = f"Errore connessione: {str(e)}" - logger.error(f"Connection error: {error_details}") + logger.error("Connection error: %s elapsed=%.2fs", error_details, time.time() - t0) return None, error_details except Exception as e: error_details = f"Errore richiesta: {type(e).__name__}: {str(e)}" - logger.error(f"Request error: {error_details}") + logger.error("Request error: %s elapsed=%.2fs", error_details, time.time() - t0) return None, error_details def get_visibility_forecast(lat, lon): @@ -271,16 +274,19 @@ def get_visibility_forecast(lat, lon): "hourly": "visibility" } try: - r = requests.get(OPEN_METEO_URL, params=params_ecmwf, headers=HTTP_HEADERS, timeout=15) + t0 = time.time() + # Timeout ridotto a 12s per fallire più velocemente + r = requests.get(OPEN_METEO_URL, params=params_ecmwf, headers=HTTP_HEADERS, timeout=12) if r.status_code == 200: data = r.json() hourly = data.get("hourly", {}) vis = hourly.get("visibility", []) # Verifica se ci sono valori validi (non tutti None) if vis and any(v is not None for v in vis): + logger.info("get_visibility_forecast ok model=ecmwf_ifs04 elapsed=%.2fs", time.time() - t0) return vis except Exception as e: - logger.debug(f"ECMWF IFS visibility request error: {e}") + logger.debug("ECMWF IFS visibility request error: %s elapsed=%.2fs", e, time.time() - t0) # Fallback: usa best match (senza models) che seleziona automaticamente GFS o ICON-D2 params_best = { @@ -291,16 +297,20 @@ def get_visibility_forecast(lat, lon): "hourly": "visibility" } try: - r = requests.get(OPEN_METEO_URL, params=params_best, headers=HTTP_HEADERS, timeout=15) + t0 = time.time() + # Timeout ridotto a 12s per fallire più velocemente + r = requests.get(OPEN_METEO_URL, params=params_best, headers=HTTP_HEADERS, timeout=12) if r.status_code == 200: data = r.json() hourly = data.get("hourly", {}) + logger.info("get_visibility_forecast ok model=best_match elapsed=%.2fs", time.time() - t0) return hourly.get("visibility", []) except Exception as e: - logger.error(f"Visibility request error: {e}") + logger.error("Visibility request error: %s elapsed=%.2fs", e, time.time() - t0) return None def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT", timezone=None) -> str: + t_total = time.time() # Determina se è Casa is_home = (abs(lat - HOME_LAT) < 0.01 and abs(lon - HOME_LON) < 0.01) @@ -596,7 +606,9 @@ def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT", if not blocks: return f"❌ Nessun dato da mostrare nelle prossime 48 ore (da {current_hour.strftime('%H:%M')})." - return f"🌤️ *METEO REPORT*\n📍 {location_name}\n🧠 Fonte: {model_name}\n\n" + "\n\n".join(blocks) + report = f"🌤️ *METEO REPORT*\n📍 {location_name}\n🧠 Fonte: {model_name}\n\n" + "\n\n".join(blocks) + logger.info("generate_weather_report ok elapsed=%.2fs", time.time() - t_total) + return report if __name__ == "__main__": args_parser = argparse.ArgumentParser() diff --git a/services/telegram-bot/nowcast_120m_alert.py b/services/telegram-bot/nowcast_120m_alert.py index 29a00f4..be31a09 100644 --- a/services/telegram-bot/nowcast_120m_alert.py +++ b/services/telegram-bot/nowcast_120m_alert.py @@ -698,6 +698,10 @@ def find_confirmed_start( def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None: LOGGER.info("--- Nowcast 120m alert ---") + # Carica state e inizializza active_events + state = load_state() + active_events = state.get("active_events", {}) + # Estendi forecast a 3 giorni per avere 48h di analisi neve completa data_arome = get_forecast(MODEL_AROME, forecast_days=3) if not data_arome: @@ -825,10 +829,10 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None # Calcola picco ICON se disponibile max_g_icon = 0.0 if len(gust_icon) >= n: - for i in range(n): - dt = parse_time_local(times[i]) + for i in range(n): + dt = parse_time_local(times[i]) if dt < window_start or dt > window_end: - continue + continue max_g_icon = max(max_g_icon, val(gust_icon, i)) # Comparazione @@ -922,7 +926,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None alerts.append("ℹ️ Nessuna allerta confermata entro %s minuti." % WINDOW_MINUTES) sig_parts.append("NO_ALERT") else: - LOGGER.info("Nessuna allerta confermata entro %s minuti.", WINDOW_MINUTES) + LOGGER.info("Nessuna allerta confermata entro %s minuti.", WINDOW_MINUTES) # Salva state aggiornato (con eventi puliti) anche se non inviamo notifiche state["active_events"] = active_events save_state(state) diff --git a/services/telegram-bot/smart_irrigation_advisor.py b/services/telegram-bot/smart_irrigation_advisor.py index e2d43d5..b88ef11 100755 --- a/services/telegram-bot/smart_irrigation_advisor.py +++ b/services/telegram-bot/smart_irrigation_advisor.py @@ -114,11 +114,36 @@ def setup_logger() -> logging.Logger: logger.setLevel(logging.DEBUG if DEBUG else logging.INFO) logger.handlers.clear() - fh = RotatingFileHandler(LOG_FILE, maxBytes=500_000, backupCount=3, encoding="utf-8") - fh.setLevel(logging.DEBUG) fmt = logging.Formatter("%(asctime)s %(levelname)s %(message)s") - fh.setFormatter(fmt) - logger.addHandler(fh) + parent_dir = os.path.dirname(LOG_FILE) + if parent_dir and not os.path.exists(parent_dir): + os.makedirs(parent_dir, exist_ok=True) + + try: + fh = RotatingFileHandler(LOG_FILE, maxBytes=500_000, backupCount=3, encoding="utf-8") + fh.setLevel(logging.DEBUG) + fh.setFormatter(fmt) + logger.addHandler(fh) + except PermissionError: + fallback_log = "/tmp/irrigation_advisor.log" + try: + fh = RotatingFileHandler(fallback_log, maxBytes=500_000, backupCount=3, encoding="utf-8") + fh.setLevel(logging.DEBUG) + fh.setFormatter(fmt) + logger.addHandler(fh) + logger.warning("Permesso negato su %s, uso fallback %s", LOG_FILE, fallback_log) + except Exception: + sh = logging.StreamHandler() + sh.setLevel(logging.DEBUG) + sh.setFormatter(fmt) + logger.addHandler(sh) + logger.warning("Permesso negato su %s, fallback su stderr", LOG_FILE) + except Exception: + sh = logging.StreamHandler() + sh.setLevel(logging.DEBUG) + sh.setFormatter(fmt) + logger.addHandler(sh) + logger.warning("Errore logger file %s, fallback su stderr", LOG_FILE) if DEBUG: sh = logging.StreamHandler() diff --git a/services/telegram-bot/student_alert.py b/services/telegram-bot/student_alert.py index d73792d..693a96e 100644 --- a/services/telegram-bot/student_alert.py +++ b/services/telegram-bot/student_alert.py @@ -691,7 +691,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None if rain_dur > 0: parts.append(f"🌧️ pioggia forte da ~{html.escape(x['rain_persist_time'] or '—')} (durata ~{rain_dur:.0f}h, totale ~{rain_tot:.1f}mm)") else: - parts.append(f"🌧️ pioggia forte da ~{html.escape(x['rain_persist_time'] or '—')}") + parts.append(f"🌧️ pioggia forte da ~{html.escape(x['rain_persist_time'] or '—')}") line += " | ".join(parts) msg.append(line)