Skip to content

OTLP browser telemetry export: follow-up improvements (KERNEL-1596)#317

Merged
archandatta merged 13 commits into
mainfrom
archand/kernel-1596/otlp-export-followups
Jul 22, 2026
Merged

OTLP browser telemetry export: follow-up improvements (KERNEL-1596)#317
archandatta merged 13 commits into
mainfrom
archand/kernel-1596/otlp-export-followups

Conversation

@archandatta

@archandatta archandatta commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

tldr

Follow-up improvements to the in-VM OTLP browser-telemetry exporter (KERNEL-1596), post #308. Independent, reviewable commits. Off-by-default export is preserved; nothing changes for existing sessions unless the new toggle/knobs are set.

What changed

  1. Externalize batch knobsBTEL_OTLP_MAX_QUEUE_SIZE (2048), BTEL_OTLP_EXPORT_INTERVAL (1s), BTEL_OTLP_EXPORT_TIMEOUT (30s). Defaults match the OTel SDK's, so behavior is unchanged unless set; validated as > 0.
  2. Per-session export toggle — new export.otlp.enabled on the telemetry API. Export is off by default and gated on the toggle, independent of whether an export destination is configured, so an always-injected relay does not generate work that gets dropped. The writer moves under a runtime-toggleable controller; toggling is best-effort (never fails a telemetry apply).
  3. Drop / failure / exported metrics — queue-overflow drops (previously log-only) plus export failures and exported counts as Prometheus counters (kernel_otlp_records_dropped_total, kernel_otlp_export_failures_total, kernel_otlp_records_exported_total), owned above the writer so they stay monotonic across export toggles.
  4. Crash/OOM severityservice_crashed and system_oom_kill now map to ERROR in the OTLP converter (were INFO), so consumers can alert on them.
  5. Renderer-crash event — detect Inspector.targetCrashed (an "Aw, Snap!" renderer crash that leaves the browser process alive, so no service_crashed fires) and emit a page_crashed event, mapped to ERROR. Categorized as page (produced by the CDP monitor, captured only while the page category is enabled).

Live test results

Verified full-stack against the headless image + a real dockerized OpenTelemetry collector, driving the real telemetry API and a real Chromium over CDP:

  • Batch knobs: custom BTEL_OTLP_* values surfaced in the boot config (otlp_max_queue_size=512 otlp_export_interval=500ms otlp_export_timeout=30s); export shows available, not started, at boot.
  • Export toggle: GET /telemetry → 404 at boot; PUT with export.otlp.enabled:true → 201 echoing it. With export off but page/network capture on, exported_total froze (10 → 10) and the collector received nothing new; re-enabling resumed export (10 → 28), confirming the controller rebuilds the one-shot writer.
  • Renderer crash: a real chrome://crash produced a page_crashed record at the collector with category=page, url=chrome://crash/, and SeverityText: ERROR.
  • Converter fidelity: network_response carried promoted attributes (http.request.method, url.full, http.response.status_code=200); screenshot/monitor categories never reached the collector.
  • Metrics: /metrics served kernel_otlp_records_dropped_total 0, kernel_otlp_export_failures_total 0, kernel_otlp_records_exported_total 28.

🤖 Generated with Claude Code


Note

Medium Risk
Changes the OTLP lifecycle (off-by-default, runtime toggle, tail-only replay) and telemetry API semantics, but export remains best-effort and existing VMs are unchanged unless the new toggle is enabled.

Overview
OTLP export no longer starts at boot when an endpoint is provisioned. Export is gated on export.otlp.enabled in the telemetry API (off by default). A runtime OTLPExportController starts/stops the sink; reconcileExport runs after PUT/PATCH under a separate exportMu so toggle-off drains do not block GET /telemetry. Re-enabled writers read from the stream tail so the ring is not replayed.

Config and observability: BTEL_OTLP_MAX_QUEUE_SIZE, BTEL_OTLP_EXPORT_INTERVAL, and BTEL_OTLP_EXPORT_TIMEOUT tune the OTel batch processor (validated at load). OTLPMetrics plus a drop-counting slog handler feed Prometheus counters for dropped, failed, and exported records.

Events: CDP Inspector.targetCrashed emits page_crashed (page category). OTLP severity maps service_crashed, system_oom_kill, and page_crashed to ERROR.

Reviewed by Cursor Bugbot for commit a31b83b. Bugbot is set up for automated code reviews on this repo. Configure here.

