Skip to content

Replace prom-client with hand-rolled Prometheus text rendering - #1441

Merged
DZakh merged 25 commits into
mainfrom
claude/prom-metrics-indexer-state-0ezqlb
Jul 21, 2026
Merged

Replace prom-client with hand-rolled Prometheus text rendering#1441
DZakh merged 25 commits into
mainfrom
claude/prom-metrics-indexer-state-0ezqlb

Conversation

@DZakh

@DZakh DZakh commented Jul 17, 2026

Copy link
Copy Markdown
Member

Summary

Removes the prom-client library dependency and replaces it with hand-rolled Prometheus text format rendering. Metrics are now computed from live indexer state at scrape time rather than imperatively updated through a global registry, eliminating the need for external metric management.

Key Changes

  • Removed Prometheus.res: Deleted 789 lines of prom-client wrapper code including SafeCounter, SafeGauge, and all metric modules
  • Rewrote Metrics.res: Replaced pull-based prom-client rendering with hand-rolled Prometheus text format generation
    • New builder-based API (block, sample, series, seriesOpt, single) for composing metrics
    • Metrics rendered directly from IndexerState and its sub-states at scrape time
    • Proper float formatting (max 3 decimals) matching Prometheus conventions
  • Migrated metric state to IndexerState:
    • Added handlerStat, storageLoadStat, storageWriteStat, historyPruneStat types
    • Counters now live on state objects instead of global prom-client registry
    • Added accessor functions and record* operations for mutation
  • Updated ChainState: Added per-chain metric counters (blockRangeFetchSeconds, blockRangeParseSeconds, reorgCount, etc.)
  • Updated EffectState: Added effectStats type with call/queue/cache metrics; removed queueCount from rate limit state
  • Updated SourceManager: Added reportedHeight and cumulative time counters (idleSeconds, waitingForNewBlockSeconds, queryingSeconds)
  • Removed metric calls throughout codebase: Deleted imperative prom-client calls from EventProcessing, ChainFetching, LoadLayer, PgStorage, Rollback, PruneStaleHistory, FetchState, and CrossChainState
  • Updated test helpers: Removed prom-client registry reset in MockIndexer; updated Metrics_test.res to test new builder API

Implementation Details

  • Metrics are now pull-based: computed from state when scraped rather than pushed imperatively
  • State mutations are monotonic: counters only increase, enabling safe rollback handling
  • No global state: metrics live on indexer state objects, making them testable and avoiding singleton issues
  • Efficient rendering: uses string concatenation (V8 ConsString optimization) rather than array joins
  • Backward compatibility maintained for legacy hyperindex_synced_to_head metric

https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups

Summary by CodeRabbit

  • New Features
    • Revamped /metrics to render from a precomputed indexing-state snapshot, adding richer timing/count metrics for handler execution, storage reads/writes, effect/queue activity, rollbacks, reorg detection, and history pruning.
    • Updated console and TUI chain details to use the same snapshot, including expanded progress/reorg/rollback telemetry.
  • Bug Fixes
    • Improved metrics continuity during reorgs/rollbacks by recording events in state rather than updating exported counters directly; reduced restart-related double-counting.
  • Tests
    • Updated metrics tests and mocks to match the new rendering behavior and updated writeBatch callback usage.

Delete Prometheus.res and the prom-client default registry usage. Counters
now live on IndexerState (batch/handler/storage/rollback stats), ChainState
(block-range fetch, reorg, latency), SourceManager (status seconds, source
heights) and EffectState (per-effect call/cache/queue stats), while gauges
are derived from live state at scrape time. Metrics.res renders the /metrics
text with declarative builder helpers: blank line between metrics, values
rounded to 3 decimals. prom-client remains only for /metrics/runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR replaces direct Prometheus updates with runtime state counters, adds callback-based storage timing, updates indexing and source instrumentation, and renders synchronous Prometheus text from collected IndexerState data. Endpoint wiring, mocks, and metric tests are updated accordingly.

