microsoft / microsoft/testfx

[quality-improver] OpenTelemetryPlatformService metric-factory, RootTraceState propagation, and gauge lifetime are untested

Closed
#11,367 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

type/automation type/tech-debt
Dominant language
C#
Stars
1k
Forks
312
Avg merge
7h 46m
Merged PRs (30d)
465

Description

🎯 Repository Quality Improvement Report — OpenTelemetry Platform Service Coverage Gap

Analysis Date: 2026-09-17
Focus Area: opentelemetry-platformservice-metrics-and-tracestate-coverage-gap
Strategy Type: Custom

Executive Summary

Microsoft.Testing.Extensions.OpenTelemetry is the platform's bridge between MTP's dependency-free telemetry interfaces (IPlatformOpenTelemetryService, ICounter<T>, IHistogram<T>, IPlatformActivity) and the real OpenTelemetry SDK. Its span-wrapping (ActivityWrapper) and metric-wrapping (CounterWrapper<T>/UpDownCounterWrapper<T>/HistogramWrapper<T>) logic already have dedicated, thorough unit tests (OpenTelemetryPlatformServiceTests, MeasurementWrapperTests). However, the orchestrating class that creates those wrappers — OpenTelemetryPlatformService itself — is only exercised indirectly through its span-creation methods (StartActivity, StartNonAmbientActivity). Its instrument-factory methods (CreateCounter<T>, CreateUpDownCounter<T>, CreateHistogram<T>, CreateObservableGauge<T>) and its cross-process trace-state propagation logic (RootTraceState / the private Stamp helper that copies RootTraceState onto Activity.TraceStateString) have zero direct test coverage.

This matters because RootTraceState is exactly the mechanism that carries a CI pipeline's externally-supplied W3C tracestate (via EnvironmentTraceContext, wired in CommonTestHost and TestHostBuilder.CommonServices) onto every span this service produces — a regression here would silently break distributed-trace correlation between a CI orchestrator and the test host without any test failing. Likewise, CreateObservableGauge<T> is the only instrument factory that isn't already covered transitively by MeasurementWrapperTests (which tests the wrapper classes directly, not the service's factory methods that construct and register them), so a bug in gauge registration (e.g., forgetting to root the ObservableGauge<T> and letting it be GC'd) would not be caught until a real OpenTelemetry export pipeline stopped receiving gauge samples.

Closing this gap requires new unit tests that exercise OpenTelemetryPlatformService end-to-end through its public factory and trace-state surface, similar in spirit to the existing OpenTelemetryPlatformServiceTests that already uses a live ActivityListener/MeterListener pattern for the wrapper classes.

Full Analysis Report
Focus Area: OpenTelemetry Platform Service Metrics & TraceState Coverage
Current State Assessment

Metrics Collected:

Metric Value Status
OpenTelemetryPlatformService.cs public members 10 (2 props, 8 methods) ⚠️
Members exercised by OpenTelemetryPlatformServiceTests.cs StartActivity, StartNonAmbientActivity, HasCurrentActivity (3) ⚠️
Members with zero direct coverage CreateCounter<T>, CreateUpDownCounter<T>, CreateHistogram<T>, CreateObservableGauge<T>, RootTraceState get/set + Stamp(...), TestFrameworkActivity get/set, Dispose()
MeasurementWrapperTests.cs scope Constructs wrapper types directly (new CounterWrapper<T>(meter.CreateCounter<T>(...))) — never goes through OpenTelemetryPlatformService.CreateCounter<T> ⚠️
Findings
Strengths
  • ActivityWrapper (ambient vs. non-ambient disposal, SetTag/SetStatus/RecordException/AddEvent) is thoroughly tested with a real ActivityListener.
  • CounterWrapper<T>/UpDownCounterWrapper<T>/HistogramWrapper<T> and MeasurementTags.ToArray have excellent, focused tests using a real MeterListener, including null-tags, empty-tags, lazy-enumerable-materialized-once, and null-instrument-throws cases.
  • StartActivity/StartNonAmbientActivity ambient-context semantics (the trickiest part of this service) are already well covered.
Areas for Improvement
  • ⚠️ CreateCounter<T>/CreateUpDownCounter<T>/CreateHistogram<T> on OpenTelemetryPlatformService are never called from a test. Only the wrapper classes they return are tested directly. A defect in how the service forwards name/unit/description/tags to _meter.CreateCounter<T>(...) (e.g., swapped parameters, wrong meter instance) would not be caught.
  • ⚠️ CreateObservableGauge<T> has no coverage at all — neither the service method nor any wrapper, since observable gauges aren't wrapped the way push instruments are. The method also has non-trivial lifetime logic (_observableInstruments list rooting the gauge so it isn't GC'd, cleared in Dispose()), which is exactly the kind of subtle bug (silently-stopped-polling due to premature collection) that only a targeted test — not incidental exercising — will catch.
  • ⚠️ RootTraceState / Stamp(Activity) — the private helper that copies RootTraceState onto Activity.TraceStateString when the activity doesn't already carry one — has no test setting RootTraceState and asserting it propagates onto a created activity (ambient or non-ambient), nor a test confirming that an activity which already set its own TraceStateString (e.g., via an explicit parentId) is left untouched (the is not null guard is the exact condition this class exists to protect).
  • ⚠️ TestFrameworkActivity get/set is a plain auto-property with no dedicated test, even though three other files (OpenTelemetryResultHandler.Notifications.cs, TestHostTestFrameworkInvoker.cs, HotReloadTestHostTestFrameworkInvoker.cs) depend on it round-tripping correctly as the parent-id source for downstream spans.
  • ⚠️ Dispose() — disposing _activitySource and clearing _observableInstruments (while deliberately not disposing _meter, per the inline comment) has no assertion confirming the activity source stops producing activities after disposal, or that the instrument list is actually cleared.

🤖 Suggested Improvement Tasks

The following actionable tasks address the findings above.

Task 1: Add direct tests for the metric-factory methods on OpenTelemetryPlatformService

Priority: High
Estimated Effort: Small

Add tests to test/UnitTests/Microsoft.Testing.Extensions.UnitTests/OpenTelemetryPlatformServiceTests.cs (or a new file) that call _service.CreateCounter<T>(...), CreateUpDownCounter<T>(...), and CreateHistogram<T>(...) directly (not the wrapper classes in isolation), using a MeterListener scoped to the service's internal Meter name (OpenTelemetryPlatformService.MeterName), and assert the emitted instrument's name, unit, and description match what was passed in, and that tags supplied at creation time are attached to the instrument metadata.


Task 2: Add coverage for CreateObservableGauge<T> including its lifetime/rooting behavior

Priority: High
Estimated Effort: Medium

Add a test that calls _service.CreateObservableGauge<T>(name, observeValue, unit, description), forces a GC pass (GC.Collect(); GC.WaitForPendingFinalizers();) to confirm the gauge is not collected prematurely, then uses a MeterListener with periodic collection (MeterListener.RecordObservableInstruments()) to confirm the callback is actually invoked and its value is reported. Also add a test confirming Dispose() clears the internal _observableInstruments list (may require an internal test hook, or can be verified indirectly by confirming no further callback invocation is observed post-dispose within the same listener).


Task 3: Add tests for RootTraceState propagation via the private Stamp helper

Priority: High
Estimated Effort: Small

Add tests that: (a) set _service.RootTraceState = "vendor=value" before calling StartActivity/StartNonAmbientActivity, and assert the resulting activity's TraceStateString equals the root trace state; (b) call StartNonAmbientActivity with an explicit parentId whose implied trace already carries its own TraceStateString and assert RootTraceState does not override it (verifying the activity.TraceStateString is null guard in Stamp). This is the exact code path (CommonTestHost/TestHostBuilder.CommonServices wiring RootTraceState from EnvironmentTraceContext) that carries a CI-injected W3C tracestate onto the test host's spans, and a regression would break distributed-trace correlation silently.


Task 4: Add tests for TestFrameworkActivity round-tripping and Dispose() teardown behavior

Priority: Medium
Estimated Effort: Small

Add a test confirming TestFrameworkActivity get/set round-trips a value assigned to it (guarding the three call sites in OpenTelemetryResultHandler.Notifications.cs, TestHostTestFrameworkInvoker.cs, and HotReloadTestHostTestFrameworkInvoker.cs that rely on it as a parent-id source). Add a test confirming that after Dispose(), _activitySource.StartActivity calls issued through the service's public StartActivity/StartNonAmbientActivity return null (since a disposed ActivitySource never creates activities), matching the documented lifetime contract in the class's Dispose() comment.


Task 5: Document the OpenTelemetryPlatformService coverage expectations in a code comment or test-plan note

Priority: Low
Estimated Effort: Small

Since MeasurementWrapperTests.cs already documents its scope precisely ("Exercises the metric wrappers against a live MeterListener... reused per test method"), add a short remark to the new or existing test file clarifying that these new tests specifically close the OpenTelemetryPlatformService-level gap (factory forwarding, trace-state propagation, gauge lifetime, disposal), distinguishing them from the existing wrapper-level tests so future contributors don't assume the service itself is already fully covered.


📊 Historical Context
Previous Focus Areas
Date Focus Area Type
2026-09-14 azuredevops-runidcoordinator-inherited-and-mismatch-paths-untested Custom
2026-09-15 security-dependency-and-attack-surface-audit Standard
2026-09-16 hotreload-extension-unit-test-coverage-gap Custom
2026-09-17 opentelemetry-platformservice-metrics-and-tracestate-coverage-gap Custom

🎯 Recommendations
Immediate Actions (This Week)
  1. Add RootTraceState/Stamp propagation tests (Task 3) — Priority: High, protects cross-process trace correlation.
  2. Add CreateObservableGauge<T> lifetime tests (Task 2) — Priority: High, guards against silent metric-export regressions.
Short-term Actions (This Month)
  1. Add metric-factory forwarding tests (Task 1) — Priority: High.
  2. Add TestFrameworkActivity/Dispose() teardown tests (Task 4) — Priority: Medium.

Next analysis: 2026-09-18 — Focus area selected based on diversity algorithm

🤖 Automated content by GitHub Copilot. Generated by the Repository Quality Improver workflow. · copilot · auto · 147.3 AIC · ⌖ 15 AIC · ⊞ 16.5K · [◷]( · )

Add this agentic workflow to your repo

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repository-quality-improver.md@main
  • expires on Sep 19, 2026, 10:29 PM UTC

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in test/UnitTests/Microsoft.Testing.Extensions.UnitTests/OpenTelemetryPlatformServiceTests.cs and compare its existing coverage with MeasurementWrapperTests.cs. Run the service tests first, then add direct coverage for the metric factories, observable-gauge lifetime, RootTraceState, TestFrameworkActivity, and Dispose; done means these public behaviors and teardown effects are asserted.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
observability, testing-qa
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.