Update run-reports to generate per-domain reports
This commit is contained in:
parent
2ec9aac290
commit
27a73ec4e8
3 changed files with 101 additions and 14 deletions
14
README.md
14
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue