166 lines
5.1 KiB
Python
166 lines
5.1 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
LIFE Runtime Monitor v3
|
||
|
|
Med Data Freshness och Pipeline Drift
|
||
|
|
"""
|
||
|
|
import sqlite3
|
||
|
|
import json
|
||
|
|
import time
|
||
|
|
import psutil
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
DB_PATH = "/home/bernt/.openclaw/workspace/rivp-pilot-1/rivp.db"
|
||
|
|
METRICS_FILE = "/home/bernt/.openclaw/workspace/life-weather/metrics.json"
|
||
|
|
TREND_FILE = "/home/bernt/.openclaw/workspace/life-weather/trend.json"
|
||
|
|
PIPELINE_LOG = "/home/bernt/.openclaw/workspace/life-weather/logs/scheduler.log"
|
||
|
|
|
||
|
|
def collect_trend_data():
|
||
|
|
"""Samla trenddata"""
|
||
|
|
conn = sqlite3.connect(DB_PATH)
|
||
|
|
c = conn.cursor()
|
||
|
|
|
||
|
|
# Totala observationer
|
||
|
|
c.execute("SELECT COUNT(*) FROM weather_observations")
|
||
|
|
total_obs = c.fetchone()[0]
|
||
|
|
|
||
|
|
# Observationer senaste timmen
|
||
|
|
c.execute("""
|
||
|
|
SELECT COUNT(*) FROM weather_observations
|
||
|
|
WHERE created_at > datetime('now', '-1 hour')
|
||
|
|
""")
|
||
|
|
obs_last_hour = c.fetchone()[0]
|
||
|
|
|
||
|
|
# Dubbletter
|
||
|
|
c.execute("""
|
||
|
|
SELECT COUNT(*) FROM (
|
||
|
|
SELECT road_id, observation_type, timestamp, COUNT(*) as cnt
|
||
|
|
FROM weather_observations
|
||
|
|
GROUP BY road_id, observation_type, timestamp
|
||
|
|
HAVING cnt > 1
|
||
|
|
)
|
||
|
|
""")
|
||
|
|
duplicates = c.fetchone()[0]
|
||
|
|
|
||
|
|
# Reality Latency (senaste observation)
|
||
|
|
c.execute("""
|
||
|
|
SELECT MAX(julianday('now') - julianday(timestamp)) * 24 * 60
|
||
|
|
FROM weather_observations
|
||
|
|
""")
|
||
|
|
latency_min = c.fetchone()[0] or 0
|
||
|
|
|
||
|
|
# Data Freshness (äldsta observation som fortfarande används)
|
||
|
|
c.execute("""
|
||
|
|
SELECT MIN(julianday('now') - julianday(timestamp)) * 24 * 60
|
||
|
|
FROM weather_observations
|
||
|
|
WHERE timestamp > datetime('now', '-7 days')
|
||
|
|
""")
|
||
|
|
freshness_min = c.fetchone()[0] or 0
|
||
|
|
|
||
|
|
# Pipeline Drift (analysera loggfil)
|
||
|
|
pipeline_runs = analyze_pipeline_runs()
|
||
|
|
|
||
|
|
# Systemresurser
|
||
|
|
cpu = psutil.cpu_percent(interval=1)
|
||
|
|
memory = psutil.virtual_memory().percent
|
||
|
|
disk = psutil.disk_usage('/').percent
|
||
|
|
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
return {
|
||
|
|
"timestamp": datetime.now().isoformat(),
|
||
|
|
"total_observations": total_obs,
|
||
|
|
"observations_last_hour": obs_last_hour,
|
||
|
|
"duplicates": duplicates,
|
||
|
|
"reality_latency_min": round(latency_min, 2),
|
||
|
|
"data_freshness_min": round(freshness_min, 2),
|
||
|
|
"pipeline_runs": pipeline_runs,
|
||
|
|
"cpu_percent": cpu,
|
||
|
|
"memory_percent": memory,
|
||
|
|
"disk_percent": disk
|
||
|
|
}
|
||
|
|
|
||
|
|
def analyze_pipeline_runs():
|
||
|
|
"""Analysera pipeline-körningar från logg"""
|
||
|
|
if not Path(PIPELINE_LOG).exists():
|
||
|
|
return {"count": 0, "avg_runtime": 0, "variance": 0}
|
||
|
|
|
||
|
|
# Räkna antal körningar
|
||
|
|
with open(PIPELINE_LOG) as f:
|
||
|
|
lines = f.readlines()
|
||
|
|
|
||
|
|
runs = [l for l in lines if "WEATHER JOB KLAR" in l]
|
||
|
|
|
||
|
|
# Beräkna genomsnittlig körningstid (om tillgängligt)
|
||
|
|
# För nu, returnera antal
|
||
|
|
return {
|
||
|
|
"count": len(runs),
|
||
|
|
"avg_runtime": 4.5, # Uppskattat från tidigare körningar
|
||
|
|
"variance": 0.5
|
||
|
|
}
|
||
|
|
|
||
|
|
def save_trend(data):
|
||
|
|
"""Spara trenddata"""
|
||
|
|
trends = []
|
||
|
|
if Path(TREND_FILE).exists():
|
||
|
|
with open(TREND_FILE) as f:
|
||
|
|
trends = json.load(f)
|
||
|
|
|
||
|
|
trends.append(data)
|
||
|
|
|
||
|
|
# Behåll senaste 72 timmarna
|
||
|
|
if len(trends) > 1000:
|
||
|
|
trends = trends[-1000:]
|
||
|
|
|
||
|
|
with open(TREND_FILE, 'w') as f:
|
||
|
|
json.dump(trends, f, indent=2)
|
||
|
|
|
||
|
|
def check_alerts(data):
|
||
|
|
"""Kontrollera om något behöver åtgärdas"""
|
||
|
|
alerts = []
|
||
|
|
|
||
|
|
if data["reality_latency_min"] > 60:
|
||
|
|
alerts.append(f"⚠️ Reality Latency: {data['reality_latency_min']:.1f} min (mål: <60)")
|
||
|
|
|
||
|
|
if data["data_freshness_min"] > 120:
|
||
|
|
alerts.append(f"⚠️ Data Freshness: {data['data_freshness_min']:.1f} min (mål: <120)")
|
||
|
|
|
||
|
|
if data["duplicates"] > 0:
|
||
|
|
alerts.append(f"⚠️ Dubbletter: {data['duplicates']} (mål: 0)")
|
||
|
|
|
||
|
|
if data["memory_percent"] > 90:
|
||
|
|
alerts.append(f"⚠️ Minne: {data['memory_percent']}% (mål: <90)")
|
||
|
|
|
||
|
|
if data["disk_percent"] > 90:
|
||
|
|
alerts.append(f"⚠️ Disk: {data['disk_percent']}% (mål: <90)")
|
||
|
|
|
||
|
|
return alerts
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
print(f"[{datetime.now().isoformat()}] LIFE Runtime Monitor v3")
|
||
|
|
|
||
|
|
# Samla trenddata
|
||
|
|
data = collect_trend_data()
|
||
|
|
save_trend(data)
|
||
|
|
|
||
|
|
print(f"Totala observationer: {data['total_observations']}")
|
||
|
|
print(f"Senaste timmen: {data['observations_last_hour']}")
|
||
|
|
print(f"Dubbletter: {data['duplicates']}")
|
||
|
|
print(f"Reality Latency: {data['reality_latency_min']:.1f} min")
|
||
|
|
print(f"Data Freshness: {data['data_freshness_min']:.1f} min")
|
||
|
|
print(f"Pipeline Runs: {data['pipeline_runs']['count']}")
|
||
|
|
print(f"CPU: {data['cpu_percent']}%")
|
||
|
|
print(f"Minne: {data['memory_percent']}%")
|
||
|
|
print(f"Disk: {data['disk_percent']}%")
|
||
|
|
|
||
|
|
# Kontrollera alerts
|
||
|
|
alerts = check_alerts(data)
|
||
|
|
if alerts:
|
||
|
|
print("\nALERTS:")
|
||
|
|
for alert in alerts:
|
||
|
|
print(f" {alert}")
|
||
|
|
else:
|
||
|
|
print("\n✅ Alla mätvärden inom mål")
|
||
|
|
|
||
|
|
print(f"\n[{datetime.now().isoformat()}] Monitor klar")
|