Changes

State-backed metric contracts

Layer / File(s) Summary
Runtime metric state and accessors
packages/envio/src/ChainState.res*, packages/envio/src/CrossChainState.res*, packages/envio/src/EffectState.res*, packages/envio/src/IndexerState.res*, packages/envio/src/FetchState.res, packages/envio/src/sources/SourceManager.res*
Runtime states gain metric counters, stat records, recording functions, and scrape-time accessors.
Persistence write callback contract
packages/envio/src/Persistence.res, packages/envio/src/PgStorage.res
Storage writes accept an onWrite callback and report PostgreSQL and sink write durations through it.

Runtime instrumentation migration

Layer / File(s) Summary
Indexing and chain instrumentation
packages/envio/src/ChainFetching.res, packages/envio/src/EventProcessing.res, packages/envio/src/sources/SourceManager.res
Fetch, reorg, handler, preload, batch, source-height, and source-status measurements are recorded in runtime state.
Effects, storage, rollback, and pruning
packages/envio/src/LoadLayer.res, packages/envio/src/Writing.res, packages/envio/src/Rollback.res, packages/envio/src/PruneStaleHistory.res
Effect, queue, cache, storage-load, storage-write, rollback, and history-prune measurements use state-backed counters.

Synchronous metric rendering

Layer / File(s) Summary
Prometheus exposition renderer
packages/envio/src/Metrics.res
Metrics formats labels and renders state-backed metric series synchronously.
Metrics endpoint and state views
packages/envio/src/Main.res, packages/envio/src/tui/Tui.res
The metrics endpoint returns synchronous output, while console and TUI views use the expanded chain snapshot.

Validation and compatibility

