Expand description
Real-time access-record stream (DW-121, feature analysis section 5 “Platform”): the record plumbing that mirrors the notice plumbing of DW-044. Where the event bus carries a handful of discrete operational events per minute (breaker transitions, ejections, config publishes), this pipeline carries ONE record per completed request — the raw firehose an operator points at their own warehouse or SIEM when the embedded analytics store (DW-043) is not the system of record. It complements, never replaces: the two are configured independently (a deployment can run either, both, or neither) and share nothing at runtime — different channels, different tasks, different failure domains.
§Placement
The stream lives in the events domain beside the webhook deliverer
because it IS an outbound delivery pipeline: the sink reuses the
DW-044 delivery engine (webhook::deliver_with_retry — one total
timeout per delivery, exponential backoff, the transient status
set) and consumes the access record type from observability, both
of which the events domain may import. The analytics domain cannot
(its dependency set is config/observability/extensions), and the
firehose needs nothing from the SQLite store — moving it there
would force a dependency-graph change to share an engine that
already lives one module over.
§Wire format (stable shape)
One NDJSON body per flushed batch — one JSON object per line, one
line per record, application/x-ndjson:
{"id":"rec-18f3c2a1b9d0-00000a","gateway":"dwara-8213-18f3c2910b07",
"timestamp":"2026-08-30T09:00:00.123Z","request_id":"req-...",
"listener":"edge","route":"billing","consumer":"acme",
"upstream":"billing-v1","endpoint":"10.0.0.4:8443","method":"GET",
"path":"/v1/invoices","status":200,"duration_ms":4.2,"attempts":1,
"rate_limited":false,"broken":false,"shed":false,
"dimensions":{"plan":"gold"}}The field list is the access record’s redacted-by-construction set:
path never carries a query string, there are no headers, no
credentials, and dimensions carries only config-declared
header-sourced tags (DW-043’s grammar, capped at 16 x 128 bytes).
id is process-unique and monotonic (rec-<hex unix ms>-<hex seq>)
and gateway is the process instance label — together they give a
receiver per-instance ordering even across restarts. A record whose
serialized line exceeds MAX_RECORD_BYTES (only possible via
absurd path/dimension lengths — the same bound class as a webhook
envelope) is dropped and counted, never truncated: a receiver
should never see a malformed line.
§Batching: one delivery per flushed batch
The flusher accumulates records and flushes on the first of:
batch_max records queued, the batch byte cap MAX_BATCH_BYTES
reached, or flush_ms elapsed since the batch’s first record (all
three read live from the current generation). ONE delivery — with
its whole retry cycle — is made per batch, not per record: a
warehouse ingest endpoint wants throughput-shaped traffic, and
per-record delivery would multiply the sink’s load by the batch
factor for zero information gain. Deliveries are made STRICTLY IN
ORDER, inline in the flusher task: a record firehose’s arrival
order is a real receiver expectation (unlike alert events, which
the deliverer dispatches as independent tasks). The cost of that
ordering is deliberate and bounded: one slow batch holds the queue
back by at most its timeout_ms budget, after which it fails and
the queue moves on — and a queue that outgrows its buffer bound
drops at OFFER time (counted), never blocks the request path.
§Emission contract: never backpressure the dataplane
AccessRecordStream::offer is an enabled-flag check plus a
bounded-channel try_send. When the queue is full (or the flusher
is stalled behind a slow batch) the record is DROPPED and the drop
counted (AccessRecordStream::dropped, surfaced as the
dwara_access_records_dropped_total gauge at scrape time). There
is no blocking wait and no unbounded buffer: a dead sink degrades
the stream’s completeness counters, never the gateway’s latency.
When the current generation has no compiled sink the flag is off
and offer returns before allocating anything — an unconfigured
stream costs one relaxed atomic load per request.
§The sink seam
RecordSink is deliberately small: one method, one batch, one
boolean answer. webhook is the one shipped implementation; a
Kafka producer is the documented second slot, deferred by the
lean-deps rule (the same decision that keeps Parquet out of DW-156
backlog territory: a sink that drags a client library must earn its
dependency weight). Sinks are compiled per config generation (a
kafka variant would compile its client the same way) and pushed
to the flusher over a watch channel — a reload retargets the stream
with no restart, mid-batch boundaries aside.
§Lifecycle
The channel and its capacity are constructed once per process (the capacity is a boot-time property; dwara-bin always constructs the stream so a live reload can arm it) and the flusher is one background task stopped by the shutdown watch, which drains what is already queued into one final flush attempt before returning. The gateway is not a durable queue: a record offered but not yet delivered at process death is lost with only its counters as the witness.
Structs§
- Access
Record Stream - The bounded record queue plus its counters and enabled flag. Share
via
Arc; the dataplane holds it behind anArcSwapOptionset once at boot. - Kafka
Record Sink - The Kafka batch sink (SCALE-08, #187,
analytics_stream.sink.kafka): produces each flushed batch as NDJSON messages to a Kafka topic via a Kafka REST Proxy. The REST Proxy endpoint receives the batch as an HTTP POST with theContent-Type: application/vnd.kafka.binary.v2+jsonmedia type; each NDJSON line becomes a Kafka message value (base64 encoded per the REST Proxy spec). Reuses the DW-044 webhook delivery engine’s retry/budget shape — one delivery (with its retries) per batch. - Stream
Record - One completed request as it crosses the stream channel: the access record’s owned copy plus the identity the flusher stamps into the envelope. Constructed on the request-completion path (the same allocation class as the analytics store’s raw-record copy).
- Stream
Targets - The per-generation compiled stream state (DW-121): pushed to the
flusher over a watch channel by the dataplane’s refresh. An empty
sinkslist is the disabled state — the offer path checks a flag the refresh derives from exactly this, so an unconfigured stream never queues a record. - Webhook
Record Sink - The webhook batch sink (DW-121,
analytics_stream.sink.webhook): one POST per flushed batch, body the NDJSON lines, media typeapplication/x-ndjson, delivered through the shared DW-044 engine (one total timeout per batch, exponential backoff, the transient status set — seeevents::webhookfor the contract).
Constants§
- MAX_
BATCH_ BYTES - Hard cap on one flushed batch (bytes), the byte half of the flush
trigger (with
batch_maxrecords, whichever comes first). Batches are delivered inline and in order, so this is also the flusher’s per-delivery memory bound. - MAX_
RECORD_ BYTES - Hard cap on one serialized record line (bytes). The field list is bounded (path length and 16x128-byte dimensions are the only growth axes), so this only binds absurd shapes; an over-cap line is dropped and counted, never sent truncated.
Traits§
- Record
Sink - One flushed batch’s delivery destination (DW-121). Deliberately small — one method, one batch, one answer — because the pipeline’s guarantees (ordering, bounded memory, drop-and-count) live in the FLUSHER, not in the sink: an implementation only decides where a batch goes and whether the receiver took it.
Functions§
- compile_
stream_ targets - Compile one generation’s stream state from the config block. A sink whose compilation fails is skipped with a LOUD error (fail closed — never delivered with placeholder bytes, never fatal to the generation); if that leaves no sinks, the stream is disabled and records drop at offer time with the counters as the witness.
- refresh_
stream_ gauges - Refresh the stream’s observation gauges at scrape time (the event bus’s model: the offer path bumps plain atomics, and only the scrape couples them to the registry).
- run_
stream_ flusher - The flusher loop (DW-121): drain the stream channel, assemble ordered batches, and deliver each batch — inline, in order, one retry cycle per batch — to the current generation’s sinks. Stops on shutdown after draining what is already queued into one final flush attempt (the gateway is not a durable queue; an offer undelivered at process death is lost with its counters as the witness).