Skip to content

fix(otel)!: real span duration + honest docs + thread-count instrument (CODE RED 1/3) - #22

Merged
ANcpLua merged 1 commit into
mainfrom
claude/codered-otel-integrity
Jul 1, 2026
Merged

fix(otel)!: real span duration + honest docs + thread-count instrument (CODE RED 1/3)#22
ANcpLua merged 1 commit into
mainfrom
claude/codered-otel-integrity

Conversation

@ANcpLua

@ANcpLua ANcpLua commented Jul 1, 2026

Copy link
Copy Markdown
Owner

An adversarial OTel-compliance + honesty audit (45 agents, 39 candidates, 23 confirmed) rated qyl's OTel code 3/10. This is the verified subset of the root-library fixes.

Verified: core.slnf Release 0/0; verify-webapi-aot-demo passes. No PublicAPI change (all internal/private).

Deliberately NOT here (separate verified increments — not faked): #3 double-count between the listener and interceptor lanes (+ the AOT fixture that masks it), #4/#11 OTLP Events/Links/Status.Message dropped on ingest (cross-repo: contract + DuckDB), #6 url.scheme, #7 method_original on the interceptor path, #9 Azure span name ignores methodName.

🤖 Generated with Claude Code

…trument (CODE RED #1/#2/#10)

An adversarial OTel-compliance + honesty audit rated this code 3/10. This lands the
verified subset of the confirmed HIGH/MEDIUM defects.

#1 (HIGH, ~0-duration lie): the HttpClient/AspNetCore/gRPC DiagnosticListeners created
their span on the framework's *.Stop event via the 2-arg StartActivity (StartTimeUtc=now)
and immediately disposed it, so every emitted span had ~0 duration instead of the real
operation latency. New internal QylActivitySource.StartAtAmbientStart(name, kind) stamps
the span to the ambient framework Activity's real StartTimeUtc (parented to it for trace
correlation), with a now-stamped fallback when there is no ambient activity. All three
listeners switched to it.

#2 (HIGH, dishonest doc): DiagnosticListenerSubscriber claimed it "publishes the same span
shapes" — false while durations were fabricated. Doc now states the actual mechanism
(reacts on *.Stop, stamps to ambient start for real duration).

#10 (MEDIUM, metrics-semconv): dotnet.thread_pool.thread.count was an unitless
ObservableGauge<int>; per OTel semconv it is an (Observable)UpDownCounter with UCUM unit
{thread}. Switched instrument type + added unit.

Verified: core.slnf Release 0/0; verify-webapi-aot-demo passes (fixture unchanged — the
attribute shape is identical; note the fixture is duration-insensitive, which is itself
tracked as a separate finding). No PublicAPI change (all internal/private).

NOT in this commit (separate verified increments): #3 double-count between the listener
and interceptor lanes (+ the fixture that masks it), #4/#11 OTLP Events/Links/Status.Message
dropped on ingest, #6 url.scheme, #7 http.request.method_original on the interceptor path,
#9 Azure span name ignores methodName.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 1, 2026 14:35
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f260a0c7-08e0-44e5-9c0a-c08ac69b9a52

📥 Commits

Reviewing files that changed from the base of the PR and between d31d94f and e22d757.

📒 Files selected for processing (6)
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs
📜 Recent review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: copilot-pull-request-reviewer
  • GitHub Check: verify (qyl-macos)
🧰 Additional context used
📓 Path-based instructions (2)
**

⚙️ CodeRabbit configuration file

AGENTS.md

**: # Qyl.OpenTelemetry.AutoInstrumentation agent rules

Mission

This repository is the runtime AOT auto-instrumentation lane for qyl, evolving into a
self-describing observability substrate. The foundation is unchanged: .NET 10
NativeAOT-compatible zero-code instrumentation through managed build assets, source generation,
DiagnosticListener consumption, and module-initializer boot. The direction is the North Star
below.

Keep this repository separate from:

  • semantic-convention package generation (Qyl.OpenTelemetry.SemanticConventions is a referenced
    vocabulary package, not generated here),
  • the old CLR-profiler/OpenTelemetry auto-instrumentation substrate.

North Star — declare and prove the whole stack

Every observability tool today is pull-by-observation: a backend learns what a service emits
by receiving samples over time, and never knows whether it has seen the whole surface. qyl has a
capability none of them have — because instrumentation is source-generated interceptors + a static
contract + a referenced semconv registry + (incrementally) DTO inference, the complete set of
telemetry a binary can ever produce is a compile-time-derivable fact, with provenance.

