Skip to content

telemetry: materialize dropped-event counter children at 0 - #927

Open
philosaether wants to merge 4 commits into
mainfrom
phil/telemetry-dropped-zero-init
Open

telemetry: materialize dropped-event counter children at 0#927
philosaether wants to merge 4 commits into
mainfrom
phil/telemetry-dropped-zero-init

Conversation

@philosaether

@philosaether philosaether commented Jul 22, 2026

Copy link
Copy Markdown

What

Pre-create the telemetry_events_dropped counter's (namespace, reason) child series at 0 when the telemetry emitter starts, for each reason the emitter can emit ({prefix}buffer_full, {prefix}send_failed).

Why

A labeled Prometheus counter exports no time series until its first labels(...) call. So telemetry_events_dropped was invisible in Prometheus/Grafana until an event was actually dropped — which means:

  • You can't alert on or graph a metric that doesn't exist yet.
  • "No drops" was indistinguishable from "metric missing / scrape broken".

Materializing the children at 0 on startup keeps the metric always present, so a missing series now signals a real problem rather than a healthy system.

Changes

  • src/telemetry/prometheus/metrics.py: add initialize_telemetry_dropped_metrics(reasons=...), which pre-touches each (namespace, reason) child (namespace auto-injected by NamespacedCounter, matching existing record_telemetry_event_dropped usage).
  • src/telemetry/emitter.py: call it on emitter start with the emitter's own prefixed reasons.

Testing

Verified in a sandbox: the counter's zero-valued children appear on the metrics endpoint before any drop occurs, and increment correctly when drops happen.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved Prometheus telemetry startup so dropped-event metric series are pre-materialized (buffer-full and send-failure reasons) and remain visible before any drops occur.
    • Made metric series materialization fail-soft to reduce startup issues when metrics initialization encounters errors.
  • New Features
    • Added a Prometheus gauge tracking the reconciliation “pending embeddings” backlog, updated after reconciliation sweeps.
    • Enhanced bounded metric initialization at startup for earlier /metrics visibility in both API and deriver.
  • Tests
    • Added a comprehensive suite validating metric zero-initialization, allowed label sets, and drift-guard behavior for event types and series.

A labeled Prometheus counter exports no series until its first labels()
call, so telemetry_events_dropped stayed invisible until an event was
actually dropped — impossible to alert on or graph, and "no drops" was
indistinguishable from "metric missing / scrape broken".

Pre-create the (namespace, reason) children at 0 on emitter start, for
each reason the emitter can emit, so the metric is always present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f430f8dd-00d2-4729-98fd-3b36636e66aa

📥 Commits

Reviewing files that changed from the base of the PR and between 313fd41 and 4b40382.

📒 Files selected for processing (1)
  • tests/telemetry/test_metric_zero_init.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/telemetry/test_metric_zero_init.py

Walkthrough

Prometheus metrics now pre-materialize bounded label series during API, deriver, and emitter startup. Reconciliation reports pending message embeddings through a gauge, with tests covering valid domains, process isolation, dropped counters, and disabled metrics.

Changes

Telemetry metrics initialization

Layer / File(s) Summary
Metric domains and zero-initialization
src/telemetry/events/__init__.py, src/telemetry/prometheus/metrics.py
Defines bounded event and deriver label domains, adds fail-soft zero-initialization helpers, and introduces the pending embeddings gauge.
Application and emitter startup initialization
src/main.py, src/deriver/__main__.py, src/telemetry/emitter.py
Initializes bounded metrics for API and deriver processes and pre-creates buffer-full and send-failure drop labels.
Pending embeddings backlog reporting
src/reconciler/sync_vectors.py, src/telemetry/prometheus/metrics.py
Counts pending embeddings after reconciliation and updates the Prometheus gauge without failing reconciliation on errors.
Metric initialization validation
tests/telemetry/test_metric_zero_init.py
Tests bounded-label completeness, process isolation, dropped-counter backfilling, and disabled-metrics behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant APIStartup
  participant DeriverStartup
  participant PrometheusMetrics
  participant Reconciler
  participant Database

  APIStartup->>PrometheusMetrics: initialize API bounded metrics
  DeriverStartup->>PrometheusMetrics: initialize deriver bounded metrics
  PrometheusMetrics-->>APIStartup: materialized zero-valued series
  PrometheusMetrics-->>DeriverStartup: materialized zero-valued series
  Reconciler->>Database: count pending MessageEmbedding rows
  Database-->>Reconciler: pending count
  Reconciler->>PrometheusMetrics: set pending embeddings gauge
Loading

Poem

A rabbit counts embeddings in a telemetry burrow,
Planting metric labels before events can scurry.
Zeroes bloom gently, and failed drops stay tame,
While pending rows hop into a gauge by name.
Hop, hop—the metrics are ready!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: pre-materializing dropped-event counter children at zero.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phil/telemetry-dropped-zero-init

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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

