Expand description
Scheduled usage-report exports (DW-120).
DW-043 made the analytics store QUERYABLE live; this module makes it REPORTABLE durably: a background worker closes each UTC calendar window of the configured kind (hourly/daily/monthly) and writes the per-consumer usage statement — period totals of requests, errors, error rate, rate-limited and shed counts, plus the quota budget figures a billing pipeline consumes — as one deterministic file per configured format (CSV and JSON in v1; Parquet is deferred to the DW-156 backlog) into a configured directory. Distinct from the query API on purpose: this is durable, scheduled OUTPUT, not an ad hoc query.
§Reconciliation with the query API (the acceptance contract)
The statement’s analytics numbers ARE the structured query
endpoint’s numbers: run_export builds them by calling
query::structured itself (grouped by consumer, plus one
ungrouped totals row) over the same rollup_fixed tables through
the same aggregation helpers. Equality with POST /analytics/query for the same period holds BY CONSTRUCTION — and
is pinned by a test that parses the written JSON file and compares
it against an independent structured() call. The per-statement
consumer cap is 10_000 (the grouped query’s limit).
§Scheduling
The worker (spawned by the dataplane via
DataPlane::spawn_export_worker in dataplane::proxy) ticks
on the same interval-plus-shutdown-watch machinery as the rollup
cascade, reads the CURRENT config generation each tick (a reload
can add/remove exports without a restart), and exports every
closed window that has no successful run record yet — oldest
first, so a restart backfills missed windows. A window is due once
its close is at least
EXPORT_DELAY_MS in the past (writer flush + rollup grace +
cascade headroom); the delay keeps the statement as complete as
the store can make it. run_export also forces one maintain()
pass first, so even a manual trigger reads settled rollups.
§Quota columns and window alignment
The quota figures come from the STATE store’s quota_counters
(DW-033) — a domain this module cannot import (see
scripts/check_deps.py), so callers ABOVE both domains (the
dataplane worker, the admin trigger endpoint) read the counters
and hand them in as plain QuotaFigures per consumer name. The
alignment rule: a budget’s figures appear only when its quota
window FULLY CONTAINS the export window — a daily export carries
the same-UTC-day daily counter (exact period consumption) and
the month-to-date monthly counter; a monthly export carries only
the monthly counter (the daily budget’s per-day rows do not sum
to the month by construction and are omitted, never fabricated).
The monthly column reads the store’s LIVE month-to-date counter,
so a backfilled or re-exported window can carry a different
quota_monthly_used than the original run: analytics columns are
deterministic, quota columns are generation-time snapshots
(QuotaBudget’s window_start_epoch_s/reset_epoch_s bound
what used covers).
§Retention and partial windows
Rollup retention deletes old windows (DW-043); a window older than
the chosen granularity’s retention may be undercounted without the
store being able to know. Such runs are written with
partial: true in the file header and run record — real data,
flagged — and the scheduler never AUTO-exports windows past the
horizon (only a manual trigger can, still flagged). Quiet windows
(no traffic) are not loss: the statement’s counts are exact
whenever partial is false.
§Output shape
Files are named dwara-usage-{window}-{utc-stamp}.{ext} (stamp
YYYY-MM-DDThHZ hourly, YYYY-MM-DD daily, YYYY-MM- trimmed to
YYYY-MM monthly) and written atomically and DURABLY (temp
file, then fsync, rename, and a directory fsync — all inside the
destination directory): a re-export of the same window simply
overwrites — output is idempotent, the rollup recompute
philosophy. CSV is RFC 4180 (CRLF rows, quoted fields containing
commas/quotes/CR/LF, doubled quotes); absent quota cells are EMPTY
strings (zero means configured-and-zero-used). JSON is pretty and
self-describing (window bounds, generation time, partial flag,
totals, per-consumer rows).
Structs§
- Export
Run - One export attempt’s ledger row (the
export_runstable and the admin list endpoint’s shape). - Manual
RunBody - The manual trigger’s request body (DW-120 admin endpoint): which window kind to export and optionally which aligned window start.
- Quota
Budget - One budget’s figures as carried in a statement (plain DTO: the state-store types stay in the state domain, which this module cannot import).
- Quota
Figures - A consumer’s quota figures for one export window (see the module docs’ alignment rule: only budgets whose window fully contains the export window appear).
- Spend
ByModel - One per-model spend breakdown row (DW-079): the JSON statement’s
spend_by_modelarray member. Aggregated from theai_spendtable grouped by model over the export window. - Statement
Row - One per-consumer statement row (the JSON member shape; the CSV writer flattens the same fields in a fixed column order).
Enums§
- Export
Format - One output format (DW-120). Parquet is deliberately absent (lean dependency rule; see the DW-156 backlog issue).
- Window
Kind - The export window kinds (DW-120): fixed UTC calendar windows, the
closed set behind
analytics.exports.window.
Constants§
- EXPORT_
DELAY_ MS - How long after a window’s close before the scheduler will export it: the writer’s flush tick plus the rollup grace plus cascade headroom, with margin. Not a correctness knob — a manual trigger bypasses it.
- EXPORT_
TICK_ MS - The scheduler worker’s tick (same cadence class as the rollup cascade’s 30 s; the per-tick work is one cheap SQL probe).
- FAILURE_
RETRY_ MS - How long a FAILED window stays skipped before the scheduler retries it: without this, a persistent failure (unwritable directory, unusable store) would re-burn a full export attempt on every 30 s tick, forever. Time-based, not attempt-count-based — no schema change.
- MAX_
CATCHUP_ WINDOWS - Backfill bound per tick: after a long downtime the oldest-first catch-up drains at most this many windows per tick (one query + file writes each), keeping any single tick’s work bounded.
Functions§
- effective_
formats - The effective formats: the configured list, or both when the list is omitted or empty (the config default and the validation rule agree — an empty list MEANS both, it is never an error).
- effective_
window - The effective exports schedule from the config block: window kind (default daily) and formats (default both).
- last_
closed_ window - The most recent CLOSED window of a kind (the manual trigger’s
default): the window containing
nowis still open by construction, so the answer is always the previous one — including at the exact boundary instant, where the previous window has just closed. - list_
runs - List run records, newest first (the admin GET endpoint’s read).
- run_due
- The scheduler’s per-tick entry (DW-120): export every closed,
settled window of the configured kind that has no successful run
record yet — oldest first, bounded by
MAX_CATCHUP_WINDOWSper tick and by the query granularity’s retention horizon (windows past it would bepartial; a manual trigger can still force them). Failed runs are retried on later ticks, at most once perFAILURE_RETRY_MSwindow (the backoff). - run_
export - Export one window: settle rollups, read the statement through the
query API’s own aggregation, write one file per format, and upsert
the run record.
quota_atsupplies the quota figures for the window (period bounds in epoch SECONDS). Never panics on I/O: a failed run is recordedfailedwith the error string.