The substrate goal: every qyl binary ships a complete, machine-readable Telemetry Capability
Graph (TCG)
— the full possible OpenTelemetry surface for that exact binary, each capability
tagged compile-time-owned vs runtime-valued — and proves it by self-hosting (instrumenting its
own pipeline with its own mechanism, zero extra code). Any external entity consumes the TCG to know
the entire stack before a span is sampled. The contract becomes the shared semantic graph; an OTLP
backend is just one consumer.

Three pillars:

  1. Self-host (the proof). qyl instruments qyl with qyl — QylSelfTelemetry /
    SemConvConformanceProcessor are the seed; the binary observing itself is how "declared TCG ==
    runtime reality" is checked.
  2. **Compile-time-complete ...

Files:

  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs

⚙️ CodeRabbit configuration file

**: Operating principles (solo-dev, agentic SDLC — reviews are advisory, agents act on them):

  1. LAZY: one self-contained, correct review beats ten partial ones. Every finding is
    definitive — concrete evidence with file:line, a concrete fix, no "consider maybe",
    no open or ambiguous questions back to the author. If you cannot decide a point
    from the diff plus repo context, stay silent on it. Never cite a source, API, or
    version you have not verified; an unverifiable claim is a dropped claim.
  2. IMPATIENT: never stall a PR. There are no compatibility obligations here — internal
    and dogfooding code has NO public-API contract; removing shims, breaking signatures,
    and deleting dead paths are normal, desirable changes. Do not flag backward
    compatibility, deprecation ceremony, or migration paths. (SemVer applies only to
    commercially sold libraries — this repo has none.)
  3. EGO: hold the bar of the best reviewer on the market — flag real correctness,
    security, data-loss, and structural problems precisely; produce zero noise.

Files:

  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs
src/**/*.cs

⚙️ CodeRabbit configuration file

src/**/*.cs: Zero-code instrumentation runtime: this code runs inside EVERY request of host
applications. Top priorities, in order: (1) allocations and boxing on hot paths —
flag closures, LINQ, params arrays, string concat in listener/semantic-tag code;
(2) tag cardinality — any attribute value that is unbounded (raw URLs, user input,
exception messages) explodes at scale; (3) Activity/Meter lifecycle — undisposed
listeners, leaked subscriptions, double-Start/Stop; (4) thread safety of shared
listener state. PublicAPI.Shipped/Unshipped.txt are analyzer-managed: edits must
come from the analyzer flow, and API breaks are fine (internal product, no
compatibility contract).

Files:

  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs
  • src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs
🔇 Additional comments (6)
src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs (1)

50-55: LGTM!

src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs (1)

43-43: Same Activity.Current corruption inherited from StartAtAmbientStart.

Call-site swap itself is correct usage of the new helper; the root-cause fix belongs in QylActivitySource.StartAtAmbientStart (flagged there) rather than here.

src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs (1)

41-41: Same Activity.Current corruption inherited from StartAtAmbientStart.

Call-site swap itself is correct usage of the new helper; the root-cause fix belongs in QylActivitySource.StartAtAmbientStart (flagged there) rather than here.

src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs (1)

42-42: Same Activity.Current corruption inherited from StartAtAmbientStart.

Call-site swap itself is correct usage of the new helper; the root-cause fix belongs in QylActivitySource.StartAtAmbientStart (flagged there) rather than here.

src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs (1)

13-17: LGTM!

src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs (1)

35-44: 🎯 Functional Correctness

StartAtAmbientStart is dead code here. src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs:35-44 has no call sites in this tree, so it does not affect Activity.Current in any listener path.

			> Likely an incorrect or invalid review comment.

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Improved tracing for HTTP server, HTTP client, and gRPC client requests so spans align more accurately with the true start time of the underlying activity.
    • Updated runtime thread pool metrics to use a more appropriate measurement type and unit.
  • Bug Fixes

    • Fixed span timing so durations better reflect real request execution, especially when an ambient activity is already present.
  • Documentation

    • Clarified the instrumentation approach in the public documentation comments.

Walkthrough

Adds an internal StartAtAmbientStart helper to QylActivitySource that starts activities using the ambient Activity.Current's context and start time when present, wires three diagnostic listeners (AspNetCore, gRPC client, HttpClient) to use it, updates subscriber documentation, and changes ThreadPoolThreads metric from ObservableGauge to ObservableUpDownCounter.