Layer / File(s) Summary
Mocks and rendering tests
scenarios/test_codegen/test/helpers/MockIndexer.res, scenarios/test_codegen/test/lib_tests/Metrics_test.res, scenarios/test_codegen/test/*
Mocks accept the updated storage callback, parse rendered metrics, and test formatting, escaping, optional series, and state-less collection.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runtime
  participant IndexerState
  participant Metrics
  participant MetricsEndpoint

  Runtime->>IndexerState: Record runtime measurements
  MetricsEndpoint->>IndexerState: toMetrics
  MetricsEndpoint->>Metrics: collect(metrics)
  Metrics->>Metrics: renderMetrics
  Metrics-->>MetricsEndpoint: Return Prometheus text
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing prom-client with custom Prometheus text rendering.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ae65dc5e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/Metrics.res Outdated
Comment on lines +79 to +82
->Array.map((s: IndexerState.handlerStat) => (
`{contract="${s.contract}",event="${s.event}"}`,
s,
))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape dynamically supplied Prometheus label values

When a configured contract/event/effect/source name contains ", \, or a newline, these interpolated labels produce invalid Prometheus exposition text (for example, a quote terminates contract early), causing Prometheus to reject the /metrics scrape. The previous prom-client label renderer escaped these values; route all dynamic label values through a Prometheus label-escaping helper before assembling the label string.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/Writing.res Outdated
Comment on lines +139 to +142
state->IndexerState.recordStorageWrite(
~storage=persistence.storage.name,
~timeSeconds=timerRef->Performance.secondsSince,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve separate sink storage-write samples

With a ClickHouse sink configured, this records one storage="postgres" sample for the whole composite PgStorage.writeBatch call, including any time spent awaiting the sink, and no longer emits the sink's own storage="clickhouse" count or duration. Previously PgStorage.writeBatchMethod timed the sink promise and Postgres write independently, so dashboards can no longer distinguish a slow/failing sink from Postgres (and a successful sink followed by a Postgres failure is now uncounted).

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/Metrics.res Outdated
Comment on lines +471 to +476
b->seriesOpt(
~name="envio_effect_cache",
~help="The number of items in the effect cache.",
~kind="gauge",
~entries=effects,
~value=s => s.cacheCount > 0 ? Some(s.cacheCount->Int.toFloat) : None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit zero-valued persistent effect-cache gauges

For an existing but empty persisted effect-cache table, makeFromDbState now seeds cacheCount with zero, but this predicate suppresses its sample. Before this change, the startup loop called EffectCacheCount.set for every discovered cache table, including zero-row tables, so dashboards could distinguish an empty persistent cache from an effect with no cache metric at all.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/envio/src/ChainState.res (1)

819-846: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align comments with the state-backed metric flow. The implementation now stores metric state for scrape-time rendering; these comments either describe the old direct-emission behavior or restate obvious structure.

  • packages/envio/src/ChainState.res#L819-L846: change “Emits the per-chain progress metrics” to describe updating metric state, or remove that clause.
  • packages/envio/src/ChainState.res#L32-L32: remove the downstream-renderer narration.
  • packages/envio/src/ChainState.res#L371-L371: remove the redundant section comment.

As per coding guidelines, **/*.res comments must not restate code, identify callers, or narrate a refactor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/ChainState.res` around lines 819 - 846, Update the
comments in packages/envio/src/ChainState.res at lines 819-846, 32, and 371:
revise the applyBatchProgress comment to say it updates metric state rather than
emits metrics, and remove the downstream-renderer narration and redundant
section comment at the two sibling sites; do not change the implementation.

Source: Coding guidelines

packages/envio/src/EventProcessing.res (1)

173-207: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Always finalize preload metrics when the handler fails.

A synchronous throw or rejected promise skips endPreloadHandler, permanently leaking preloadPendingCount. Subsequent preload wall-clock metrics for this handler will never close.

Proposed fix
             let timerRef =
               indexerState->IndexerState.startPreloadHandler(
                 ~contract=contractName,
                 ~event=eventName,
               )
+            let finish = () =>
+              indexerState->IndexerState.endPreloadHandler(
+                timerRef,
+                ~contract=contractName,
+                ~event=eventName,
+              )
             promises->Array.push(
               handler({
                 event: item->Ecosystem.getItemEvent(~ecosystem=config.ecosystem),
                 context: UserContext.getHandlerContext({
                   item,
                   indexerState,
                   loadManager,
                   persistence,
                   checkpointId,
                   isPreload: true,
                   chains,
                   isResolved: false,
                   config,
                 }),
               })
-              ->Promise.thenResolve(_ => {
-                indexerState->IndexerState.endPreloadHandler(
-                  timerRef,
-                  ~contract=contractName,
-                  ~event=eventName,
-                )
-              })
-              ->Utils.Promise.silentCatch,
+              ->Promise.thenResolve(_ => finish())
+              ->Promise.catch(_ => finish()),
             )
           } catch {
-          | _ => ()
+          | _ =>
+            indexerState->IndexerState.endPreloadHandler(
+              timerRef,
+              ~contract=contractName,
+              ~event=eventName,
+            )
           }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/EventProcessing.res` around lines 173 - 207, Update the
preload handler flow around startPreloadHandler and handler so endPreloadHandler
is invoked for both synchronous throws and rejected promises, not only
successful Promise.thenResolve completion. Ensure each started timerRef is
finalized exactly once while preserving the existing silent error handling.
🧹 Nitpick comments (1)
packages/envio/src/Metrics.res (1)

1-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the refactor narration.

This comment describes the module purpose and the removal of prom-client, rather than a non-obvious constraint. As per coding guidelines, “Never narrate the refactor itself.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/Metrics.res` around lines 1 - 3, Remove the top-of-file
comment in Metrics.res that narrates the refactor and mentions prom-client;
leave the implementation unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/envio/src/Metrics.res`:
- Around line 20-31: The metrics pipeline currently treats labels as opaque
strings, allowing delimiters and special characters to corrupt Prometheus output
and parsing. In packages/envio/src/Metrics.res:20-31, update series and related
rendering to accept structured labels and centrally escape quotes, backslashes,
and newlines; in scenarios/test_codegen/test/helpers/MockIndexer.res:625-655,
parse quoted and escaped label values instead of splitting directly on commas or
equals signs; in scenarios/test_codegen/test/lib_tests/Metrics_test.res:16-27,
add whole-output regression cases covering escaped labels and quoted delimiters.

