108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
# server.py
|
|
import os, json, time, sqlite3
|
|
from datetime import datetime
|
|
from typing import Any
|
|
from flask import Flask, request, jsonify, send_file
|
|
from flask_cors import CORS
|
|
import re
|
|
|
|
AUTH_KEY = os.getenv("AUTH_KEY", "dred123")
|
|
ROLLING_WINDOW_SECONDS = 3600 * 6 # 6 hours
|
|
|
|
app = Flask(__name__)
|
|
CORS(app)
|
|
|
|
|
|
def normalize_sqlite_key(key: str) -> str:
|
|
# Replace anything not alphanum or underscore with underscore
|
|
return re.sub(r'[^a-zA-Z0-9_]', '_', key)
|
|
|
|
|
|
def get_db_path(hostname: str) -> str:
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
host_clean = hostname.replace(" ", "_").replace("-", "_")
|
|
path = f"./dbs/{today}/{host_clean}.sqlite3"
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
return path
|
|
|
|
@app.route('/api/metrics', methods=['POST'])
|
|
def receive_metrics():
|
|
token = request.headers.get("Authorization", "").replace("Bearer ", "")
|
|
if token != AUTH_KEY:
|
|
return jsonify({"error": "unauthorized"}), 403
|
|
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({"error": "invalid JSON"}), 400
|
|
|
|
host = data.get("hostname") # crude host ID
|
|
timestamp = int(data.get("time", time.time()))
|
|
db_path = get_db_path(host)
|
|
table_name = f"{host.replace('-', '_')}" # sanitize
|
|
|
|
with sqlite3.connect(db_path) as db:
|
|
cursor = db.cursor()
|
|
|
|
# Create or alter table based on incoming keys
|
|
raw_keys = list(data.keys())
|
|
safe_keys = [normalize_sqlite_key(k) for k in raw_keys]
|
|
key_map = dict(zip(raw_keys, safe_keys))
|
|
|
|
columns = ", ".join([f"\"{k}\" TEXT" for k in safe_keys])
|
|
cursor.execute(f"""
|
|
CREATE TABLE IF NOT EXISTS "{table_name}" (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp INTEGER,
|
|
{columns}
|
|
)
|
|
""")
|
|
|
|
existing_cols = set(row[1] for row in cursor.execute(f"PRAGMA table_info('{table_name}')"))
|
|
for safe_k in safe_keys:
|
|
if safe_k not in existing_cols:
|
|
cursor.execute(f'ALTER TABLE "{table_name}" ADD COLUMN "{safe_k}" TEXT')
|
|
|
|
# Insert data
|
|
placeholders = ", ".join(["?"] * (1 + len(safe_keys))) # 1 for timestamp
|
|
cols = ", ".join(["timestamp"] + list(safe_keys))
|
|
values = [timestamp] + [
|
|
json.dumps(data[k]) if isinstance(data[k], (dict, list)) else str(data[k]) for k in raw_keys
|
|
]
|
|
|
|
cursor.execute(f'INSERT INTO "{table_name}" ({cols}) VALUES ({placeholders})', values)
|
|
db.commit()
|
|
|
|
return jsonify({"status": "ok", "table": table_name}), 200
|
|
|
|
@app.route('/api/data/<host>')
|
|
def serve_host_data(host) -> Any:
|
|
db_path = get_db_path(host)
|
|
cutoff = int(time.time()) - ROLLING_WINDOW_SECONDS
|
|
table_name = f"{host.replace('-', '_')}"
|
|
|
|
with sqlite3.connect(db_path) as db:
|
|
cursor = db.cursor()
|
|
try:
|
|
cur = cursor.execute(
|
|
f"SELECT timestamp, * FROM '{table_name}' WHERE timestamp > ? ORDER BY timestamp ASC",
|
|
(cutoff,)
|
|
)
|
|
cols = [desc[0] for desc in cur.description]
|
|
entries = [dict(zip(cols, row)) for row in cur.fetchall()]
|
|
except sqlite3.OperationalError:
|
|
return jsonify([]) # Empty if table doesn't exist yet
|
|
|
|
return jsonify(entries)
|
|
|
|
@app.route('/db/<host>')
|
|
def serve_sqlite_copy(host):
|
|
db_path = get_db_path(host)
|
|
if not os.path.exists(db_path):
|
|
return jsonify({"error": "No DB yet for this host"}), 404
|
|
|
|
if os.path.getsize(db_path) < 3_000_000: # ~3MB max
|
|
return send_file(db_path, mimetype='application/octet-stream')
|
|
return jsonify({"error": "DB too large"}), 413
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=7331)
|