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
This commit is contained in:
Executable
+193
@@ -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()
|
||||
Reference in New Issue
Block a user