Initial OpenClaw usage exporter
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
fastapi==0.116.1
|
||||
uvicorn==0.35.0
|
||||
Reference in New Issue
Block a user