Update run-reports to generate per-domain reports

This commit is contained in:
Jordan Wages 2025-07-18 02:10:46 -05:00
commit 27a73ec4e8
3 changed files with 101 additions and 14 deletions

View file

@ -22,6 +22,18 @@ python scripts/generate_reports.py weekly
python scripts/generate_reports.py monthly python scripts/generate_reports.py monthly
``` ```
Each command accepts optional flags to generate per-domain reports. Use
`--domain <name>` to limit output to a specific domain or `--all-domains`
to generate a subdirectory for every domain found in the database:
```bash
# Hourly reports for example.com only
python scripts/generate_reports.py hourly --domain example.com
# Weekly reports for all domains individually
python scripts/generate_reports.py weekly --all-domains
```
Reports are written under the `output/` directory. Each command updates the corresponding `<interval>.json` file and produces an HTML dashboard using Chart.js. Reports are written under the `output/` directory. Each command updates the corresponding `<interval>.json` file and produces an HTML dashboard using Chart.js.
### Configuring Reports ### Configuring Reports
@ -73,7 +85,7 @@ Use the `run-reports.sh` script to run all report intervals in one step. The scr
./run-reports.sh ./run-reports.sh
``` ```
Running this script will create or update the hourly, daily, weekly and monthly reports under `output/`. Running this script will create or update the hourly, daily, weekly and monthly reports under `output/`. It also detects all unique domains found in the database and writes per-domain reports to `output/<domain>/<interval>` alongside the aggregate data.
## Serving Reports with Nginx ## Serving Reports with Nginx

View file

@ -18,13 +18,20 @@ else
source .venv/bin/activate source .venv/bin/activate
fi fi
# Generate all reports # Generate reports for all domains combined
echo "[INFO] Generating reports..." echo "[INFO] Generating aggregate reports..."
python scripts/generate_reports.py hourly python scripts/generate_reports.py hourly
python scripts/generate_reports.py daily python scripts/generate_reports.py daily
python scripts/generate_reports.py weekly python scripts/generate_reports.py weekly
python scripts/generate_reports.py monthly python scripts/generate_reports.py monthly
# Generate reports for each individual domain
echo "[INFO] Generating per-domain reports..."
python scripts/generate_reports.py hourly --all-domains
python scripts/generate_reports.py daily --all-domains
python scripts/generate_reports.py weekly --all-domains
python scripts/generate_reports.py monthly --all-domains
# Deactivate to keep cron environment clean # Deactivate to keep cron environment clean
if type deactivate >/dev/null 2>&1; then if type deactivate >/dev/null 2>&1; then
deactivate deactivate

View file

@ -1,7 +1,7 @@
import json import json
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
from typing import List, Dict from typing import List, Dict, Optional
import yaml import yaml
@ -26,6 +26,16 @@ INTERVAL_FORMATS = {
app = typer.Typer(help="Generate aggregated log reports") app = typer.Typer(help="Generate aggregated log reports")
def _get_domains() -> List[str]:
"""Return a sorted list of unique domains from the logs table."""
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("SELECT DISTINCT host FROM logs ORDER BY host")
domains = [row[0] for row in cur.fetchall()]
conn.close()
return domains
def _load_config() -> List[Dict]: def _load_config() -> List[Dict]:
if not REPORT_CONFIG.exists(): if not REPORT_CONFIG.exists():
typer.echo(f"Config file not found: {REPORT_CONFIG}") typer.echo(f"Config file not found: {REPORT_CONFIG}")
@ -55,7 +65,7 @@ def _bucket_expr(interval: str) -> str:
raise typer.Exit(1) raise typer.Exit(1)
return f"strftime('{fmt}', datetime(time))" return f"strftime('{fmt}', datetime(time))"
def _generate_interval(interval: str) -> None: def _generate_interval(interval: str, domain: Optional[str] = None) -> None:
cfg = _load_config() cfg = _load_config()
if not cfg: if not cfg:
typer.echo("No report definitions found") typer.echo("No report definitions found")
@ -66,13 +76,25 @@ def _generate_interval(interval: str) -> None:
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
cur = conn.cursor() cur = conn.cursor()
out_dir = OUTPUT_DIR / interval # Create a temporary view so queries can easily be filtered by domain
cur.execute("DROP VIEW IF EXISTS logs_view")
if domain:
cur.execute(
"CREATE TEMP VIEW logs_view AS SELECT * FROM logs WHERE host = ?",
(domain,),
)
out_dir = OUTPUT_DIR / domain / interval
else:
cur.execute("CREATE TEMP VIEW logs_view AS SELECT * FROM logs")
out_dir = OUTPUT_DIR / interval
out_dir.mkdir(parents=True, exist_ok=True) out_dir.mkdir(parents=True, exist_ok=True)
report_list = [] report_list = []
for definition in cfg: for definition in cfg:
name = definition["name"] name = definition["name"]
query = definition["query"].replace("{bucket}", bucket) query = definition["query"].replace("{bucket}", bucket)
query = query.replace("FROM logs", "FROM logs_view")
cur.execute(query) cur.execute(query)
rows = cur.fetchall() rows = cur.fetchall()
headers = [c[0] for c in cur.description] headers = [c[0] for c in cur.description]
@ -90,25 +112,71 @@ def _generate_interval(interval: str) -> None:
_render_html(interval, report_list, out_dir / "index.html") _render_html(interval, report_list, out_dir / "index.html")
typer.echo(f"Generated {interval} reports") typer.echo(f"Generated {interval} reports")
def _generate_all_domains(interval: str) -> None:
"""Generate reports for each unique domain."""
for domain in _get_domains():
_generate_interval(interval, domain)
@app.command() @app.command()
def hourly() -> None: def hourly(
domain: Optional[str] = typer.Option(
None, help="Generate reports for a specific domain"
),
all_domains: bool = typer.Option(
False, "--all-domains", help="Generate reports for each domain"
),
) -> None:
"""Generate hourly reports.""" """Generate hourly reports."""
_generate_interval("hourly") if all_domains:
_generate_all_domains("hourly")
else:
_generate_interval("hourly", domain)
@app.command() @app.command()
def daily() -> None: def daily(
domain: Optional[str] = typer.Option(
None, help="Generate reports for a specific domain"
),
all_domains: bool = typer.Option(
False, "--all-domains", help="Generate reports for each domain"
),
) -> None:
"""Generate daily reports.""" """Generate daily reports."""
_generate_interval("daily") if all_domains:
_generate_all_domains("daily")
else:
_generate_interval("daily", domain)
@app.command() @app.command()
def weekly() -> None: def weekly(
domain: Optional[str] = typer.Option(
None, help="Generate reports for a specific domain"
),
all_domains: bool = typer.Option(
False, "--all-domains", help="Generate reports for each domain"
),
) -> None:
"""Generate weekly reports.""" """Generate weekly reports."""
_generate_interval("weekly") if all_domains:
_generate_all_domains("weekly")
else:
_generate_interval("weekly", domain)
@app.command() @app.command()
def monthly() -> None: def monthly(
domain: Optional[str] = typer.Option(
None, help="Generate reports for a specific domain"
),
all_domains: bool = typer.Option(
False, "--all-domains", help="Generate reports for each domain"
),
) -> None:
"""Generate monthly reports.""" """Generate monthly reports."""
_generate_interval("monthly") if all_domains:
_generate_all_domains("monthly")
else:
_generate_interval("monthly", domain)
if __name__ == "__main__": if __name__ == "__main__":
app() app()