🤖 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 `@src/telemetry/prometheus/metrics.py`:
- Around line 341-347: Update initialize_telemetry_dropped_metrics to catch only
the expected Prometheus labels(reason=reason) validation exception, using the
appropriate specific Prometheus exception type. Let unexpected exceptions from
telemetry_events_dropped_counter.labels propagate instead of routing them
through _handle_metric_error, preserving the existing handling for valid label
errors.
🪄 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: f114054c-c059-4b97-816c-ba9cad4842e6

📥 Commits

Reviewing files that changed from the base of the PR and between 4f9a413 and 56ab5dc.

📒 Files selected for processing (2)
  • src/telemetry/emitter.py
  • src/telemetry/prometheus/metrics.py

Comment thread src/telemetry/prometheus/metrics.py Outdated
Comment thread src/telemetry/prometheus/metrics.py

@Rajat-Ahuja1997 Rajat-Ahuja1997 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

looks good, 1 comment

Extends #927 (which zero-inited telemetry_events_dropped) to every counter
whose label domain is bounded and known at startup, so metrics are present in
Prometheus before their first event — a missing series then signals a broken
scrape rather than "nothing happened yet".

- add initialize_bounded_metrics(instance_type) on PrometheusMetrics; call it
  per-process from main.py (api) and deriver/__main__.py (deriver).
- extract a shared _touch() helper; refactor initialize_telemetry_dropped_metrics
  onto it (that one stays per-emitter in start() — it's prefix-dependent).
- explicit ALL_EVENT_TYPES / HIGH_VOLUME_EVENT_TYPES registry in telemetry.events,
  drift-guarded by tests that walk BaseEvent subclasses.
- only VALID (task_type, token_type, component) tuples for deriver_tokens (the
  cartesian product would fabricate impossible always-0 series); only high-volume
  event types for sampled_out; high-cardinality labels (endpoint, workspace_name)
  left open.
- gauges: zero-init embed_now_tasks_in_flight + telemetry_buffer_size; add a new
  message_embeddings_pending backlog gauge, set each reconciliation cycle and
  zero-inited at deriver startup (Rajat's pending/in-flight ask).
- backfills the tests #927 shipped without.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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

🤖 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 `@src/telemetry/prometheus/metrics.py`:
- Around line 403-458: Replace the direct gauge initialization calls in
initialize_bounded_metrics with the class’s fail-soft _touch path, including
telemetry_buffer_size_gauge, embed_now_tasks_in_flight_gauge, and
message_embeddings_pending_gauge. Preserve initializing each gauge to zero while
ensuring any metric setup failure is handled consistently without interrupting
startup.

In `@tests/telemetry/test_metric_zero_init.py`:
- Around line 227-241: The test test_api_init_does_not_touch_deriver_counters
currently covers only API-to-deriver isolation. Add a before/after assertion
around prometheus_metrics.initialize_bounded_metrics(instance_type="deriver")
using an API-only bounded metric and its appropriate labels, asserting the
samples are unchanged so deriver initialization cannot materialize API metrics.
- Around line 95-102: Update the validation around _DERIVER_TOKEN_COMBOS to
compare the complete set of token/component pairs against the exact expected
pair set, rather than checking token and component membership independently.
Also assert the combo collection length equals the expected pair-set length so
duplicate entries are rejected, while preserving the existing coverage checks as
appropriate.
- Around line 31-43: Use unique namespaces in test_metric_zero_init.py for all
global REGISTRY assertions: replace the fixed NS value used by sample() with a
module/test-specific namespace, and introduce a separate unique namespace for
the disabled-metrics check at lines 258-269. Update the relevant metric
configuration and assertions to use these namespaces so materialized children
from other tests cannot affect results.
🪄 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 Plus

Run ID: 605569e1-376d-4abd-a7a8-df494bba2741

📥 Commits

Reviewing files that changed from the base of the PR and between 56ab5dc and e472f8d.

📒 Files selected for processing (6)
  • src/deriver/__main__.py
  • src/main.py
  • src/reconciler/sync_vectors.py
  • src/telemetry/events/__init__.py
  • src/telemetry/prometheus/metrics.py
  • tests/telemetry/test_metric_zero_init.py

Comment thread src/telemetry/prometheus/metrics.py Outdated
Comment thread tests/telemetry/test_metric_zero_init.py Outdated
Comment thread tests/telemetry/test_metric_zero_init.py Outdated
Comment thread tests/telemetry/test_metric_zero_init.py
I1: _DERIVER_TOKEN_COMBOS was factored task-independently, materializing the
impossible (ingestion, input, previous_summary) series — previous_summary is
summary-only. Make combos task-aware (_DERIVER_TOKEN_COMBOS_BY_TASK) so no
always-0 impossible series is fabricated, matching the PR's own goal. Tests
tightened to assert the ingestion/previous_summary series is absent.

I2: the three gauge .set(0) zero-inits were bare while the counter inits go
through the fail-soft _touch. Add _set_gauge_zero() so a gauge init can't
propagate an exception into process startup either.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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.

🧹 Nitpick comments (1)
src/telemetry/prometheus/metrics.py (1)

379-381: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a Google-style docstring.

Document the gauge parameter in an Args: section.

Proposed fix
     def _set_gauge_zero(self, gauge: NamespacedGauge) -> None:
-        """Materialize a (namespace-only) gauge at 0. Fail-soft, like ``_touch``:
-        a startup init must never propagate an exception into process boot."""
+        """Materialize a namespace-only gauge at zero without failing startup.
+
+        Args:
+            gauge: The namespace-aware gauge to materialize.
+        """
🤖 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 `@src/telemetry/prometheus/metrics.py` around lines 379 - 381, Update the
_set_gauge_zero docstring to Google style by adding an Args: section that
documents the gauge parameter and its NamespacedGauge type, while preserving the
existing behavior description.

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.

Nitpick comments:
In `@src/telemetry/prometheus/metrics.py`:
- Around line 379-381: Update the _set_gauge_zero docstring to Google style by
adding an Args: section that documents the gauge parameter and its
NamespacedGauge type, while preserving the existing behavior description.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70ebefb0-e803-4f21-a26c-4130f2563d10

📥 Commits

Reviewing files that changed from the base of the PR and between e472f8d and 313fd41.

📒 Files selected for processing (2)
  • src/telemetry/prometheus/metrics.py
  • tests/telemetry/test_metric_zero_init.py

@karaokedjodua

Copy link
Copy Markdown

Finn-loop review of 313fd41

CI: not configured (gh pr checks --required returns "no required checks reported on the 'phil/telemetry-dropped-zero-init' branch")
Mergeability: BLOCKED — mergeStateStatus=BLOCKED indicates branch protection blocks merge. Despite all individual checks passing (basedpyright, test-python, CodeQL, CodeRabbit), GitHub's merge queue reports BLOCKED, meaning required checks exist in branch-protection rules but don't match the ones running. This is a CI configuration gap, not a code defect.

Review

Summary: PR materializes Prometheus counter children at 0 on startup so metrics like telemetry_events_dropped are visible in Grafana before any event fires — "no drops" vs "metric missing" becomes distinguishable. The code also pre-initializes the full bounded-label domain (event types, dialectic/deriver/dreamer tokens) and adds a new message_embeddings_pending gauge tracking the embedding backlog.

1. Must fix before merge

  • [CI] mergeStateStatus is BLOCKED. All individual checks pass, but branch protection rules don't see them as satisfying the required-checks gate. A repo admin needs to either add the correct checks to the branch-protection rule or remove the stale requirement. This is not a code defect — it's a repo-configuration gap that prevents ANY merge, regardless of code quality.
  • [CI] / [DEFECT] No required CI checks are registered on this branch. gh pr checks 927 --required exits 1 with "no required checks reported". While individual checks do run (basedpyright, test-python), they aren't gating. A human should decide whether these should become required before this (or any future) PR can merge.

2. Should fix soon

  • [AC-N] Scope: The PR body describes pre-creating telemetry_events_dropped children at 0, but the actual diff does significantly more:

    • ALL_EVENT_TYPES + HIGH_VOLUME_EVENT_TYPES registries (src/telemetry/events/init.py) — new infrastructure for enumerating event types
    • Full bounded-metrics init (initialize_bounded_metrics) covering dialectic tokens, deriver tokens, dreamer tokens, embed-now gauges, and buffer gauges — way beyond the stated dropped-metrics scope
    • message_embeddings_pending gauge + _record_pending_embeddings_backlog() DB query in src/reconciler/sync_vectors.py — an entirely separate feature (embedding-backlog tracking) bolted onto the same PR

    The code is well-written and tested, but these three additions each deserve their own PR with their own contract/testing story. A human should decide whether to accept the scope-creep as-is or request splitting.

3. Safe to merge

No — merge is mechanically blocked by BLOCKED status, and required CI is not configured. Code quality is high: fail-soft on init (never crashes startup), drift-guard tests prevent stale registries, cartesian-product explosion is avoided by explicit valid-combo tables, and the 294-line test suite is thorough.

Label notes

This reviewer's account does not have admin rights to plastic-labs/honcho, so labels cannot be applied via API. A repo admin should apply:

  • loop-changes-requested — CI gate is missing + scope concerns
  • needs-human-review — BLOCKED merge state needs admin attention

Global-REGISTRY assertions used a fixed "test" namespace, which several
other suites also pin, so another test's materialized children could
satisfy a presence assertion or break an absence one. Each test now runs
under a unique namespace resolved from settings at read time.

Adds the inverse per-process isolation test: deriver-only init must not
materialize API-only series (dialectic tokens, embed_now).

Co-Authored-By: Claude <noreply@anthropic.com>
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.

3 participants