commit e8b053071f76bcbe60983132a2b7e57cb010fce3 Author: hitonabi Date: Tue Jul 14 04:07:29 2026 +0200 feat: implementiere Blogwatcher Logging-Skript - JSON-Lines Format für strukturierte Protokollierung - 6 Kernmetriken (timestamp, action, blog_count, article_count, unread_count, duration_ms, success, error_message) - 4 optionale Metriken (feed_url, blog_name, article_id, source, session_id) - Integration via CLI-Parameter - Warnung an stderr bei Fehler - Logdatei: ~/.hermes/profiles/werkstatt/logs/blogwatcher.log diff --git a/scripts/blogwatcher-logger.py b/scripts/blogwatcher-logger.py new file mode 100755 index 0000000..7a469a8 --- /dev/null +++ b/scripts/blogwatcher-logger.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Blogwatcher Logging-Skript + +Protokolliert Blogwatcher-Aktivitäten im JSON-Lines-Format. +Logdatei: /home/hitonabi/.hermes/profiles/werkstatt/logs/blogwatcher.log +""" + +import argparse +import json +import os +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + + +def get_db_stats(db_path: str) -> dict: + """Holt aktuelle DB-Statistiken (blog_count, article_count, unread_count).""" + import sqlite3 + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM blogs") + blog_count = cursor.fetchone()[0] + cursor.execute("SELECT COUNT(*) FROM articles") + article_count = cursor.fetchone()[0] + cursor.execute("SELECT COUNT(*) FROM articles WHERE is_read = 0") + unread_count = cursor.fetchone()[0] + conn.close() + return { + "blog_count": blog_count, + "article_count": article_count, + "unread_count": unread_count, + } + + +def ensure_log_dir(log_path: Path) -> None: + """Erstellt Log-Verzeichnis, falls nötig.""" + log_path.parent.mkdir(parents=True, exist_ok=True) + + +def write_log_entry(log_path: Path, entry: dict) -> None: + """Schreibt einen JSON-Lines-Eintrag in die Logdatei.""" + ensure_log_dir(log_path) + with open(log_path, "a", encoding="utf-8") as f: + f.write(json.dumps(entry, ensure_ascii=False) + "\n") + + +def get_timestamp() -> str: + """Gibt aktuellen Timestamp im ISO 8601 UTC Format zurück.""" + return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def parse_bool(value: str) -> bool: + """Parsed einen String zu Boolean (true/false, 1/0, yes/no).""" + if isinstance(value, bool): + return value + return value.lower() in ("true", "1", "yes") + + +def main(): + parser = argparse.ArgumentParser( + description="Blogwatcher Logging-Skript - protokolliert Aktivitäten im JSON-Lines-Format" + ) + parser.add_argument( + "--action", + required=True, + choices=[ + "scan", "add_blog", "remove_blog", "read_article", "unread_article", + "read_all", "list_blogs", "list_articles", "import_opml" + ], + help="Aktion, die protokolliert wird" + ) + parser.add_argument( + "--duration", + type=int, + default=0, + help="Dauer der Aktion in Millisekunden (0 bei Listenoperationen)" + ) + parser.add_argument( + "--success", + type=str, + default="true", + help="Erfolg der Aktion (true/false, 1/0, yes/no)" + ) + parser.add_argument( + "--blog-count", + type=int, + default=None, + help="Anzahl verfolgter Blogs (default: aus DB abgefragt)" + ) + parser.add_argument( + "--article-count", + type=int, + default=None, + help="Anzahl gespeicherter Artikel (default: aus DB abgefragt)" + ) + parser.add_argument( + "--unread-count", + type=int, + default=None, + help="Anzahl ungelesener Artikel (default: aus DB abgefragt)" + ) + parser.add_argument( + "--error-message", + type=str, + default="", + help="Fehlerdetail bei success=false (optional)" + ) + parser.add_argument( + "--feed-url", + type=str, + default="", + help="Feed-URL bei scan/add_blog (optional)" + ) + parser.add_argument( + "--blog-name", + type=str, + default="", + help="Name des Blogs (optional)" + ) + parser.add_argument( + "--article-id", + type=int, + default=None, + help="ID des Artikels bei read/unread (optional)" + ) + parser.add_argument( + "--source", + type=str, + default="cli", + choices=["cli", "cron", "api_server", "telegram"], + help="Quelle der Aktion (default: cli)" + ) + parser.add_argument( + "--session-id", + type=str, + default="", + help="Session-ID (optional, aus Hermes)" + ) + + args = parser.parse_args() + + # DB-Stats abrufen, falls nicht übergeben + db_path = "/home/hitonabi/.blogwatcher-cli/blogwatcher-cli.db" + stats = get_db_stats(db_path) + + blog_count = args.blog_count if args.blog_count is not None else stats["blog_count"] + article_count = args.article_count if args.article_count is not None else stats["article_count"] + unread_count = args.unread_count if args.unread_count is not None else stats["unread_count"] + + # Log-Eintrag zusammenstellen + entry = { + "timestamp": get_timestamp(), + "action": args.action, + "blog_count": blog_count, + "article_count": article_count, + "unread_count": unread_count, + "duration_ms": args.duration, + "success": parse_bool(args.success), + "source": args.source, + "session_id": args.session_id, + } + + # Optionale Felder nur aufnehmen, wenn gesetzt + if args.error_message: + entry["error_message"] = args.error_message + if args.feed_url: + entry["feed_url"] = args.feed_url + if args.blog_name: + entry["blog_name"] = args.blog_name + if args.article_id is not None: + entry["article_id"] = args.article_id + + # Log-Pfad definieren + log_path = Path.home() / ".hermes" / "profiles" / "werkstatt" / "logs" / "blogwatcher.log" + + # Eintrag schreiben + try: + write_log_entry(log_path, entry) + except Exception as e: + print(f"ERROR: Could not write log entry: {e}", file=sys.stderr) + sys.exit(1) + + # Bei Fehler Warnung an stderr + if not parse_bool(args.success) and args.error_message: + print(f"WARN: Action '{args.action}' failed: {args.error_message}", file=sys.stderr) + + sys.exit(0) + + +if __name__ == "__main__": + main()