import json import sqlite3 from pathlib import Path from typing import List, Dict import yaml import typer from jinja2 import Environment, FileSystemLoader DB_PATH = Path("database/ngxstat.db") OUTPUT_DIR = Path("output") TEMPLATE_DIR = Path("templates") REPORT_CONFIG = Path("reports.yml") # Mapping of interval names to SQLite strftime formats. These strings are # substituted into report queries whenever the special ``{bucket}`` token is # present so that a single report definition can be reused for multiple # intervals. INTERVAL_FORMATS = { "hourly": "%Y-%m-%d %H:00:00", "daily": "%Y-%m-%d", "weekly": "%Y-%W", "monthly": "%Y-%m", } app = typer.Typer(help="Generate aggregated log reports") def _load_config() -> List[Dict]: if not REPORT_CONFIG.exists(): typer.echo(f"Config file not found: {REPORT_CONFIG}") raise typer.Exit(1) with REPORT_CONFIG.open("r") as fh: data = yaml.safe_load(fh) or [] if not isinstance(data, list): typer.echo("reports.yml must contain a list of report definitions") raise typer.Exit(1) return data def _save_json(path: Path, data: List[Dict]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2)) def _render_html(interval: str, reports: List[Dict], out_path: Path) -> None: env = Environment(loader=FileSystemLoader(TEMPLATE_DIR)) template = env.get_template("report.html") out_path.write_text(template.render(interval=interval, reports=reports)) def _bucket_expr(interval: str) -> str: """Return the SQLite strftime expression for the given interval.""" fmt = INTERVAL_FORMATS.get(interval) if not fmt: typer.echo(f"Unsupported interval: {interval}") raise typer.Exit(1) return f"strftime('{fmt}', datetime(time))" def _generate_interval(interval: str) -> None: cfg = _load_config() if not cfg: typer.echo("No report definitions found") return bucket = _bucket_expr(interval) conn = sqlite3.connect(DB_PATH) cur = conn.cursor() out_dir = OUTPUT_DIR / interval out_dir.mkdir(parents=True, exist_ok=True) report_list = [] for definition in cfg: name = definition["name"] query = definition["query"].replace("{bucket}", bucket) cur.execute(query) rows = cur.fetchall() headers = [c[0] for c in cur.description] data = [dict(zip(headers, row)) for row in rows] json_path = out_dir / f"{name}.json" _save_json(json_path, data) report_list.append({ "name": name, "label": definition.get("label", name.title()), "chart": definition.get("chart", "line"), "json": f"{name}.json", }) _save_json(out_dir / "reports.json", report_list) _render_html(interval, report_list, out_dir / "index.html") typer.echo(f"Generated {interval} reports") @app.command() def hourly() -> None: """Generate hourly reports.""" _generate_interval("hourly") @app.command() def daily() -> None: """Generate daily reports.""" _generate_interval("daily") @app.command() def weekly() -> None: """Generate weekly reports.""" _generate_interval("weekly") @app.command() def monthly() -> None: """Generate monthly reports.""" _generate_interval("monthly") if __name__ == "__main__": app()