Skip to content

refactor(runtime)!: rename the dispatch latency and source reconnect metrics - #279

Merged
mfw78 merged 1 commit into
mainfrom
refactor/source-metrics
Aug 18, 2026
Merged

refactor(runtime)!: rename the dispatch latency and source reconnect metrics#279
mfw78 merged 1 commit into
mainfrom
refactor/source-metrics

Conversation

@mfw78

@mfw78 mfw78 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This is the last item in the vocabulary freeze. nexum_runtime_event_latency_seconds and nexum_runtime_stream_reconnects_total were the two metric names still speaking the retired vocabulary, and the reconnect counter's kind label was the last bare kind on a metric surface.

Operator impact

A Prometheus series is its full label set including __name__, so a rename is not a rename to Prometheus. It is a death and a birth. At the deploy that carries this change, both old series stop being scraped and end at whatever cumulative value they last held, and the new series begin at zero. rate() and increase() lose the delta across that boundary: the last window before the deploy and the first window after it both see a truncated series, and the reconnect counter's apparent reset is not a process restart, it is the old name going away.

Old New
nexum_runtime_event_latency_seconds nexum_runtime_dispatch_latency_seconds
nexum_runtime_stream_reconnects_total nexum_runtime_source_reconnects_total
nexum_runtime_stream_reconnects_total{kind="..."} nexum_runtime_source_reconnects_total{source_kind="..."}

The label values are unchanged. block and chain-log are still the two values the reconnect counter carries, block per chain and chain-log also carrying module. Nothing else in the metric set moves: the other seven names, their types and their label sets are exactly as they were.

There is deliberately no dual-emit window

The usual courtesy for a metric rename is to emit both names for a release so dashboards and rules can be moved without a gap. This change does not do that, on purpose. Nothing is published, there is no released binary carrying the old names into anyone's Prometheus, and no dashboard or rule set exists anywhere outside docs/production.md, which moves to the new names in this same commit. A migration window exists to give somebody time to migrate. There is nobody to migrate. Building the dual emit would mean adding a second emit site at each of the three call sites, adding the shadow names to the METRICS table so the emitted-name scan stays green, documenting the deprecation in the handbook, and then reverting all of it a release later. That is machinery built to be removed, and it would put the retired vocabulary back into metrics.rs, which is the one file this freeze exists to clean.

The kind label

Renaming the label to source_kind forced the question of what its values should say, because the chain-log task disagreed with itself: the metric emitted "chain-log" while the tracing side emitted NextTrigger::StreamPanic("event"), so one source produced two words on two surfaces an operator correlates by hand.

The value stays chain-log on both, and the tracing side moved to match. The reasoning is that this value names the RPC transport, an eth_getLogs log stream, and the transport layer deliberately spells log because that is what the RPC returns. event is precisely the trigger word this freeze retires, so promoting it to a transport value would be the same category error running the other way. The issue's "does not change" section fixes the value set at block and chain-log, and that is what both surfaces now carry.

The tracing field keeps the name kind rather than following the metric label to source_kind. The divergence the issue describes is a disagreement of values, not of field names, and event_loop.rs is a contested file where only what the issue names should move.

The handbook's alert rules

Section 7's six rules were all checked against the emitted name set, not just the two that mention the renamed series. NexumReconnectStorm and NexumDispatchLatency were rewritten onto the new names in this commit. NexumModulePoisoned, NexumModuleTraps, NexumRpcErrorRate and NexumDown reference nexum_runtime_module_poisoned, nexum_runtime_module_errors_total{error_kind}, nexum_runtime_chain_request_total{outcome} and up{job="nexum"}, all of which still exist and match the scrape config in section 6. No rule in the handbook names a series that stopped existing at this deploy.

Section 6's metric table moves in the same commit, so the METRICS table, the emitted names and the handbook agree, and the emitted-name scan holds in both directions.

Review findings

Eight findings were raised across the review passes, four distinct after deduplication. Two were fixed, two were rejected.

