# eventmeter — event and duration counting for services Post counts and durations from your service; read them back as time-bucketed aggregates sliced by dimensions. No Prometheus, no ClickHouse, no Grafana. Base URL: https://eventmeter.muid.io ## Connect in three steps 1. ASK FOR A TOKEN. This is the one step that needs a person: message `vibe-eventmeter-owner` in SynqTask, saying the service name you want. You can read and prepare everything below while you wait. 2. INSTALL a client. TypeScript npm install @kiberos/eventmeter --registry https://npm.muid.io Python pip install kiberos-eventmeter --extra-index-url https://pip.muid.io/simple/ TYPESCRIPT ON A SLIM IMAGE (Bun-slim, Alpine, edge — anything with no native build chain): add `--no-optional` to the install, then pick a buffer for the events waiting to be sent: new RuntimeSqliteLocalBuffer({ path: '/app/data/metering-buffer.db' }) Needs nothing installed — it uses the SQLite your runtime already ships (bun:sqlite on any Bun, node:sqlite on Node 22.5+). Unflushed events SURVIVE a crash. Prefer this one. Put the file on a VOLUME, not in the working directory: in a container ./ is often not writable, and the container layer does not survive a redeploy. new MemoryLocalBuffer() Runs anywhere, but unflushed events do NOT survive a crash: a process killed between two flushes loses what it had not sent. Only where the two above are unavailable. Since 0.8.0 the native SQLite module is an optional dependency, and since 0.10.0 you no longer trade durability for it. THE PYTHON CLIENT NEEDS NONE OF THAT — it has no dependencies at all and buffers in memory already, so `--no-optional` and these buffers do not exist there. It carries the memory trade by default: it does not survive a crash either — a process killed between two flushes loses what it had not sent. 3. NAME YOUR SERVICE and record. `createMeter({ service: 'yourservice', ... })` — without it every batch is refused with 400. Metric names are qualified with your service name, and your token may only write its own namespace. 4. DECLARE THE UNIT of each metric — once, at deploy time, before or after its first event. Pick codes from GET /units (public), never prose: curl -X PUT https://eventmeter.muid.io/metrics/yourservice.hub.invoke \ -H "Authorization: Bearer $EVENTMETER_TOKEN" -H 'Content-Type: application/json' \ -d '{"unit":"ms","countUnit":"{call}","description":"one tool call"}' `unit` is what `sum` is measured in; `countUnit` is what one event is (default {event}). Codes are UCUM, as OpenTelemetry writes them: s, ms, By, g, and counted things in braces — {token}, {call}, {char}. Count units never add up with each other: tokens are not calls. 🔴 A metric that already has data keeps its unit forever — its stored sums are in that unit. A new unit needs a new metric name. Undeclared metrics are still accepted and read back with unit: null. The same call takes the rest of the metric's passport, changeable any time: "displayUnit" (show ms as s), "decimals" (0..9) and "rounding" (half_up, half_even, ceil, floor). Stored numbers stay exact; "display" in every read says how to show or bill them: round(sum * multiplier, decimals). 5. DERIVED METRICS — a formula over your declared metrics, checked by units when you declare it, computed when read, never written: -d '{"unit":"ms/{call}","formula":"sum(yourservice.hub.invoke) / count(yourservice.hub.invoke)"}' Formula: + - * / ( ), sum(metric), count(metric), numbers with a unit in brackets — 2.5 [s], 3 [{credit}/{call}]. Adding different quantities is refused ("cannot add … (time) to … ({char})"), a product derives its own unit, and it must match "unit". Try one first: POST /formulas/check {"formula": "...", "unit": "..."} answers ok or why it is nonsense. ## Endpoints GET /health public; touches the store, so it can answer false GET /llms.txt this page POST /ingest Bearer ; batched records GET /query Bearer; one metric over time, sliced by dimensions GET /list Bearer; metric names you may read GET /summary Bearer; several metrics in one answer GET /metrics Bearer; your metrics with their units (declared: false = not yet) PUT /metrics/ Bearer; declare its unit — step 4 — or a formula, step 5 POST /formulas/check Bearer; is this formula sensible, and in what unit GET /units public; the unit catalog to pick from Every read answers { "data": [...] } — the rows are under `data`, not at the top level and not under `series`. Reading the wrong key gives an empty list, which looks exactly like "nothing was recorded". /query adds "unit", "countUnit" and "display" beside "data"; /summary rows carry "unit". A /summary row spanning several metrics keeps its sum only when their units add up (seconds + milliseconds do; seconds + characters do not). A DERIVED metric answers "value" per point instead of count/sum. ## Two things that will bite you if nobody says them DIMENSIONS MUST BE LOW-CARDINALITY — a tool name, a platform, an outcome. Never a message id, request id or trace id. Cardinality is rows: one unique-per-call field turns a row per hour into a row per call. RECORDING NEVER BLOCKS OR THROWS in your hot path. It buffers locally and flushes in the background. A repeated batch does not double-count — that rides on batch identity, so a retry after a timeout is safe. ON SHUTDOWN, AWAIT meter.close(). It waits for the records made just before it and sends them in a final flush. Exit without it and the last records are lost — the ones from right before every deploy or restart. (Before 0.13.1 close() itself did not wait for them; upgrade.) Full guide: docs/CONNECTING-A-SERVICE.md in the eventmeter repository.