Skip to content

test(telemetry): pin the wire format and what a dead collector costs - #736

Open
latekvo wants to merge 1 commit into
feat/otel-telemetryfrom
test/otel-wire-e2e
Open

test(telemetry): pin the wire format and what a dead collector costs#736
latekvo wants to merge 1 commit into
feat/otel-telemetryfrom
test/otel-wire-e2e

Conversation

@latekvo

@latekvo latekvo commented Aug 4, 2026

Copy link
Copy Markdown
Member

Stacked on #570 - base is feat/otel-telemetry, so the diff here is just the tests.

The suite on #570 proves the exporter is configured right: the url, the headers, the timeouts, all read off the constructor. Nothing looks at the request that comes out of it, or at what happens when the collector does not answer.

Both gaps are invisible from inside Argent by design. A failed export never reaches the caller - the batch processor hands the error to OpenTelemetry’s global handler, so shutdown() resolves identically for a delivered batch, a 401, and a dead host. And an OTLP exporter counts any 2xx as delivered, so a payload the collector maps differently is still a success from here.

otel-wire.test.ts

Captures the real exporter’s request off a loopback socket and asserts:

  • uncompressed OTLP/JSON on POST /v1/logs - not protobuf, and no content-encoding
  • service.name, the instrumentation scope, the record body, the severity and the attribute value types land where the ingestion schema reads them
  • a request never carries more than maxExportBatchSize records

otel-unreachable.test.ts

Drives the same exporter against a socket that accepts and never answers, a refused port, a 401 and a 204, and asserts the drain stays inside its deadline and the stalled socket is destroyed rather than parked in the keep-alive pool.

Dropping timeoutMillis from createExporter fails two of these; the SDK’s own default would let the drain run to 10s.

otel-endpoint.test.ts

The wire test mirrors OtelClient.emit rather than calling it - getClient() resolves its endpoint from the hard-coded OTLP_LOGS_ENDPOINT, and that being unredirectable is the anti-exfiltration property the client is meant to have, so there is deliberately no seam to aim it at a test server. That left the source constants unpinned, so this adds the other half against the real emit path: SERVICE_NAME, the logger name, event-name-as-body, and the dropping of null/undefined properties.

Provenance

These were shell rigs used while verifying #570 by hand (a wire probe, a capture receiver with a blackhole mode, a silent TCP server, a proxy). They were parked in the infra repo, where nobody would ever run them. This is them as tests that run on every push.

Verified

  • npm test -w @argent/telemetry - 304 passed, 19 files
  • typecheck:tests, eslint --max-warnings 0, prettier --check all clean
  • Mutation-tested. Each new assertion fails on broken source, exactly one test each: SERVICE_NAME renamed, logger name changed, body no longer the event, severityText flipped, the null-property skip removed, and timeoutMillis dropped. Removing httpAgentOptions does not fail these - that option covers a socket stuck in connect, which needs an address whose packets are dropped and is not portably reproducible here; it stays pinned at the constructor in otel-endpoint.test.ts, and the docstring says so rather than implying coverage it does not have.

CI does not run on this PR yet

Every substantive workflow in this repo is gated on pull_request: branches: [main], so a PR based on feat/otel-telemetry gets zero checks. That resolves itself when #570 merges and GitHub retargets this base to main.

These tests cannot run against main at all in the meantime - they exercise createExporter and the rest of otel.ts, which only exist on #570. So the stack is required, not a convenience.

Run locally in the meantime, mirroring unit-tests.yml on its Node 20: npm test -w @argent/telemetry 304 passed, typecheck:tests clean, eslint --max-warnings 0 clean, prettier --check clean.

The telemetry tests so far prove the exporter is CONFIGURED correctly - the
right url, the right headers, the right timeouts on the constructor. Nothing
looks at the request that comes out of it, or at what happens when the collector
on the other end does not answer.

Both gaps matter because a failing export is silent by design: the batch
processor hands errors to OpenTelemetry's global handler, so shutdown() resolves
the same way for a delivered batch, a 401 and a dead host. Nothing downstream
can tell us the payload stopped matching what the collector reads, either - an
OTLP exporter counts any 2xx as delivered.

otel-wire.test.ts captures the real exporter's request off a loopback socket and
asserts it is uncompressed OTLP/JSON on POST /v1/logs, that service.name, the
instrumentation scope, the record body, the severity and the attribute types
land where the ingestion schema reads them, and that a batch never exceeds
maxExportBatchSize records.

otel-unreachable.test.ts drives the same exporter against a socket that accepts
and never answers, a refused port, a 401 and a 204, and asserts the drain stays
inside its deadline and the stalled socket is destroyed rather than left in the
keep-alive pool. Dropping timeoutMillis from createExporter fails two of these;
the SDK's own default would let the drain run to 10s.

otel-endpoint.test.ts gains the source-side half: the wire test builds its record
the way OtelClient.emit does rather than calling it, because the endpoint is
deliberately unredirectable and there is no seam to point the client at a test
server. So SERVICE_NAME, the logger name, the event-name-as-body mapping and the
dropping of null properties are pinned here, against the real emit path.
@latekvo
latekvo marked this pull request as ready for review August 5, 2026 12:16
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.

1 participant