In `@packages/envio/src/PgStorage.res`:
- Around line 1782-1788: Preserve the sink-specific timer metrics in the
writeBatch path instead of returning only the exception from the Promise chain.
Update the sink handling around sink.writeBatch and the corresponding
IndexerState accumulation to return or pass the sink duration/count recording,
while retaining the existing Some(exn) failure behavior and outer
persistence.storage.name metrics.

In `@packages/envio/src/sources/SourceManager.res`:
- Around line 126-128: Update the idleSeconds, waitingForNewBlockSeconds, and
queryingSeconds accessors to include the elapsed time from the current
statusStart via Performance.secondsSince, in addition to each stored counter.
Ensure scrape results reflect the active interval before the next status
transition.

In `@scenarios/test_codegen/test/lib_tests/Metrics_test.res`:
- Around line 16-27: Add whole-output assertions in the metrics rendering test
around the existing b.out expectation, covering label values containing quotes,
backslashes, newlines, commas, and equals signs. Verify the rendered output uses
the shared escaping and delimiter contract while preserving the existing metric
lines and values.

---

Outside diff comments:
In `@packages/envio/src/ChainState.res`:
- Around line 819-846: Update the comments in packages/envio/src/ChainState.res
at lines 819-846, 32, and 371: revise the applyBatchProgress comment to say it
updates metric state rather than emits metrics, and remove the
downstream-renderer narration and redundant section comment at the two sibling
sites; do not change the implementation.

In `@packages/envio/src/EventProcessing.res`:
- Around line 173-207: Update the preload handler flow around
startPreloadHandler and handler so endPreloadHandler is invoked for both
synchronous throws and rejected promises, not only successful
Promise.thenResolve completion. Ensure each started timerRef is finalized
exactly once while preserving the existing silent error handling.

---

Nitpick comments:
In `@packages/envio/src/Metrics.res`:
- Around line 1-3: Remove the top-of-file comment in Metrics.res that narrates
the refactor and mentions prom-client; leave the implementation unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 10c52092-334d-4fd0-855c-50354dd57d2d

📥 Commits

Reviewing files that changed from the base of the PR and between 61274cd and 3ae65dc.

📒 Files selected for processing (23)
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/ChainState.res
  • packages/envio/src/ChainState.resi
  • packages/envio/src/CrossChainState.res
  • packages/envio/src/CrossChainState.resi
  • packages/envio/src/EffectState.res
  • packages/envio/src/EffectState.resi
  • packages/envio/src/EventProcessing.res
  • packages/envio/src/FetchState.res
  • packages/envio/src/IndexerState.res
  • packages/envio/src/IndexerState.resi
  • packages/envio/src/LoadLayer.res
  • packages/envio/src/Main.res
  • packages/envio/src/Metrics.res
  • packages/envio/src/PgStorage.res
  • packages/envio/src/Prometheus.res
  • packages/envio/src/PruneStaleHistory.res
  • packages/envio/src/Rollback.res
  • packages/envio/src/Writing.res
  • packages/envio/src/sources/SourceManager.res
  • packages/envio/src/sources/SourceManager.resi
  • scenarios/test_codegen/test/helpers/MockIndexer.res
  • scenarios/test_codegen/test/lib_tests/Metrics_test.res
💤 Files with no reviewable changes (1)
  • packages/envio/src/Prometheus.res