@archandatta
archandatta force-pushed the archand/kernel-1596/otlp-export-followups branch from 643f503 to 5e56d6a Compare July 17, 2026 13:11
@archandatta
archandatta requested a review from Sayan- July 21, 2026 18:24
@archandatta
archandatta marked this pull request as ready for review July 21, 2026 18:24
archandatta and others added 10 commits July 21, 2026 18:25
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Export is off by default and gated on the toggle, independent of whether
an endpoint is provisioned, so an always-injected relay does not export
by default. Writer lifecycle moves under a runtime-toggleable controller.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces queue-overflow drops (previously log-only) plus export failures and
exported counts as Prometheus counters, owned above the writer so they stay
monotonic across export toggles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Detects Inspector.targetCrashed (an 'Aw, Snap!' renderer crash that leaves the
browser process alive, so no service_crashed fires) and emits a page_crashed
event, mapped to ERROR severity in the OTLP converter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Exercises the export path end-to-end against a real dockerized collector:
severity mapping, category exclusion, promoted attributes, export counters,
and a forced queue overflow proving the drop metric. Run with -tags livetest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, crash guard

- Drop 'on the VM' from the public export spec descriptions.
- Thread request ctx into export reconcile; log via the context logger, and
  drain with context.WithoutCancel so a toggle-off outlives the request.
- Still emit page_crashed (and warn) if a crash fires on an untracked session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… test

Forces a real batch-queue overflow against a local hung listener so CI
exercises the SDK->global-logger->counter chain every run; an SDK message
rename now fails here instead of silently zeroing the metric. Removes the
build-tagged docker live test (its coverage is now unit-side).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… dash

- page_crashed on an untracked session now falls back to target_type
  "other" instead of an empty string, so the emitted event stays a valid
  BrowserTargetType. Adds a regression test for the untracked path.
- Restore the PATCH /telemetry no-op short-circuit when the body carries
  neither a category block nor an export toggle.
- Drop an em dash from a merge comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@archandatta
archandatta force-pushed the archand/kernel-1596/otlp-export-followups branch from 3286139 to 666c148 Compare July 21, 2026 18:26
Comment thread server/cmd/api/main.go
Comment thread server/cmd/api/main.go
Comment thread server/lib/events/otlpstorage.go
…eout

- Stop OTLP export after the HTTP servers drain (in main, mirroring
  s2Writer) instead of concurrently inside apiService.Shutdown, so
  events emitted during the shutdown window are still exported.
- OTLPStorageWriter.Stop now shuts the SDK provider down even when the
  read-loop wait exhausts ctx, using a short detached close budget, so a
  slow flush can't orphan the batch-export goroutines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread server/lib/events/otlpstorage.go
The runtime export toggle rebuilds the OTLP writer on each enable, and
the writer read from seq 0, so a disable/re-enable re-exported the whole
retained ring (up to the buffer capacity), duplicating records at the
collector and inflating kernel_otlp_records_exported_total.

Start the OTLP writer's reader from the current stream tail instead, so
only events published while export is on are forwarded. Adds
NewStorageWriterAfter (S2 keeps starting from 0) and a regression test
that toggles off/on and asserts no replay and no off-window export.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread server/lib/events/otlpstorage.go
})
}
return h.Handler.Handle(ctx, r)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Drop counter wrapper loses attrs

Medium Severity

dropCountingHandler embeds slog.Handler and only overrides Handle, so promoted WithAttrs / WithGroup return the unwrapped base handler. Any logr/OTel path that names or attaches values before logging dropped log records bypasses the counter, leaving kernel_otlp_records_dropped_total stuck at 0 despite real queue overflow.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit be7dc1d. Configure here.

TargetId: info.targetID,
TargetType: targetType,
Url: info.url,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale URL on page crash

Medium Severity

page_crashed fills url from m.sessions attach-time target info, which is never updated on navigation. Other page events use the computed nav context’s current URL. After the first navigation, crash events report the wrong page URL even though the OpenAPI text describes the URL at crash time.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit be7dc1d. Configure here.

Comment thread server/cmd/api/api/telemetry.go

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

Approve — reviewed all 11 commits (correctness + adversarial), ran the affected Go test packages, and rebuilt the headless image to re-run the live test plan end-to-end against a real dockerized OTel collector. Everything checks out.

