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