262 lines
7.3 KiB
Python
262 lines
7.3 KiB
Python
"""SQLite database for storing metrics history."""
|
|
import sqlite3
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
import json
|
|
|
|
|
|
DB_PATH = Path("/app/data/metrics.db")
|
|
|
|
|
|
def get_connection() -> sqlite3.Connection:
|
|
"""Get database connection."""
|
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(str(DB_PATH))
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def init_db():
|
|
"""Initialize database tables."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Metrics history table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS metrics (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
service_name TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
latency_ms REAL,
|
|
message TEXT,
|
|
checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
# Incidents table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS incidents (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
service_name TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
message TEXT,
|
|
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
resolved_at TIMESTAMP,
|
|
notified BOOLEAN DEFAULT FALSE
|
|
)
|
|
""")
|
|
|
|
# SSL certificates table
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS ssl_certificates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
domain TEXT NOT NULL UNIQUE,
|
|
issuer TEXT,
|
|
expires_at TIMESTAMP,
|
|
days_until_expiry INTEGER,
|
|
checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
# Create indexes
|
|
cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_metrics_service_time
|
|
ON metrics(service_name, checked_at DESC)
|
|
""")
|
|
cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_incidents_service
|
|
ON incidents(service_name, started_at DESC)
|
|
""")
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def save_metric(service_name: str, status: str, latency_ms: Optional[float], message: Optional[str]):
|
|
"""Save a metric record."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"INSERT INTO metrics (service_name, status, latency_ms, message) VALUES (?, ?, ?, ?)",
|
|
(service_name, status, latency_ms, message)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def get_latency_history(service_name: str, hours: int = 24) -> list[dict]:
|
|
"""Get latency history for a service."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
since = datetime.now() - timedelta(hours=hours)
|
|
cursor.execute("""
|
|
SELECT latency_ms, status, checked_at
|
|
FROM metrics
|
|
WHERE service_name = ? AND checked_at > ? AND latency_ms IS NOT NULL
|
|
ORDER BY checked_at ASC
|
|
""", (service_name, since.isoformat()))
|
|
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
|
|
return [
|
|
{
|
|
"latency_ms": row["latency_ms"],
|
|
"status": row["status"],
|
|
"checked_at": row["checked_at"]
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
|
|
def get_uptime_stats(service_name: str, hours: int = 24) -> dict:
|
|
"""Calculate uptime statistics for a service."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
since = datetime.now() - timedelta(hours=hours)
|
|
|
|
cursor.execute("""
|
|
SELECT COUNT(*) as total,
|
|
SUM(CASE WHEN status = 'operational' THEN 1 ELSE 0 END) as successful
|
|
FROM metrics
|
|
WHERE service_name = ? AND checked_at > ?
|
|
""", (service_name, since.isoformat()))
|
|
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
|
|
total = row["total"] or 0
|
|
successful = row["successful"] or 0
|
|
|
|
return {
|
|
"total_checks": total,
|
|
"successful_checks": successful,
|
|
"uptime_percent": (successful / total * 100) if total > 0 else 100.0
|
|
}
|
|
|
|
|
|
def get_avg_latency(service_name: str, hours: int = 24) -> Optional[float]:
|
|
"""Get average latency for a service."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
since = datetime.now() - timedelta(hours=hours)
|
|
cursor.execute("""
|
|
SELECT AVG(latency_ms) as avg_latency
|
|
FROM metrics
|
|
WHERE service_name = ? AND checked_at > ? AND latency_ms IS NOT NULL
|
|
""", (service_name, since.isoformat()))
|
|
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
|
|
return row["avg_latency"]
|
|
|
|
|
|
def create_incident(service_name: str, status: str, message: Optional[str]) -> int:
|
|
"""Create a new incident."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"INSERT INTO incidents (service_name, status, message) VALUES (?, ?, ?)",
|
|
(service_name, status, message)
|
|
)
|
|
incident_id = cursor.lastrowid
|
|
conn.commit()
|
|
conn.close()
|
|
return incident_id
|
|
|
|
|
|
def resolve_incident(service_name: str):
|
|
"""Resolve open incidents for a service."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
UPDATE incidents
|
|
SET resolved_at = CURRENT_TIMESTAMP
|
|
WHERE service_name = ? AND resolved_at IS NULL
|
|
""", (service_name,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def get_open_incident(service_name: str) -> Optional[dict]:
|
|
"""Get open incident for a service."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
SELECT * FROM incidents
|
|
WHERE service_name = ? AND resolved_at IS NULL
|
|
ORDER BY started_at DESC LIMIT 1
|
|
""", (service_name,))
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
|
|
if row:
|
|
return dict(row)
|
|
return None
|
|
|
|
|
|
def mark_incident_notified(incident_id: int):
|
|
"""Mark incident as notified."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("UPDATE incidents SET notified = TRUE WHERE id = ?", (incident_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def get_recent_incidents(limit: int = 10) -> list[dict]:
|
|
"""Get recent incidents."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
SELECT * FROM incidents
|
|
ORDER BY started_at DESC
|
|
LIMIT ?
|
|
""", (limit,))
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
def save_ssl_info(domain: str, issuer: str, expires_at: datetime, days_until_expiry: int):
|
|
"""Save SSL certificate info."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
INSERT OR REPLACE INTO ssl_certificates
|
|
(domain, issuer, expires_at, days_until_expiry, checked_at)
|
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
""", (domain, issuer, expires_at.isoformat(), days_until_expiry))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def get_ssl_info(domain: str) -> Optional[dict]:
|
|
"""Get SSL certificate info."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT * FROM ssl_certificates WHERE domain = ?", (domain,))
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
|
|
if row:
|
|
return dict(row)
|
|
return None
|
|
|
|
|
|
def cleanup_old_metrics(days: int = 1):
|
|
"""Delete metrics older than specified days (default: 24 hours)."""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
cutoff = datetime.now() - timedelta(days=days)
|
|
cursor.execute("DELETE FROM metrics WHERE checked_at < ?", (cutoff.isoformat(),))
|
|
deleted = cursor.rowcount
|
|
conn.commit()
|
|
conn.close()
|
|
return deleted
|