Skip to content

[Extensions] Add consistent probability sampler - #4629

Merged
martincostello merged 26 commits into
open-telemetry:mainfrom
martincostello:gh-3678
Aug 13, 2026
Merged

[Extensions] Add consistent probability sampler#4629
martincostello merged 26 commits into
open-telemetry:mainfrom
martincostello:gh-3678

Conversation

@martincostello

@martincostello martincostello commented Jul 1, 2026

Copy link
Copy Markdown
Member

Fixes #3678

Changes

Add an implementation of a consistent probability sampler (spec).

Benchmarks

Summarised by Claude 🐙

Analysis

All four scenarios measure the per-span cost of a single ShouldSample call with the sampler configured at 25%. The operations are tens-to-hundreds of nanoseconds and allocate only small, bounded amounts; the allocation figures are the most stable and meaningful signal (the net10.0 timings have wide error bars because the machine was noisier during that run).

By scenario:

  • RandomTraceId is the cheapest path on both runtimes. No rv is generated or written and no other tracestate members need preserving, so the outgoing tracestate is just ot=th:<x>. On net462 it allocates 0 B — the trace-id randomness is read via a span slice, the threshold is formatted without an intermediate string, and there are no lists to allocate. On net10.0 it still shows 168 B solely because ActivityTraceId.ToHexString() allocates a 32-char string (there is no public byte-level accessor to avoid it).
  • RootSpan (baseline) and ExplicitRandomValue are dominated by building the outgoing ot=th:<x>;rv:<y> string — the StringBuilder plus the final serialized string, which is unavoidable since it is the return value. ExplicitRandomValue is slightly cheaper/leaner than the root case because it reuses the inherited rv instead of generating one.
  • ExplicitRandomValueWithOtherMembers is the most expensive (~1.3× time and ~2× allocations vs. baseline). The two extra vendor members must be parsed into preserved strings and re-emitted. This is exactly the lazy-list code path that allocates only when extras are present — the common cases above pay nothing for it.

Runtime differences:

  • net10.0 uses the more efficient #if NET paths (Random.Shared.NextInt64, long.TryFormat into stack buffers, stackalloc spans), giving lower allocations on the string-building paths than net462, which falls back to NextBytes + ToString.
  • net462 is slower in absolute terms on the allocating paths but tighter/more stable in its measurements, and confirms the zero-allocation RandomTraceId path holds on the fallback code.

Takeaways:

  • The struct + lazy-list + span/TryFormat work pays off: the hot paths allocate one small string (the required output) or nothing, and extra-member preservation is pay-for-what-you-use.
  • The remaining RootSpan/ExplicitRandomValue allocations are the serialized tracestate string itself (intrinsic to the sampler's contract) plus, on net10.0, the TraceId.ToHexString() string on the trace-id path.
Expand to view

ConsistentProbabilitySampler.ShouldSample (sampler configured at 25%)

net10.0

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8655/25H2/2025Update/HudsonValley2)
13th Gen Intel Core i7-13700H 2.90GHz, 1 CPU, 20 logical and 14 physical cores
.NET SDK 10.0.301
  [Host]     : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3
  DefaultJob : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3
Method Mean Error StdDev Median Ratio RatioSD Allocated Alloc Ratio
RootSpan 120.68 ns 8.335 ns 24.181 ns 111.66 ns 1.04 0.28 280 B 1.00
ExplicitRandomValue 81.04 ns 3.933 ns 10.897 ns 76.17 ns 0.70 0.15 272 B 0.97
RandomTraceId 57.12 ns 3.308 ns 9.598 ns 53.74 ns 0.49 0.12 168 B 0.60
ExplicitRandomValueWithOtherMembers 154.03 ns 6.455 ns 18.520 ns 146.02 ns 1.32 0.28 648 B 2.31

net462

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8655/25H2/2025Update/HudsonValley2)
13th Gen Intel Core i7-13700H 2.90GHz, 1 CPU, 20 logical and 14 physical cores
  [Host]     : .NET Framework 4.8.1 (4.8.9325.0), X64 RyuJIT VectorSize=256
  DefaultJob : .NET Framework 4.8.1 (4.8.9325.0), X64 RyuJIT VectorSize=256
Method Mean Error StdDev Median Ratio RatioSD Allocated Alloc Ratio
RootSpan 229.91 ns 3.404 ns 2.842 ns 229.88 ns 1.00 0.02 401 B 1.00
ExplicitRandomValue 251.62 ns 4.835 ns 7.944 ns 249.63 ns 1.09 0.04 337 B 0.84
RandomTraceId 68.57 ns 3.577 ns 10.492 ns 63.55 ns 0.30 0.05 - 0.00
ExplicitRandomValueWithOtherMembers 449.76 ns 14.388 ns 40.346 ns 434.78 ns 1.96 0.18 738 B 1.84

Merge requirement checklist

  • CONTRIBUTING guidelines followed (license requirements, nullable enabled, static analysis, etc.)
  • Unit tests added/updated
  • Appropriate CHANGELOG.md files updated for non-trivial changes
  • Changes in public API reviewed (if applicable)

Add an implementation of a consistent probability sampler.

Contributes to open-telemetry#3678.
- Add fuzz tests for `ConsistentProbability`.
- Fix StyleCop warning.
Comment thread src/OpenTelemetry.Extensions/CHANGELOG.md Outdated
@github-actions github-actions Bot added the comp:extensions Things related to OpenTelemetry.Extensions label Jul 1, 2026
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.49057% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.88%. Comparing base (6aea88b) to head (19e6de1).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ons/Internal/OpenTelemetryExtensionsEventSource.cs 66.66% 2 Missing ⚠️
...penTelemetry.Extensions/Internal/OtelTraceState.cs 98.82% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4629      +/-   ##
==========================================
+ Coverage   77.55%   77.88%   +0.32%     
==========================================
  Files         468      471       +3     
  Lines       19920    20177     +257     
==========================================
+ Hits        15449    15714     +265     
+ Misses       4471     4463       -8     
Flag Coverage Δ
unittests-Contrib.Shared.Tests 90.26% <ø> (ø)
unittests-Exporter.Geneva 56.72% <ø> (+0.24%) ⬆️
unittests-Exporter.InfluxDB 97.96% <ø> (ø)
unittests-Exporter.OneCollector 95.60% <ø> (ø)
unittests-Extensions 95.74% <98.49%> (+5.13%) ⬆️
unittests-Extensions.Enrichment 100.00% <ø> (ø)
unittests-Extensions.Enrichment.AspNetCore 86.27% <ø> (ø)
unittests-Extensions.Enrichment.Http 94.33% <ø> (ø)
unittests-Instrumentation.AWS 85.35% <ø> (ø)
unittests-Instrumentation.AspNet 79.01% <ø> (ø)
unittests-Instrumentation.AspNetCore 87.20% <ø> (ø)
unittests-Instrumentation.Cassandra 92.30% <ø> (ø)
unittests-Instrumentation.ConfluentKafka 82.83% <ø> (ø)
unittests-Instrumentation.ElasticsearchClient 80.36% <ø> (ø)
unittests-Instrumentation.EntityFrameworkCore 81.02% <ø> (ø)
unittests-Instrumentation.EventCounters 75.45% <ø> (ø)
unittests-Instrumentation.GrpcCore 91.12% <ø> (ø)
unittests-Instrumentation.GrpcNetClient 78.09% <ø> (ø)
unittests-Instrumentation.Hangfire 88.88% <ø> (ø)
unittests-Instrumentation.Http 75.61% <ø> (ø)
unittests-Instrumentation.Kusto 92.42% <ø> (ø)
unittests-Instrumentation.Owin 89.02% <ø> (ø)
unittests-Instrumentation.Process 100.00% <ø> (ø)
unittests-Instrumentation.Quartz 77.77% <ø> (ø)
unittests-Instrumentation.Remoting 65.59% <ø> (ø)
unittests-Instrumentation.Runtime 100.00% <ø> (ø)
unittests-Instrumentation.ServiceFabricRemoting 39.91% <ø> (ø)
unittests-Instrumentation.SqlClient 83.36% <ø> (ø)
unittests-Instrumentation.StackExchangeRedis 94.98% <ø> (ø)
unittests-Instrumentation.Wcf 82.64% <ø> (ø)
unittests-OpAmp.Client 87.05% <ø> (ø)
unittests-PersistentStorage 69.20% <ø> (-1.47%) ⬇️
unittests-Resources.AWS 72.78% <ø> (ø)
unittests-Resources.Azure 84.18% <ø> (ø)
unittests-Resources.Container 80.76% <ø> (ø)
unittests-Resources.Gcp 80.70% <ø> (ø)
unittests-Resources.Host 72.34% <ø> (ø)
unittests-Resources.OperatingSystem 77.16% <ø> (ø)
unittests-Resources.Process 90.90% <ø> (ø)
unittests-Resources.ProcessRuntime 80.76% <ø> (ø)
unittests-Sampler.AWS 96.05% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...metry.Extensions/Internal/ConsistentProbability.cs 100.00% <100.00%> (ø)
...y.Extensions/Trace/ConsistentProbabilitySampler.cs 100.00% <100.00%> (ø)
...ons/Internal/OpenTelemetryExtensionsEventSource.cs 69.23% <66.66%> (+9.23%) ⬆️
...penTelemetry.Extensions/Internal/OtelTraceState.cs 98.82% <98.82%> (ø)

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new consistent probability-based sampler to OpenTelemetry.Extensions, implementing the OpenTelemetry tracestate probability-sampling spec (including encoding/decoding thresholds and propagating ot=th/ot=rv), along with unit and property-based tests plus documentation and API surface tracking.

Changes:

  • Added ConsistentProbabilitySampler (public) with supporting internal codec utilities and tracestate (ot) parsing/serialization.
  • Added unit tests for the codec, tracestate handling, and sampler behavior; added fuzz/property tests to validate invariants across randomized inputs.
  • Updated OpenTelemetry.Extensions docs/changelog and public API declarations; added the new fuzz test project to the solution.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/OpenTelemetry.Extensions.Tests/Trace/OtelTraceStateTests.cs New unit tests for parsing/serializing ot tracestate (th/rv) while preserving other members/subkeys.
test/OpenTelemetry.Extensions.Tests/Trace/ConsistentProbabilityTests.cs New unit tests validating threshold encoding/decoding against spec tables and invariants.
test/OpenTelemetry.Extensions.Tests/Trace/ConsistentProbabilitySamplerTests.cs New unit tests validating sampler constructor validation, decisions, and tracestate propagation semantics.
test/OpenTelemetry.Extensions.FuzzTests/OpenTelemetry.Extensions.FuzzTests.csproj New fuzz test project for randomized/property-based validation.
test/OpenTelemetry.Extensions.FuzzTests/ConsistentProbabilityFuzzTests.cs Property-based tests (FsCheck) for codec/tracestate/sampler invariants.
src/OpenTelemetry.Extensions/Trace/ConsistentProbabilitySampler.cs New sampler implementation that resolves randomness from rv, TraceId random flag, or generated randomness, and sets/clears th.
src/OpenTelemetry.Extensions/Internal/OtelTraceState.cs New internal parser/serializer for ot tracestate entry supporting th/rv.
src/OpenTelemetry.Extensions/Internal/ConsistentProbability.cs New internal codec for converting probability ↔ threshold and parsing hex56 values.
src/OpenTelemetry.Extensions/README.md Documentation for ConsistentProbabilitySampler with usage snippet.
src/OpenTelemetry.Extensions/CHANGELOG.md Added Unreleased entry for the new sampler.
src/OpenTelemetry.Extensions/.publicApi/PublicAPI.Unshipped.txt Added new public API entries for ConsistentProbabilitySampler.
src/OpenTelemetry.Extensions/OpenTelemetry.Extensions.csproj Added InternalsVisibleTo for the new fuzz test assembly.
opentelemetry-dotnet-contrib.slnx Added the fuzz test project to the solution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/OpenTelemetry.Extensions/Internal/ConsistentProbability.cs Outdated
Comment thread src/OpenTelemetry.Extensions/Trace/ConsistentProbabilitySampler.cs Outdated
Comment thread src/OpenTelemetry.Extensions/README.md
Comment thread src/OpenTelemetry.Extensions/CHANGELOG.md Outdated
Add benchmarks for the sampler.
@github-actions github-actions Bot added the perf Performance related label Jul 1, 2026
Add more test coverage.
- Fix incorrect threshold clamping.
- Prevent too-small thresholds from being specified.
- Add missing test coverage.
- Fix typo in code sample.
Fix flaky test on .NET Framework by using a Stopwatch instead of `DateTime.UtcNow`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the package is beta and that the new public API exposed here is effectively an implementation of a stable abstract class and otherwise only accepts a double value to set the probability for sampling, this is OK to not be experimental?

If the spec were to change that would just be internal implementation details and we'd just ship a new version right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, since the package is not stable, we do not need to do the experimental API dance.

Rename `injectedRandom` to `random`.
Wrap the ranges in `c` tags.
Use `Math.ILogB()` instead of manual code on .NET 8+.
Add an end-to-end test that demonstrates usage across multiple processes as if in a real distributed system.
@martincostello
martincostello marked this pull request as ready for review July 2, 2026 08:05
@martincostello
martincostello requested a review from a team as a code owner July 2, 2026 08:05
@github-actions
github-actions Bot requested a review from MikeGoldsmith July 3, 2026 11:17
@martincostello martincostello added the keep-open Prevents issues and pull requests being closed as stale label Jul 8, 2026
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Jul 18, 2026

Copy link
Copy Markdown

Pull request dashboard status

Merged · refreshed 2026-08-13 12:24 UTC

Status above doesn't look right?
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@Kielek Kielek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed some feedback directly to this PR. With this LGTM.

Please double check before merge.

Add polyfill for modern .NET char methods for ASCII characters and use in `OtelTraceState`.
@martincostello

Copy link
Copy Markdown
Member Author

Please double check before merge.

LGTM. I pulled through the char extensions/polyfills from the SDK repo and used them where relevant. When the code is ported to the SDK it will benefit automatically.

I'll look to see if there's other places in this repo that can benefit from the new polyfills separately.

@martincostello
martincostello added this pull request to the merge queue Aug 13, 2026
Merged via the queue into open-telemetry:main with commit 9ad732f Aug 13, 2026
321 checks passed
@martincostello
martincostello deleted the gh-3678 branch August 13, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:extensions Things related to OpenTelemetry.Extensions keep-open Prevents issues and pull requests being closed as stale perf Performance related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement consistent probability sampler.

6 participants