Commit-by-commit

  • Externalize batch knobsBTEL_OTLP_MAX_QUEUE_SIZE/EXPORT_INTERVAL/EXPORT_TIMEOUT, validated > 0, defaults match the OTel SDK so behavior is unchanged unless set. Applied only when > 0 in newOTLPStorage. Good.
  • Per-session export toggle — export is off by default and gated on export.otlp.enabled, independent of whether an endpoint is provisioned; writer lifecycle moves under OTLPExportController (one-shot writer rebuilt per enable). Rollback-on-failure, PATCH-preserves / PUT-replaces semantics, and the no-op short-circuit all trace correctly.
  • Drop/failure/exported metrics — verified the full chain against vendored SDK v0.20.0: global.Warn("dropped log records", "dropped", d uint64)V(1).Info → logr→slog level Info-1 → the Info-1 handler threshold in main.go. attrUint64 handles all three type boxings. Counters owned above the writer so they stay monotonic across toggles.
  • Crash/OOM → ERRORservice_crashed, system_oom_kill, page_crashed map to ERROR in the converter.
  • Renderer-crash eventInspector.enable added to enableDomains; Inspector.targetCrashedpage_crashed (category page). Untracked-session path falls back to target_type: other (valid enum) rather than "", with a regression test. Category gating confirmed: the monitor publishes via telemetrySession.Publish, which drops page events unless the page category is active.
  • Shutdown ordering (Bugbot) — OTLP stop moved to main after the servers drain (mirrors s2Writer); Stop's ctx-exhausted branch closes the provider on a detached 2s budget so a hung flush can't orphan batch-export goroutines. Correct.

Tests

go test ./lib/events/ ./lib/cdpmonitor/ ./lib/metrics/ ./cmd/api/api/ ./cmd/config/ — all green. TestOTLPMetrics_CountsRealDrops forces a real batch-queue overflow through the SDK global logger, so a future SDK message rename fails CI instead of silently zeroing the metric — nice guard.

Live test plan (rebuilt headless image + real OTel collector)

Built the image and ran it against a dockerized otel-collector-contrib + an nginx target, driving the real telemetry API and real Chromium 148 over CDP (custom knobs 512 / 500ms / 30s). All PR claims reproduced:

  • Boot config surfaced the knobs; OTLP export available (not started).
  • GET /telemetry → 404 at boot; PUT with export.otlp.enabled:true → 201 echoing it.
  • Real chrome://crashpage_crashed at collector with SeverityText: ERROR / SeverityNumber: Error(17), source.event=Inspector.targetCrashed, category=page.
  • Converter fidelity: network_response carried http.request.method, url.full, http.response.status_code=Int(200); zero screenshot/monitor records reached the collector.
  • Toggle: with export off, a full page navigation produced zero new exported records (exported_total frozen 22→22); re-enabling rebuilt the writer and resumed (22→54).
  • /metrics: kernel_otlp_records_dropped_total 0, export_failures_total 0, records_exported_total climbing.

Non-blocking notes (no changes required)

  1. reconcileExport holds monitorMu for up to otlpStopTimeout (5s) during a toggle-off drain; if the relay is unreachable this can briefly head-of-line-block concurrent GET/PUT/PATCH /telemetry. Latency only, not correctness — consider draining outside the lock if toggling gets frequent.
  2. BTEL_OTLP_MAX_QUEUE_SIZE is only validated > 0 while the export batch size is fixed at 200; setting it below 200 won't crash but interacts oddly with the constant. Consider validating >= 200 or a doc note.
  3. PUT with an omitted export block disables export (defaults false) — correct PUT-replace semantics and consistent with how omitted categories turn off, but a sharp edge for consumers; PATCH correctly preserves. Worth a sentence in the openapi description.

Nice, well-tested change. LGTM.

…ce docs

- Reconcile OTLP export outside monitorMu: a new exportMu serializes export
  start/stop and reads the committed desired state from the session, so a
  toggle-off drain (bounded by otlpStopTimeout) no longer head-of-line-blocks
  concurrent GET/PUT/PATCH /telemetry. TelemetrySession.Stop now clears the
  export flag so the session stays the single source of desired state. Adds a
  regression test proving a blocked drain doesn't stall a concurrent read.
- Validate BTEL_OTLP_MAX_QUEUE_SIZE against the export batch size (must hold at
  least one full batch) via a shared exported constant so the two can't drift.
- Document PUT-replace vs PATCH-merge semantics for the export block in the
  OpenAPI spec; regenerated oapi.go.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a31b83b. Configure here.

c.cancel()
err := c.writer.Stop(ctx)
c.writer, c.cancel = nil, nil
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Disable drain exports off-window

Medium Severity

On export disable, Stop cancels the read loop then Drains the ring with no seq cutoff. Events published after the toggle-off (capture still on) are forwarded, violating the “only while export is on” invariant and inflating kernel_otlp_records_exported_total.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a31b83b. Configure here.

@archandatta
archandatta merged commit 3be26fc into main Jul 22, 2026
11 checks passed
@archandatta
archandatta deleted the archand/kernel-1596/otlp-export-followups branch July 22, 2026 18:03
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