Repository navigation
Conversation
The controller and writer take a constructor instead of S2 credentials, so a second backend can share their lifecycle and fork gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When BTEL_STORAGE_PATH is set, stored telemetry is posted in batches to the metro-api relay with the instance JWT instead of appended to S2 with a credential baked into the VM. Direct S2 stays available when only the S2 credentials are set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retry temporary transport errors like the OTLP exporter, count batches cut off by Close as shutdown drops, keep the post-deadline wait short, and refuse a relay path with no host to reach it on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
metro-api closes idle connections when a deploy replaces its process. A batch written to a kept-alive connection at that moment got an EOF and was dropped. Mark the request as safe to resend so the HTTP client sends it again on a fresh connection. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The relay sink sent each record's event time and the relay stamped the S2 record with it. The direct sink sends none, so S2 stamps arrival time and the since/until window of a telemetry read means arrival time. Send the body alone so both sinks store records the same way. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Sayan-
marked this pull request as ready for review
November 13, 2026 03:04
Contributor
Author
|
Picked rgarcia to review: keeping one reviewer for the telemetry storage changes. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Serve browser telemetry storage through the metro-api relay instead of baking S2 credentials into VMs. This PR adds the in-VM sink.
When
BTEL_STORAGE_PATHis set, stored telemetry is posted to the metro-api relay instead of appended to S2. Direct S2 (S2_BASINplusS2_ACCESS_TOKEN) is unchanged and still used when the path is unset. The relay wins when both are set.Each record body is
json.Marshal(envelope), exactly what the direct sink appended, so S2 contents and the read path are unchanged. No timestamp is sent, so S2 stamps arrival time as it does for the direct sink.The sink follows the OTLP sink's semantics:
Retry-After, for up to 30s per batch.The bearer token (instance JWT) and relay host are resolved per batch, so a fork posts with its own identity to the metro-api it was handed. Host order:
BTEL_OTLP_ENDPOINT, withBTEL_OTLP_INSECUREpicking the scheme.The token goes only on requests to that URL, and redirects are refused.
The storage controller now takes a storage constructor. The relay controller shares the S2 controller's start/stop/once-only lifecycle and its fork gate: nothing opens until
S2_STREAM(or the applied fork identity's stream) resolves. The telemetry handler is unchanged.Config:
BTEL_STORAGE_PATH,BTEL_STORAGE_MAX_RECORDS(200),BTEL_STORAGE_MAX_BYTES(786432),BTEL_STORAGE_LINGER(100ms),BTEL_STORAGE_MAX_QUEUE(8192). When the path is set, values are validated against S2's batch limits and a relay host is required. Documented inserver/README.md.Metrics:
kernel_btel_relay_exported_total,_dropped_total{reason},_retries_total,_failed_total{status},_queue_depth.Rollout
Release this image before provisioning sets
BTEL_STORAGE_PATH. Images without this sink provisioned without S2 credentials store nothing.Validation
lib/eventsrelay sink tests againsthttptest:json.Marshal(envelope)byte for byte, including HTML and line-separator escapes. A record on the wire is its body alone, with no timestamp.Retry-Afteris honoured.Controller contract tests run against both the S2 and relay controllers: lifecycle, the fork gate, and rollback when start is cancelled.
cmd/api/api/telemetry_test.go: the storage toggle runs against both real controllers. Storage opens once, refuses to turn off (409), and stays closed while no stream resolves.Config validation cases, the metrics collector, and the relay base URL fallback.
e2e, run locally against a freshly built headless image:
TestRelayStorage: relay plus direct S2 credentials; records reach a mock relay with the instance JWT and valid envelopes.TestRelayStorageForkIdentity: a fork boots with no JWT and a dead endpoint. After the identity handoff, storage reaches the handed metro-api with the fresh JWT.Both pass, as does the existing
TestOTLPExport. Locally, testcontainers neededTESTCONTAINERS_RYUK_DISABLED=trueandTESTCONTAINERS_HOST_OVERRIDE=127.0.0.1because of this machine's DNS.go test ./...outside e2e passes. One devtoolsproxy test flaked once on Chromium temp-dir cleanup and passed on rerun.Not tested: a real metro-api relay and S2.
🤖 Generated with Claude Code
Note
Medium Risk
Changes the telemetry persistence and auth path (JWT bearer to metro-api) with batching/drop behavior on failure, though direct S2 remains when the relay path is unset and coverage is extensive.
Overview
Adds an optional metro-api storage relay path so VMs can persist browser telemetry without embedding S2 credentials. When
BTEL_STORAGE_PATHis set, the API swaps in a relay storage controller (it wins over directS2_BASIN/S2_ACCESS_TOKEN) that POSTsjson.Marshal(envelope)batches to the relay using the instance JWT, with host resolution preferring the fork/metro identity URL and falling back toBTEL_OTLP_ENDPOINT.The new
relayStoragesink mirrors OTLP semantics: bounded queue with drop-oldest, batching by record/byte count and linger, single-flight ordered sends, retry policy for throttling/outages (honoringRetry-After), no redirect following, and per-batch JWT/base URL resolution for fork handoffs.S2StorageControlleris refactored behind an injectableopenhook so direct S2 and relay share the same start-once / fork stream gate behavior.Config validation, README env docs,
kernel_btel_relay_*metrics, and unit/controller/e2e tests cover the new path (including fork identity and relay-over-S2 precedence).Reviewed by Cursor Bugbot for commit 5aa0895. Bugbot is set up for automated code reviews on this repo. Configure here.