Fixed, blocker: NexumDispatchLatency read a _bucket series the exporter never emitted. The rule reads rate(nexum_runtime_dispatch_latency_seconds_bucket[10m]) grouped by le, but PrometheusAddOn built a bare PrometheusBuilder::new() with no bucket configuration on either install path, and metrics-exporter-prometheus renders a histogram with no configured buckets as a quantile summary. Rendering a live recorder confirmed it: # TYPE ... summary, quantile labelled series, _sum and _count, no _bucket and no le anywhere. The rule matched an empty vector at every evaluation, so it could never enter pending and could never fire. It was equally dead before the rename, under the old name, and the rename would have carried a silent alert forward wearing a fresh name. Fixed by setting explicit buckets for nexum_runtime_dispatch_latency_seconds on both install paths, with bounds spanning the rule's 5 s threshold, and by a test that renders a local recorder and asserts the exposition is # TYPE ... histogram with _bucket series and le labels, so the rule and the exporter cannot drift apart again.

Fixed, major: section 6 declared the latency metric's type as histogram while the exposition said summary. The Type column is the operator's contract for how to aggregate a series, and it was untrue, which is what leads a reader to write a _bucket query and get an empty panel with no error. The buckets fix resolves this at the source rather than by editing the cell: the exporter now emits a real Prometheus histogram, so the row's declared type and label set are accurate as written, and the new test pins them.

Rejected, minor: the reopen path logs "event source reopened" immediately above a counter that emits source_kind="chain-log". The observation is correct and the mismatch is real, but the fix was not taken. The issue's done-when requires the tracing field and the metric label to agree, and they do, both chain-log. The issue also explicitly endorses the "event source" prose vocabulary elsewhere in its text, so changing it here would contradict the specification this change implements, and event_loop.rs is a contested file where only what the issue names may move. The prose sweep belongs to its own change with its own specification.

Rejected, minor: a stray untracked test file was reported to fail just ci. crates/nexum-runtime/tests/zz_reviewer_probe.rs does not exist. The tests/ directory is absent from the crate, the worktree is clean, and the full run passes 785 of 785.

Verification

nix develop --command just ci is green on this commit: fmt, clippy under -D warnings, nextest at 785 passed of 785, doctests and content-lint. That count includes the new bucket rendering test.

Closes #243

AI Assistance: Claude Fable 5 and Claude Opus 5 used for the implementation, review, and this description.

@mfw78
mfw78 force-pushed the refactor/sources-not-subscriptions branch from b172b21 to b5c4bba Compare August 18, 2026 01:14
@mfw78
mfw78 force-pushed the refactor/source-metrics branch from 722f1b0 to 77160fd Compare August 18, 2026 01:16
Base automatically changed from refactor/sources-not-subscriptions to main August 18, 2026 01:17
…metrics

Rename nexum_runtime_event_latency_seconds to
nexum_runtime_dispatch_latency_seconds and
nexum_runtime_stream_reconnects_total to
nexum_runtime_source_reconnects_total, and rename the reconnect counter
label kind to source_kind.

The label values stay block and chain-log: the value names the RPC
transport, not a trigger. The chain-log reconnect task's tracing field
carried event; it now carries chain-log, so the log line and the series
agree for one source.

The dropped and latency help strings now say triggers, and the reconnect
help describes the emitted label set. docs/production.md section 6 and
the section 7 alert rules move to the new names in the same change.

The exporter now sets buckets for the latency histogram. Without them
metrics-exporter-prometheus renders a histogram as a quantile summary,
so the section 7 latency rule read a _bucket series that never existed
and could never fire, and the section 6 type column was untrue. A test
renders the recorder and holds the metric to its bucket series.

BREAKING CHANGE: a Prometheus series is its full label set including
__name__, so each renamed series ends at its cumulative value and a new
series starts at zero across the deploy; rate() and increase() lose the
delta at that boundary. There is no dual-emit window.

Closes #243

AI Assistance: Claude Fable 5 used for the implementation.
@mfw78
mfw78 force-pushed the refactor/source-metrics branch from 77160fd to 4c48d04 Compare August 18, 2026 01:18
@mfw78
mfw78 merged commit 7213bff into main Aug 18, 2026
5 checks passed
@mfw78
mfw78 deleted the refactor/source-metrics branch August 18, 2026 01:25
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.

observability: dispatch latency and source reconnects

1 participant