Skip to content

feat(metrics): record stream, queue and per-shard counters (#44) - #64

Merged
oreofeolurin merged 1 commit into
devfrom
fix/44-metrics-instrumentation
Aug 30, 2026
Merged

feat(metrics): record stream, queue and per-shard counters (#44)#64
oreofeolurin merged 1 commit into
devfrom
fix/44-metrics-instrumentation

Conversation

@oreofeolurin

Copy link
Copy Markdown
Contributor

Partial fix for #44 — the families with the clearest user impact. Scope is stated explicitly at the bottom rather than implied.

The problem

Families were registered and exported but never written, so they scraped as 0 forever:

flo_stream_append_records_total{topic="orders"} 0

...on a stream actively taking writes. That's worse than the family being absent — a dashboard or alert reads "no traffic" instead of "not instrumented".

What's wired

Path Now records
stream append recordAppend with the batch's logical record count — a 3-record batch counts 3, not 1 entry
stream read recordRead (same batch expansion), recordEmptyRead on nothing
queue enqueue/dequeue recordEnqueue, recordDequeue, recordEmptyDequeue
per-shard shardMetrics() had zero callers, so every flo_shard_* row exported 0 while its global equivalent moved

registerStream and registerQueue already return the per-entity metrics, and the handlers already called them and discarded the result with _ =. Most of this is capturing a pointer that was being thrown away.

Tests assert values, not presence

Four e2e tests, with a parser that pulls the number out of the exposition line rather than substring-matching:

try testing.expectEqual(@as(?u64, 3), seriesValue(resp.body, "flo_stream_append_records_total", "orders"));

All four fail with the instrumentation stashed — I checked rather than assumed. A metrics test that passes uninstrumented is precisely what let this hide, and the issue calls that out specifically.

Verified: test-unit, test-integration, and the metrics, e2e/stream, e2e/queue filters all pass.

Deliberately not in this change

processing and workflow — neither handler has a metrics_registry field and the shard doesn't wire one, so these need plumbing first rather than a call next to an existing site. Both also keep separate module-local metrics (src/processing/metrics.zig, context.zig) unrelated to the registry families, which is worth untangling in its own change.

KVMetrics and TieredLogMetricsexportPrometheus doesn't emit them and registerTieredLog has no callers. Whether to export or delete is a product decision; I'm not making it unilaterally.

The note in docs/deployment/clustering.mdx covers all these families, so it stays until the rest land. #44 should stay open.

Several metric families were registered and exported but never written, so
they scraped as 0 forever. That is worse than being absent: a dashboard built
on flo_stream_append_records_total{topic="orders"} reads "no traffic" on a
stream actively taking writes.

Wired the write paths to the record* methods that already existed:

- stream append — recordAppend with the batch's logical record count, so a
  three-record batch counts three rather than one entry
- stream read — recordRead, likewise summing batch contents, and
  recordEmptyRead when a read returns nothing
- queue enqueue/dequeue — recordEnqueue, recordDequeue, recordEmptyDequeue
- per-shard — shardMetrics() had no callers at all, so every flo_shard_* row
  exported 0 while its global flo_* equivalent moved. The shard now resolves
  its metrics once at wire-up and records alongside the global counters.

registerStream and registerQueue already return the per-entity metrics and the
handlers already called them, discarding the result — so most of this is
capturing a pointer that was being thrown away.

## Tests

Four e2e tests asserting values, not presence: append three records and assert
the counter says 3, read them back and assert the read counter says 3, enqueue
two and dequeue and assert both, and assert per-shard commands are non-zero.
A parser pulls the numeric value out of the exposition line rather than
substring-matching, so a wrong number fails instead of passing on the metric
name appearing.

All four fail with the instrumentation stashed — verified rather than assumed,
since a metrics test that passes uninstrumented is exactly what let this hide.

## Not in this change

- processing and workflow: neither handler has a metrics_registry field and the
  shard does not wire one, so those need plumbing first rather than a call at
  an existing site. Both keep separate module-local metrics that are unrelated
  to the registry families.
- KVMetrics and TieredLogMetrics: exportPrometheus does not emit them and
  registerTieredLog has no callers. Whether to export or delete them is a
  product decision, not a mechanical fix.

The note in docs/deployment/clustering.mdx covers all of these families, so it
stays until the rest land.
@oreofeolurin
oreofeolurin merged commit cca2aa9 into dev Aug 30, 2026
4 of 5 checks passed
oreofeolurin added a commit that referenced this pull request Aug 30, 2026
#67)

Completes the instrumentation started in #64. Both families exported 0
regardless of traffic because neither handler ever touched the registry.

Contrary to the note in #64, no plumbing was needed: every dispatch function in
both handlers already receives `shard: *Shard`, which carries the registry. The
counters go in at the points every transition already passes through:

- workflow: `completeRun` is the single terminal transition, so completed /
  failed / cancelled / timed_out all record there; started in `handleStart`,
  signal delivery in `handleSignal`, and step execution at the four
  `step_completed` history sites (all four have `shard` in scope).
- processing: submitted in `handleSubmit`, and `persistStatusChange` covers
  cancelled and stopped.

## A crash this surfaced

Eight workflow unit tests began aborting. Their test shard is built as
`var shard: Shard = undefined` with fields assigned one at a time, so
`metrics_registry` held garbage and the new read dereferenced it. The tests
passed before only because nothing in the workflow path read that field.

Both fields are now assigned in the helper. This is the third instance of the
same hazard today — `allocator.create` and `= undefined` both leave field
defaults inapplicable, and the compiler cannot see it.

## Tests

Two more value-asserting e2e tests: start two workflows and assert
`flo_workflow_started_total` is 2; submit a job and assert
`flo_processing_jobs_submitted_total` is 1. Both fail with the instrumentation
stashed.

test-unit, test-integration, and the metrics / workflow / processing e2e
filters all pass.

## Still open on #44

KVMetrics and TieredLogMetrics remain unexported and unregistered. Whether to
emit or delete them is a product decision, so #44 stays open for that alone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant