commit f214e2eafebb4ad431c5a9d89aae6cd45f31ac9e Author: luna Date: Wed Jul 8 21:07:59 2026 +0000 Initial OpenClaw usage exporter diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77ac754 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv/ +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..7abc29e --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# openclaw-daily-tokens-exporter + +Tiny FastAPI Prometheus exporter for OpenClaw daily token totals. + +## What it exports + +Reads OpenClaw session JSONL files and exposes today's totals at `/metrics`: + +- `openclaw_daily_total_tokens` +- `openclaw_daily_input_tokens` +- `openclaw_daily_output_tokens` +- `openclaw_daily_cache_read_tokens` +- `openclaw_daily_cache_write_tokens` +- `openclaw_daily_reasoning_tokens` +- `openclaw_daily_usage_entries` + +Timezone defaults to `Europe/Berlin`. + +## Run locally + +```bash +python3 -m venv .venv +. .venv/bin/activate +pip install -r requirements.txt +uvicorn app:app --host 127.0.0.1 --port 9487 +``` + +## Env + +- `OPENCLAW_SESSIONS_DIR` default: `/root/.openclaw/agents/main/sessions` +- `OPENCLAW_EXPORTER_TZ` default: `Europe/Berlin` +- `PORT` ignored by app directly; set it in the service/uvicorn command diff --git a/app.py b/app.py new file mode 100644 index 0000000..a7a28b1 --- /dev/null +++ b/app.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import json +import os +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from zoneinfo import ZoneInfo + +from fastapi import FastAPI +from fastapi.responses import PlainTextResponse + +SESSIONS_DIR = Path(os.environ.get("OPENCLAW_SESSIONS_DIR", "/root/.openclaw/agents/main/sessions")) +TIMEZONE = ZoneInfo(os.environ.get("OPENCLAW_EXPORTER_TZ", "Europe/Berlin")) + +app = FastAPI(title="openclaw-daily-tokens-exporter") + + +@dataclass +class Totals: + total: int = 0 + input: int = 0 + output: int = 0 + cache_read: int = 0 + cache_write: int = 0 + reasoning: int = 0 + entries: int = 0 + + +def _as_int(value: object) -> int: + if value is None: + return 0 + if isinstance(value, bool): + return int(value) + if isinstance(value, (int, float)): + return int(value) + return 0 + + +def _today_in_tz() -> datetime.date: + return datetime.now(TIMEZONE).date() + + +def collect_daily_totals() -> Totals: + today = _today_in_tz() + totals = Totals() + + for path in sorted(SESSIONS_DIR.glob("*.jsonl*")): + try: + with path.open("r", encoding="utf-8") as handle: + for line in handle: + if not line.strip(): + continue + try: + record = json.loads(line) + except json.JSONDecodeError: + continue + + if record.get("type") != "message": + continue + + message = record.get("message") or {} + usage = message.get("usage") + if not isinstance(usage, dict): + continue + + timestamp_ms = message.get("timestamp") + if not isinstance(timestamp_ms, (int, float)): + continue + + usage_date = datetime.fromtimestamp(timestamp_ms / 1000, TIMEZONE).date() + if usage_date != today: + continue + + totals.entries += 1 + totals.total += _as_int(usage.get("totalTokens")) + totals.input += _as_int(usage.get("input")) + totals.output += _as_int(usage.get("output")) + totals.cache_read += _as_int(usage.get("cacheRead")) + totals.cache_write += _as_int(usage.get("cacheWrite")) + totals.reasoning += _as_int(usage.get("reasoningTokens")) + except OSError: + continue + + return totals + + +@app.get("/metrics", response_class=PlainTextResponse) +def metrics() -> str: + totals = collect_daily_totals() + today = _today_in_tz().isoformat() + + lines = [ + "# HELP openclaw_daily_total_tokens Total OpenClaw tokens recorded today.", + "# TYPE openclaw_daily_total_tokens gauge", + f'openclaw_daily_total_tokens{{date="{today}",tz="{TIMEZONE.key}"}} {totals.total}', + "# HELP openclaw_daily_input_tokens OpenClaw input tokens recorded today.", + "# TYPE openclaw_daily_input_tokens gauge", + f'openclaw_daily_input_tokens{{date="{today}",tz="{TIMEZONE.key}"}} {totals.input}', + "# HELP openclaw_daily_output_tokens OpenClaw output tokens recorded today.", + "# TYPE openclaw_daily_output_tokens gauge", + f'openclaw_daily_output_tokens{{date="{today}",tz="{TIMEZONE.key}"}} {totals.output}', + "# HELP openclaw_daily_cache_read_tokens OpenClaw cache-read tokens recorded today.", + "# TYPE openclaw_daily_cache_read_tokens gauge", + f'openclaw_daily_cache_read_tokens{{date="{today}",tz="{TIMEZONE.key}"}} {totals.cache_read}', + "# HELP openclaw_daily_cache_write_tokens OpenClaw cache-write tokens recorded today.", + "# TYPE openclaw_daily_cache_write_tokens gauge", + f'openclaw_daily_cache_write_tokens{{date="{today}",tz="{TIMEZONE.key}"}} {totals.cache_write}', + "# HELP openclaw_daily_reasoning_tokens OpenClaw reasoning tokens recorded today.", + "# TYPE openclaw_daily_reasoning_tokens gauge", + f'openclaw_daily_reasoning_tokens{{date="{today}",tz="{TIMEZONE.key}"}} {totals.reasoning}', + "# HELP openclaw_daily_usage_entries OpenClaw usage entries recorded today.", + "# TYPE openclaw_daily_usage_entries gauge", + f'openclaw_daily_usage_entries{{date="{today}",tz="{TIMEZONE.key}"}} {totals.entries}', + "", + ] + return "\n".join(lines) diff --git a/openclaw-daily-tokens-exporter.service b/openclaw-daily-tokens-exporter.service new file mode 100644 index 0000000..e15fa59 --- /dev/null +++ b/openclaw-daily-tokens-exporter.service @@ -0,0 +1,16 @@ +[Unit] +Description=OpenClaw daily tokens Prometheus exporter +After=network-online.target openclaw-gateway.service +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/root/.openclaw/workspace/openclaw-daily-tokens-exporter +Environment=OPENCLAW_SESSIONS_DIR=/root/.openclaw/agents/main/sessions +Environment=OPENCLAW_EXPORTER_TZ=Europe/Berlin +ExecStart=/root/.openclaw/workspace/openclaw-daily-tokens-exporter/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 9487 +Restart=always +RestartSec=5 + +[Install] +WantedBy=default.target diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..135a119 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.116.1 +uvicorn==0.35.0