Update requirements and AGENTS instructions
This commit is contained in:
parent
e744276481
commit
d216433a07
6 changed files with 153 additions and 4 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -211,3 +211,6 @@ database/*.db
|
||||||
database/*.sqlite
|
database/*.sqlite
|
||||||
database/*.sqlite3
|
database/*.sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
# Generated reports
|
||||||
|
output/
|
||||||
|
|
|
@ -19,7 +19,10 @@ This document outlines general practices and expectations for AI agents assistin
|
||||||
```bash
|
```bash
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
````
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
The `init.sh` script can create this environment automatically. Always
|
||||||
|
activate it before running scripts or tests.
|
||||||
|
|
||||||
* Dependency management: Use `requirements.txt` or `pip-tools`
|
* Dependency management: Use `requirements.txt` or `pip-tools`
|
||||||
* Use standard libraries where feasible (e.g., `sqlite3`, `argparse`, `datetime`)
|
* Use standard libraries where feasible (e.g., `sqlite3`, `argparse`, `datetime`)
|
||||||
|
@ -96,3 +99,4 @@ As the project matures, agents may also:
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
* **2025-07-17**: Initial version by Jordan + ChatGPT
|
* **2025-07-17**: Initial version by Jordan + ChatGPT
|
||||||
|
* **2025-07-17**: Expanded virtual environment usage guidance
|
||||||
|
|
23
README.md
23
README.md
|
@ -1,4 +1,25 @@
|
||||||
# ngxstat
|
# ngxstat
|
||||||
Per-domain Nginx log analytics with hybrid static reports and live insights.
|
Per-domain Nginx log analytics with hybrid static reports and live insights.
|
||||||
|
|
||||||
##
|
## Generating Reports
|
||||||
|
|
||||||
|
Use the `generate_reports.py` script to build aggregated JSON and HTML files from `database/ngxstat.db`.
|
||||||
|
|
||||||
|
Create a virtual environment and install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run one or more of the interval commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/generate_reports.py hourly
|
||||||
|
python scripts/generate_reports.py daily
|
||||||
|
python scripts/generate_reports.py weekly
|
||||||
|
python scripts/generate_reports.py monthly
|
||||||
|
```
|
||||||
|
|
||||||
|
Reports are written under the `output/` directory. Each command updates the corresponding `<interval>.json` file and produces an HTML dashboard using Chart.js.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Core tools
|
# Core tools
|
||||||
typer[all] # For CLI commands
|
typer[all]>=0.9 # For CLI commands
|
||||||
Jinja2 # For static HTML generation
|
Jinja2>=3.1 # For static HTML generation
|
||||||
sqlite-utils # Optional: high-level SQLite handling
|
sqlite-utils # Optional: high-level SQLite handling
|
||||||
Flask # For optional lightweight API server
|
Flask # For optional lightweight API server
|
||||||
|
|
||||||
|
|
79
scripts/generate_reports.py
Normal file
79
scripts/generate_reports.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
DB_PATH = Path("database/ngxstat.db")
|
||||||
|
OUTPUT_DIR = Path("output")
|
||||||
|
TEMPLATE_DIR = Path("templates")
|
||||||
|
|
||||||
|
app = typer.Typer(help="Generate aggregated log reports")
|
||||||
|
|
||||||
|
def _load_existing(path: Path) -> List[Dict]:
|
||||||
|
if path.exists():
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text())
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
return []
|
||||||
|
|
||||||
|
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, json_name: str, out_path: Path) -> None:
|
||||||
|
env = Environment(loader=FileSystemLoader(TEMPLATE_DIR))
|
||||||
|
template = env.get_template("report.html")
|
||||||
|
out_path.write_text(template.render(interval=interval, json_path=json_name))
|
||||||
|
|
||||||
|
def _aggregate(interval: str, fmt: str) -> None:
|
||||||
|
json_path = OUTPUT_DIR / f"{interval}.json"
|
||||||
|
html_path = OUTPUT_DIR / f"{interval}.html"
|
||||||
|
|
||||||
|
existing = _load_existing(json_path)
|
||||||
|
last_bucket = existing[-1]["bucket"] if existing else None
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
query = f"SELECT strftime('{fmt}', datetime(time)) as bucket, COUNT(*) as hits FROM logs"
|
||||||
|
params = []
|
||||||
|
if last_bucket:
|
||||||
|
query += " WHERE datetime(time) > datetime(?)"
|
||||||
|
params.append(last_bucket)
|
||||||
|
query += " GROUP BY bucket ORDER BY bucket"
|
||||||
|
|
||||||
|
rows = cur.execute(query, params).fetchall()
|
||||||
|
for bucket, hits in rows:
|
||||||
|
existing.append({"bucket": bucket, "hits": hits})
|
||||||
|
|
||||||
|
existing.sort(key=lambda x: x["bucket"])
|
||||||
|
_save_json(json_path, existing)
|
||||||
|
_render_html(interval, json_path.name, html_path)
|
||||||
|
typer.echo(f"Generated {json_path} and {html_path}")
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def hourly() -> None:
|
||||||
|
"""Aggregate logs into hourly buckets."""
|
||||||
|
_aggregate("hourly", "%Y-%m-%d %H:00:00")
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def daily() -> None:
|
||||||
|
"""Aggregate logs into daily buckets."""
|
||||||
|
_aggregate("daily", "%Y-%m-%d")
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def weekly() -> None:
|
||||||
|
"""Aggregate logs into weekly buckets."""
|
||||||
|
_aggregate("weekly", "%Y-%W")
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def monthly() -> None:
|
||||||
|
"""Aggregate logs into monthly buckets."""
|
||||||
|
_aggregate("monthly", "%Y-%m")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
42
templates/report.html
Normal file
42
templates/report.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ interval.title() }} Report</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">{{ interval.title() }} Report</h1>
|
||||||
|
<canvas id="chart"></canvas>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
fetch('{{ json_path }}')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const labels = data.map(x => x.bucket);
|
||||||
|
const hits = data.map(x => x.hits);
|
||||||
|
new Chart(document.getElementById('chart'), {
|
||||||
|
type: '{{ 'bar' if interval == 'hourly' else 'line' }}',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Hits',
|
||||||
|
data: hits,
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
||||||
|
borderColor: 'rgba(54, 162, 235, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
fill: true,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
scales: {
|
||||||
|
y: { beginAtZero: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue