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