209 lines
5.7 KiB
Python
Executable File
209 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
metrics - Track arbitrary metrics over time
|
|
|
|
Log numeric values and see trends.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from statistics import mean, stdev
|
|
|
|
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
|
|
METRICS_FILE = WORKSPACE / "data" / "metrics.json"
|
|
|
|
def load_metrics() -> dict:
|
|
"""Load metrics data."""
|
|
METRICS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
if METRICS_FILE.exists():
|
|
with open(METRICS_FILE) as f:
|
|
return json.load(f)
|
|
return {'metrics': {}}
|
|
|
|
def save_metrics(data: dict):
|
|
"""Save metrics data."""
|
|
with open(METRICS_FILE, 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
def define_metric(name: str, unit: str = '', description: str = ''):
|
|
"""Define a new metric to track."""
|
|
data = load_metrics()
|
|
|
|
key = name.lower().replace(' ', '_')
|
|
if key in data['metrics']:
|
|
print(f"Metric already exists: {name}")
|
|
return
|
|
|
|
data['metrics'][key] = {
|
|
'name': name,
|
|
'unit': unit,
|
|
'description': description,
|
|
'entries': [],
|
|
'created': datetime.now().isoformat(),
|
|
}
|
|
|
|
save_metrics(data)
|
|
print(f"📊 Created metric: {name}")
|
|
|
|
def log_value(name: str, value: float, note: str = None):
|
|
"""Log a value for a metric."""
|
|
data = load_metrics()
|
|
key = name.lower().replace(' ', '_')
|
|
|
|
# Find metric by partial match
|
|
matches = [k for k in data['metrics'] if key in k]
|
|
if not matches:
|
|
print(f"Metric not found: {name}")
|
|
print("Create with: metrics define <name>")
|
|
return
|
|
|
|
key = matches[0]
|
|
metric = data['metrics'][key]
|
|
|
|
entry = {
|
|
'value': value,
|
|
'timestamp': datetime.now().isoformat(),
|
|
'note': note,
|
|
}
|
|
|
|
metric['entries'].append(entry)
|
|
save_metrics(data)
|
|
|
|
unit = metric.get('unit', '')
|
|
print(f"📈 {metric['name']}: {value}{unit}")
|
|
|
|
def show_metric(name: str, days: int = 7):
|
|
"""Show metric details and trend."""
|
|
data = load_metrics()
|
|
key = name.lower().replace(' ', '_')
|
|
|
|
matches = [k for k in data['metrics'] if key in k]
|
|
if not matches:
|
|
print(f"Metric not found: {name}")
|
|
return
|
|
|
|
key = matches[0]
|
|
metric = data['metrics'][key]
|
|
entries = metric['entries']
|
|
|
|
print(f"\n📊 {metric['name']}")
|
|
if metric.get('description'):
|
|
print(f" {metric['description']}")
|
|
print("=" * 40)
|
|
|
|
if not entries:
|
|
print("No data yet")
|
|
return
|
|
|
|
# Filter to date range
|
|
cutoff = datetime.now() - timedelta(days=days)
|
|
recent = [e for e in entries if datetime.fromisoformat(e['timestamp']) > cutoff]
|
|
|
|
if not recent:
|
|
print(f"No data in last {days} days")
|
|
return
|
|
|
|
values = [e['value'] for e in recent]
|
|
unit = metric.get('unit', '')
|
|
|
|
# Stats
|
|
avg = mean(values)
|
|
latest = values[-1]
|
|
high = max(values)
|
|
low = min(values)
|
|
|
|
print(f"\n Last {days} days ({len(recent)} entries):")
|
|
print(f" Latest: {latest}{unit}")
|
|
print(f" Average: {avg:.1f}{unit}")
|
|
print(f" High: {high}{unit}")
|
|
print(f" Low: {low}{unit}")
|
|
|
|
# Trend
|
|
if len(values) >= 2:
|
|
first_half = mean(values[:len(values)//2])
|
|
second_half = mean(values[len(values)//2:])
|
|
if second_half > first_half * 1.05:
|
|
print(f" Trend: 📈 Up")
|
|
elif second_half < first_half * 0.95:
|
|
print(f" Trend: 📉 Down")
|
|
else:
|
|
print(f" Trend: ➡️ Stable")
|
|
|
|
# Recent entries
|
|
print(f"\n Recent:")
|
|
for e in recent[-5:]:
|
|
date = e['timestamp'][:10]
|
|
note = f" ({e['note']})" if e.get('note') else ""
|
|
print(f" {date}: {e['value']}{unit}{note}")
|
|
|
|
print()
|
|
|
|
def list_metrics():
|
|
"""List all metrics."""
|
|
data = load_metrics()
|
|
|
|
if not data['metrics']:
|
|
print("No metrics defined yet")
|
|
print("Create with: metrics define <name> [unit] [description]")
|
|
return
|
|
|
|
print(f"\n📊 Metrics ({len(data['metrics'])})")
|
|
print("=" * 40)
|
|
|
|
for key, metric in data['metrics'].items():
|
|
entries = len(metric['entries'])
|
|
unit = metric.get('unit', '')
|
|
|
|
if entries > 0:
|
|
latest = metric['entries'][-1]['value']
|
|
print(f" {metric['name']}: {latest}{unit} ({entries} entries)")
|
|
else:
|
|
print(f" {metric['name']}: (no data)")
|
|
|
|
print()
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage:")
|
|
print(" metrics define <name> [unit] [description]")
|
|
print(" metrics log <name> <value> [note]")
|
|
print(" metrics show <name> [days]")
|
|
print(" metrics list")
|
|
print("")
|
|
print("Examples:")
|
|
print(" metrics define weight kg")
|
|
print(" metrics log weight 75.5")
|
|
print(" metrics show weight 30")
|
|
list_metrics()
|
|
return
|
|
|
|
cmd = sys.argv[1]
|
|
|
|
if cmd == 'define' and len(sys.argv) > 2:
|
|
name = sys.argv[2]
|
|
unit = sys.argv[3] if len(sys.argv) > 3 else ''
|
|
desc = ' '.join(sys.argv[4:]) if len(sys.argv) > 4 else ''
|
|
define_metric(name, unit, desc)
|
|
|
|
elif cmd == 'log' and len(sys.argv) > 3:
|
|
name = sys.argv[2]
|
|
value = float(sys.argv[3])
|
|
note = ' '.join(sys.argv[4:]) if len(sys.argv) > 4 else None
|
|
log_value(name, value, note)
|
|
|
|
elif cmd == 'show' and len(sys.argv) > 2:
|
|
name = sys.argv[2]
|
|
days = int(sys.argv[3]) if len(sys.argv) > 3 else 7
|
|
show_metric(name, days)
|
|
|
|
elif cmd == 'list':
|
|
list_metrics()
|
|
|
|
else:
|
|
print("Unknown command. Run 'metrics' for help.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|