Repository navigation
Observability: OpenTelemetry-ready tracing and metrics - #10
Merged
Merged
Conversation
UseTelemetry() records a span and a request-duration measurement per attempt, under the OpenTelemetry HTTP client semantic conventions. No dependency on OpenTelemetry, and there will not be one: spans go to System.Diagnostics.Activity and measurements to a Meter, both in the BCL and both the vendor-neutral seam collectors already subscribe to. Taking the SDK as a dependency would impose it on every consumer, including the ones exporting somewhere else or nowhere at all. One span per attempt rather than per call. That is what the conventions describe — a resend carries http.request.resend_count — and it is the only shape that separates a call that took four seconds because the server was slow from one that took four seconds because two tries failed and the backoff was waited out. A single span reports four seconds for both. Conventional attribute names throughout, because conformance is the feature: a dashboard built for HttpClient keeps working, and http.method instead of http.request.method is indistinguishable from no instrumentation to anything consuming it. The transport is the one addition, namespaced as runax.http.transport. Outermost stage, at Order 50. That is a correctness constraint rather than a preference: the correlation stage derives its id from Activity.Current, so a span started after it would leave the traceparent reaching the server naming a span that does not exist — breaking the join between client and server traces silently, since both halves look fine on their own. The histogram is in seconds as the convention specifies, and tagged with low-cardinality attributes only; url.full and the correlation id are deliberately absent, since each distinct id in a path would become its own time series. Failures are still measured, because a p99 that improves when slow calls start failing instead of completing is a graph that lies by omission. Credentials in a URL are stripped from url.full unconditionally, not as an option. Bodies are never recorded. Both for the reason the logging stage gives: a span attribute persists in a backend exactly the way a log line does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Closes the last roadmap item before 1.1.0.
UseTelemetry()records a span and anhttp.client.request.durationmeasurement per request attempt, under the OpenTelemetry HTTP client semantic conventions, with the active transport as a tag.The decisions worth arguing about
No dependency on OpenTelemetry. Spans go to
System.Diagnostics.Activity, measurements to aSystem.Diagnostics.Metrics.Meter— both in the BCL, both the vendor-neutral seam collectors already subscribe to. Taking the SDK as a dependency would impose it on every consumer, including the ones exporting somewhere else or nowhere.One span per attempt, not per call. What the conventions describe (
http.request.resend_count), and the only shape that separates "the server was slow" from "two tries failed and we waited out the backoff". A single span reports the same total for both.Outermost stage,
Order50 — outside correlation's 100. This one is a correctness constraint, not a preference:CorrelationMiddlewarederives its id fromActivity.Current, so a span started after it would leave thetraceparentreaching the server naming a span that does not exist. Both halves of the trace look fine alone; only the join is broken. There is a test for exactly this, asserting thetraceparentheader carries the recorded span's trace id.Conventional attribute names, not nicer ones. Conformance is the feature — a dashboard built for
HttpClientkeeps working, andhttp.methodinstead ofhttp.request.methodis indistinguishable from no instrumentation to anything consuming it. The transport is the one addition, namespacedrunax.http.transport.The histogram is in seconds, as the convention specifies: a unit differing from every other HTTP duration in the same backend cannot be compared with them. Tagged with low-cardinality attributes only —
url.fulland the correlation id are deliberately absent, since each distinct id in a path would become its own time series. Failures are still measured, because a p99 that improves when slow calls start failing rather than completing is a graph that lies by omission.Secrets. Credentials in a URL are stripped from
url.fullunconditionally and that is not configurable. The query string is included by default, matching what the logging stage does with the same information — one library should not take two positions on it — withRecordQueryString = falsefor an API that puts a key there. Bodies are never recorded. A span attribute persists in a backend exactly the way a log line does.Inert until subscribed.
StartActivityreturnsnullwith no listener and the histogram skips an unsubscribed measurement, so this is safe to leave on in a library whose consumers may have no collector.Verification
ActivityListener/MeterListenerrather than a seam of our own — the claim under test is "an OpenTelemetry setup will see this", and OTel sees it through those two types. A fake would only prove the middleware talks to the fake.dotnet build --warnaserrorclean,dotnet formatclean.Also in here
docs/observability.md, and the docs index no longer has a "not written yet" entry.docs/pipeline.mdordering table and the package README updated with the new stage.[Unreleased]closed to## [1.1.0].🤖 Generated with Claude Code