# 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/') 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/') 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)