Changes

Ambient-Start Activity Timing

Layer / File(s) Summary
StartAtAmbientStart helper
src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs
New internal helper returns null when no listeners exist, otherwise starts an activity using Activity.Current's Context and StartTimeUtc when an ambient activity exists, falling back to default start otherwise.
Listener wiring and documentation
.../AspNetCore/AspNetCoreDiagnosticListener.cs, .../GrpcClient/GrpcClientDiagnosticListener.cs, .../HttpClient/HttpClientDiagnosticListener.cs, .../DiagnosticListenerSubscriber.cs
Three listeners switch from QylActivitySource.Source.StartActivity(...) to QylActivitySource.StartAtAmbientStart(...); subscriber base class doc comment updated to describe AOT-safe, ambient-start-stamped span emission.

Thread Pool Metric Type

Layer / File(s) Summary
ThreadPoolThreads instrument change
src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs
ThreadPoolThreads changes from ObservableGauge<int> to ObservableUpDownCounter<int>, adds a "{thread}" unit, and comments justifying it as non-unitless per semconv.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DiagnosticListener
  participant QylActivitySource
  participant ActivityCurrent

  DiagnosticListener->>QylActivitySource: StartAtAmbientStart(operationName, kind)
  QylActivitySource->>ActivityCurrent: read Activity.Current
  alt ambient activity present
    QylActivitySource->>QylActivitySource: StartActivity(name, kind, Context, startTime: ambient.StartTimeUtc)
  else no ambient activity
    QylActivitySource->>QylActivitySource: StartActivity(name, kind)
  end
  QylActivitySource-->>DiagnosticListener: Activity or null
Loading

Estimated code review effort: 2 (Simple) | ~12 minutes

QylActivitySource.cs:36 — when no ambient Activity exists, StartAtAmbientStart falls through to StartActivity(operationName, activityKind) without Context, silently dropping any parent linkage that Source.StartActivity's default overload would have picked up from Activity.Current anyway — redundant branch, not a bug, but the null-check-then-reimplement-default pattern here is pointless code duplication. Ship it.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main changes: span duration fix, doc correction, and thread-count metric update.
Description check ✅ Passed The description is directly aligned with the implemented OpenTelemetry listener, docs, and metric changes.
✨ 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 claude/codered-otel-integrity
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/codered-otel-integrity

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

@ANcpLua
ANcpLua merged commit a000875 into main Jul 1, 2026
5 of 10 checks passed
@ANcpLua
ANcpLua deleted the claude/codered-otel-integrity branch July 1, 2026 14:36

Copilot AI 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.

Pull request overview

This PR addresses OTel semantic compliance issues in the DiagnosticListener-based tracing bridges by ensuring emitted qyl spans reflect real operation durations (rather than ~0-duration spans created on *.Stop), updates related documentation to accurately describe the mechanism, and corrects the runtime thread-pool thread-count metric instrument type/unit to match semconv.

Changes:

  • Introduce QylActivitySource.StartAtAmbientStart(...) to start qyl Activities at the ambient framework Activity’s start timestamp.
  • Switch HttpClient / ASP.NET Core / gRPC DiagnosticListeners to use StartAtAmbientStart when creating spans on *.Stop.
  • Update dotnet.thread_pool.thread.count to an ObservableUpDownCounter with unit {thread}.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs Updates thread-pool thread count metric to semconv-correct instrument type and unit.
src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs Adds helper to start spans at ambient Activity start time to produce correct durations.
src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs Uses the new ambient-start helper so HttpClient spans have real durations.
src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs Uses the new ambient-start helper so gRPC client spans have real durations.
src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs Updates documentation to accurately describe the DiagnosticListener stop-event bridge behavior.
src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs Uses the new ambient-start helper so ASP.NET Core server spans have real durations.

Comment on lines +40 to +43
var ambient = Activity.Current;
return ambient is null
? Source.StartActivity(operationName, activityKind)
: Source.StartActivity(operationName, activityKind, ambient.Context, tags: null, links: null, startTime: ambient.StartTimeUtc);
Comment on lines +13 to +15
/// primitive that's been AOT-safe since .NET 8, so this layer emits spans without any IL rewriting
/// or runtime code generation. Concrete subscribers react on the completion (<c>*.Stop</c>) event and
/// stamp the span to the ambient framework activity's start (via
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