Comment thread packages/envio/src/Metrics.res
Comment thread packages/envio/src/PgStorage.res Outdated
Comment thread packages/envio/src/sources/SourceManager.res Outdated
Comment thread scenarios/test_codegen/test/lib_tests/Metrics_test.res
…conds

Escape quotes/backslashes/newlines in dynamic Prometheus label values; restore
per-storage write samples by threading an onWrite callback through
storage.writeBatch so a configured sink is timed separately from postgres;
emit a zero envio_effect_cache sample for existing empty persisted caches;
include the in-progress status interval in idle/waiting/querying seconds at
scrape time; parse escaped label values in the MockIndexer metric helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
Merge the metrics snapshot into ChainState.chainData so the status API, TUI
and metrics renderer share one toData view. Cumulative metric counters
(block-range events/blocks/count, handler counts, storage load count/where/
size, effect call count) become floats since int32 is unsafe for them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31cfe5cb29

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/Metrics.res Outdated
Comment on lines +104 to +107
->Utils.Dict.forEach((s: EffectState.effectStats) =>
entries->Array.push((
`{effect="${s.effectName->escapeLabelValue}",scope="${s.scope->Internal.EffectCache.scopeToString}"}`,
s,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve effect-only label sets

For an effect configured with the same name on multiple chain scopes, this shared entry list now adds scope to envio_effect_cache_invalidations and envio_effect_queue_wait_seconds. Those metrics previously had only the effect label (and their values were aggregated across scopes), so existing PromQL/dashboard selectors such as {effect="foo"} no longer match and aggregate totals become separate series. Render scope-qualified entries only for the metric families that previously used that label.

Useful? React with 👍 / 👎.

Comment on lines +5 to +8
// Prometheus floats keep at most 3 decimals; integral values render without a
// fractional part.
@inline
let formatValue = (value: float) => (Math.round(value *. 1000.) /. 1000.)->Float.toString

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not round Prometheus sample values

Prometheus accepts full-precision floating-point samples, but this rounds every metric—not just the previously hand-rendered source-request duration—to milliseconds. Consequently, fast handler/load/effect operations under 0.0005 seconds are emitted as 0, and longer-running counters lose accumulated fractional time on every scrape; this makes short-window rates and latency-derived dashboards materially inaccurate compared with the prior prom-client output. Render the original float value instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 3-decimal rounding is intentional for this PR — it's an explicit requirement of the change to keep the /metrics payload compact. Sub-millisecond samples rounding to 0 is an accepted trade-off; the affected counters accumulate at full precision in state and only the rendered text is rounded, so no accumulated time is lost between scrapes.


Generated by Claude Code

envio_effect_cache_invalidations and envio_effect_queue_wait_seconds keep
their effect-only label set, aggregated across scopes, matching the previous
prom-client series identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/envio/src/EffectState.resi (1)

15-20: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep invalidationsCount as a float counter.

queueCount is a gauge, so int is fine there. invalidationsCount is the monotonic metric that can still overflow over long runs; widen that field and its increment sites to float.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/EffectState.resi` around lines 15 - 20, Change
EffectState’s invalidationsCount field from int to float, and update every
increment or mutation site for invalidationsCount to use float arithmetic. Leave
queueCount as int and preserve the existing counter behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/envio/src/EffectState.resi`:
- Around line 15-20: Change EffectState’s invalidationsCount field from int to
float, and update every increment or mutation site for invalidationsCount to use
float arithmetic. Leave queueCount as int and preserve the existing counter
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60d3289f-5b00-4e75-b668-dcf09428a86b

📥 Commits

Reviewing files that changed from the base of the PR and between f73cda5 and 31cfe5c.

📒 Files selected for processing (10)
  • packages/envio/src/ChainState.res
  • packages/envio/src/ChainState.resi
  • packages/envio/src/EffectState.res
  • packages/envio/src/EffectState.resi
  • packages/envio/src/IndexerState.res
  • packages/envio/src/IndexerState.resi
  • packages/envio/src/LoadLayer.res
  • packages/envio/src/Main.res
  • packages/envio/src/Metrics.res
  • packages/envio/src/tui/Tui.res
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/envio/src/IndexerState.resi
  • packages/envio/src/LoadLayer.res
  • packages/envio/src/EffectState.res
  • packages/envio/src/Metrics.res
  • packages/envio/src/IndexerState.res
  • packages/envio/src/ChainState.res

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd4df38360

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/Metrics.res Outdated
->SourceManager.getSourceHeightSamples
->Array.forEach((s: SourceManager.sourceHeightSample) =>
entries->Array.push((
`{source="${s.sourceName->escapeLabelValue}",chainId="${s.chainId->Int.toString}"}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate source-height label sets before rendering

When a chain config contains two RPC sources with the same host (for example, primary and fallback URLs that differ only by path or credentials), both are named RPC (<host>). Once both have observed a height, this loop emits two envio_source_known_height samples with identical {source,chainId} labels, which causes Prometheus to reject the scrape as duplicate samples. The previous prom-client gauge coalesced these updates into one series; aggregate or select one sample per label set before rendering.

Useful? React with 👍 / 👎.

Two sources can share a name (e.g. primary and fallback RPC urls on the same
host); aggregate request counts/seconds and take the max height per label set
so the scrape never contains duplicate samples.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2a568ee25

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/IndexerState.res Outdated
}

let getHandlerStat = (state: t, ~contract, ~event) => {
let key = contract ++ ":" ++ event

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use an unambiguous handler-stat key

When configured contract/event names contain :, distinct handler pairs such as (A:B, C) and (A, B:C) both map to the same key. The new stateful metric collector then combines their processing/preload counters and renders all of them with the labels from whichever pair was first observed, whereas the prior prom-client handler metrics kept the two label dimensions distinct. Contract and event names come from user configuration, so use a collision-free composite key (or nested dictionaries).

Useful? React with 👍 / 👎.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
Metrics.res now defines the metrics snapshot types (chain/handler/effect/
storage/source records plus Metrics.t) and renders prometheus text from the
snapshot instead of reaching into indexer state. IndexerState.toMetrics is
the single read-only window into the mutable counters: the stat records are
hidden behind the IndexerState and EffectState interfaces (effect stat
mutations go through startCall/endCall/queue*/recordInvalidation/
commitCacheCount), and the TUI, MockIndexer and the /metrics endpoint all
consume Metrics.t. The console/state HTTP response keeps exactly the previous
chainData field set for RACE compatibility, mapped from the snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/envio/src/Metrics.res (1)

1-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove comments that narrate the module and helper implementations.

These comments restate structure or behavior already clear from the types and function names; retain only comments documenting non-obvious constraints.

As per coding guidelines, “Don't write a comment that restates what the code already says” and “Never narrate the refactor itself.”

Also applies to: 157-158, 174-175, 194-194

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/envio/src/Metrics.res` around lines 1 - 5, Remove the introductory
module comment in Metrics.res and the comments at the referenced helper
locations that merely describe the module structure or implementation behavior.
Retain only comments conveying non-obvious constraints, without adding
replacement narration.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/envio/src/ChainState.res`:
- Around line 735-737: Remove the caller/purpose narration above
ChainState.toMetrics in packages/envio/src/ChainState.res (lines 735-737). At
packages/envio/src/ChainState.res lines 810-814, replace the stale emission
claim with only the readiness/timestamp invariant. In
packages/envio/src/EffectState.resi lines 36-49, remove the generic “Metric
mutations” and “Metric reads” headings while preserving the rollback and restart
invariants.

---

Nitpick comments:
In `@packages/envio/src/Metrics.res`:
- Around line 1-5: Remove the introductory module comment in Metrics.res and the
comments at the referenced helper locations that merely describe the module
structure or implementation behavior. Retain only comments conveying non-obvious
constraints, without adding replacement narration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 102e26df-f1b4-4118-a90d-e66e0480458e

📥 Commits

Reviewing files that changed from the base of the PR and between e806af2 and ca7bd11.

📒 Files selected for processing (13)
  • packages/envio/src/ChainState.res
  • packages/envio/src/ChainState.resi
  • packages/envio/src/EffectState.res
  • packages/envio/src/EffectState.resi
  • packages/envio/src/IndexerState.res
  • packages/envio/src/IndexerState.resi
  • packages/envio/src/LoadLayer.res
  • packages/envio/src/Main.res
  • packages/envio/src/Metrics.res
  • packages/envio/src/Writing.res
  • packages/envio/src/tui/Tui.res
  • scenarios/test_codegen/test/helpers/MockIndexer.res
  • scenarios/test_codegen/test/lib_tests/Metrics_test.res
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/envio/src/tui/Tui.res
  • packages/envio/src/Writing.res
  • scenarios/test_codegen/test/lib_tests/Metrics_test.res
  • packages/envio/src/EffectState.res
  • scenarios/test_codegen/test/helpers/MockIndexer.res
  • packages/envio/src/IndexerState.res
  • packages/envio/src/LoadLayer.res

Comment thread packages/envio/src/ChainState.res Outdated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd65c4539d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +704 to +708
| 1 => "minor"
| 2 => "major"
| 4 => "incremental"
| 8 => "weakcb"
| _ => "unknown"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the Node GC kind constants

When /metrics/runtime observes any non-minor GC on the supported Node 22+ runtime, perf_hooks reports kind values as 1=minor, 4=major, 8=incremental, and 16=weakcb. This mapping instead exports major pauses as kind="incremental", incremental pauses as kind="weakcb", and weak-callback pauses as unknown, so GC dashboards or alerts broken down by kind become misleading. Fresh evidence in this version is the new gcKindName renderer that now emits GC metrics but uses the wrong constants.

Useful? React with 👍 / 👎.

Node perf_hooks reports GC kinds as MINOR=1, MAJOR=4, INCREMENTAL=8,
WEAKCB=16. The previous mapping mislabeled major pauses as incremental,
incremental as weakcb, and dropped weakcb to unknown, skewing GC
dashboards broken down by kind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
…abel

- Escape the effect scope label value like every other label so an
  unusual scope can't break the scrape.
- Length-prefix the storage-load stat key so a storage name containing
  the separator can't collide, matching the handler-stat key.
- Compute the active-resource label string once per resource.

Adds a collect test asserting both effect and scope labels are escaped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
Cover comma and equals in label values (passed through unescaped)
alongside the quote/backslash/newline escaping, in both the
escapeLabelValue unit test and the whole-output collect render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
// Highest height observed for this source, rendered into
// envio_source_known_height. Kept apart from knownHeight, which drives the
// height-subscription wait logic.
mutable reportedHeight: int,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't knownHeight is exactly what we want to report?

~blockNumber=newHeight.contents,
)
if newHeight.contents > sourceState.reportedHeight {
sourceState.reportedHeight = newHeight.contents

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to report anything. We can simply read a known hate directly from source state.

Drop the separate reportedHeight field and the reportActiveSourceHeight
path: write the observed height back into sourceState.knownHeight (which
already exists for the height-wait logic) and read it for the
envio_source_known_height metric.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8a9672a5d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +913 to +918
~name="nodejs_gc_duration_seconds_sum",
~help="Cumulative garbage collection pause time by kind, one of major, minor, incremental or weakcb.",
~kind="counter",
~entries=gcEntries,
~value=s => s.seconds,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve GC duration histogram buckets

When users rely on prom-client's default GC histogram (for example histogram_quantile(..., rate(nodejs_gc_duration_seconds_bucket[5m]))), this replacement emits only _sum and _count counter families and never emits nodejs_gc_duration_seconds_bucket samples under the base histogram metric. Fresh evidence in this version is that the current renderer adds GC sum/count here but still omits the bucket series, so existing GC latency quantile dashboards disappear even though GC metrics appear to be present.

Useful? React with 👍 / 👎.

Comment on lines +131 to 134
chainState->ChainState.recordBlockRangeFetch(
~totalTimeElapsed=stats.totalTimeElapsed,
~parsingTimeElapsed=stats.parsingTimeElapsed->Option.getOr(0.),
~numEvents=parsedQueueItems->Array.length,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update per-source height from block-range responses

When a block-range response advances knownHeight during a long backfill, we now only record fetch stats here; envio_source_known_height is rendered from SourceManager's per-source knownHeight, which is updated by waitForNewBlock rather than by these query responses. The previous code updated the source-height gauge from this response path, so the per-source height can remain stale until the chain catches up and waits for another block.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/Metrics.res Outdated
~name="nodejs_version_info",
~help="Node.js version info.",
~kind="gauge",
~entries=[(`{version="${NodeJs.Process.version}"}`, ())],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Node version labels

When dashboards or alerts filter nodejs_version_info by major, minor, or patch, this replacement only emits the version label. prom-client's default runtime collector exposed all four labels, so existing selectors like {major="22"} stop matching even though the metric name is still present; split NodeJs.Process.version and render the legacy labels too.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/Metrics.res Outdated
~name="process_start_time_seconds",
~help="Start time of the process since unix epoch in seconds.",
~kind="gauge",
~value=Date.now() /. 1000. -. NodeJs.Process.uptime(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep process start time stable

When /metrics/runtime is scraped over a wall-clock adjustment or enough timing jitter crosses the 3-decimal rounding boundary, recomputing process_start_time_seconds from Date.now() and process.uptime() on every scrape can make this standard start-time gauge change without a restart. Dashboards and alerts that use changes in process_start_time_seconds to detect restarts can then false-positive; compute this once when the runtime collectors start and reuse it.

Useful? React with 👍 / 👎.

- Compute process_start_time_seconds once when the runtime collectors
  start and reuse it, so clock jitter across the rounding boundary can't
  make it read as a phantom restart.
- Emit major/minor/patch labels on nodejs_version_info alongside version,
  matching prom-client's default collector so existing label selectors
  keep matching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
During a long backfill the wait loop doesn't run, so the per-source
known height (rendered into envio_source_known_height) would stay stale.
Advance sourceState.knownHeight from block-range response heights too,
keeping the single-field design while restoring backfill freshness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
Move the per-source height update from ChainFetching into executeQuery,
where the exact source the response came from is in scope. Drops the
"assume it's the active source" approximation and removes the
reportActiveSourceHeight indirection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
rateLimits: dict<effectRateLimitState>,
// Metric counters keyed by the same name. Survive rollback: prometheus
// counters must stay monotonic.
stats: dict<effectStats>,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it different from the stats we already have on effect cache and memory tables?

Comment on lines +531 to +532
res->endWithData(
Metrics.collect(~metrics=getIndexerState()->Option.map(IndexerState.toMetrics)),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning None looks like an issue. Let's return an empty string, or a fallback - give me suggestions

it("Hand-rolls prometheus gauge text with one sample per chain", t => {
let rendered = Metrics.renderGauge(
describe("Metrics rendering helpers", () => {
it("Renders metrics separated by a blank line, keeping 3 decimals after the point", t => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have a test with all metric values present

Assert the complete /metrics output for a Metrics.t with every field
and array populated, covering all metric families and the 3-decimal
rounding end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfXVEyPmzDojxvrbfv3Ups
@DZakh
DZakh merged commit d61cc48 into main Jul 21, 2026
8 checks passed
@DZakh
DZakh deleted the claude/prom-metrics-indexer-state-0ezqlb branch July 21, 2026 09:08
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.

2 participants