Skip to content

Commit 7a9d7a0

Browse files
committed
[SDK] Implement the ProbabilitySampler
Implements the spec's ProbabilitySampler per the consistent probability sampling rules: the 56-bit rejection threshold is computed from the configured ratio, the randomness value comes from the rv sub-key of the OpenTelemetry tracestate entry when present and from the rightmost 7 bytes of the trace id otherwise, spans are sampled when R >= T, and sampled spans carry the threshold as the th sub-key of the ot tracestate entry. The composable probability sampler configuration now builds this sampler instead of falling back to TraceIdRatioBasedSampler. Fixes #4127.
1 parent e649c7f commit 7a9d7a0

11 files changed

Lines changed: 761 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ Increment the:
5454
* [CODE HEALTH] Move func_grpc_main classes into anonymous namespace
5555
[#4129](https://github.com/open-telemetry/opentelemetry-cpp/pull/4129)
5656

57+
* [SDK] Implement the ProbabilitySampler
58+
[#4135](https://github.com/open-telemetry/opentelemetry-cpp/pull/4135)
59+
5760
## [1.27.0] 2026-05-13
5861

5962
* [RELEASE] Bump main branch to 1.27.0-dev

docs/public/sdk/GettingStarted.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,13 @@ Sampler
108108
^^^^^^^
109109

110110
Sampling is mechanism to control/reducing the number of samples of traces collected and sent to the backend.
111-
OpenTelemetry C++ SDK offers four samplers out of the box:
111+
OpenTelemetry C++ SDK offers five samplers out of the box:
112112

113113
- AlwaysOnSampler which samples every trace regardless of upstream sampling decisions.
114114
- AlwaysOffSampler which doesn’t sample any trace, regardless of upstream sampling decisions.
115115
- ParentBased which uses the parent span to make sampling decisions, if present.
116116
- TraceIdRatioBased which samples a configurable percentage of traces.
117+
- ProbabilitySampler which samples a configurable ratio of traces following the consistent probability sampling specification.
117118

118119
.. code:: cpp
119120
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#pragma once
5+
6+
#include <stdint.h>
7+
#include <string>
8+
9+
#include "opentelemetry/nostd/string_view.h"
10+
#include "opentelemetry/sdk/trace/sampler.h"
11+
#include "opentelemetry/trace/span_metadata.h"
12+
#include "opentelemetry/trace/trace_id.h"
13+
#include "opentelemetry/version.h"
14+
15+
OPENTELEMETRY_BEGIN_NAMESPACE
16+
namespace sdk
17+
{
18+
namespace trace
19+
{
20+
/**
21+
* The ProbabilitySampler computes and returns a decision based on the
22+
* span's 56-bit randomness value and the configured ratio, following the
23+
* consistent probability sampling specification.
24+
*/
25+
class ProbabilitySampler : public Sampler
26+
{
27+
public:
28+
/**
29+
* @param ratio a required value, 1.0 >= ratio >= 0.0. If the randomness
30+
* value of the span is greater than or equal to the rejection threshold
31+
* derived from the ratio, ShouldSample will return RECORD_AND_SAMPLE.
32+
*/
33+
explicit ProbabilitySampler(double ratio);
34+
35+
/**
36+
* @return Returns either RECORD_AND_SAMPLE or DROP based on the comparison
37+
* of the span's randomness value against the configured rejection
38+
* threshold. The parent SampledFlag is ignored.
39+
*/
40+
SamplingResult ShouldSample(
41+
const opentelemetry::trace::SpanContext &parent_context,
42+
opentelemetry::trace::TraceId trace_id,
43+
nostd::string_view /*name*/,
44+
opentelemetry::trace::SpanKind /*span_kind*/,
45+
const opentelemetry::common::KeyValueIterable & /*attributes*/,
46+
const opentelemetry::trace::SpanContextKeyValueIterable & /*links*/) noexcept override;
47+
48+
/**
49+
* @return Description MUST be ProbabilitySampler{0.000100}
50+
*/
51+
nostd::string_view GetDescription() const noexcept override;
52+
53+
private:
54+
std::string description_;
55+
const uint64_t threshold_;
56+
};
57+
} // namespace trace
58+
} // namespace sdk
59+
OPENTELEMETRY_END_NAMESPACE
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#pragma once
5+
6+
#include <memory>
7+
8+
#include "opentelemetry/sdk/trace/sampler.h"
9+
#include "opentelemetry/version.h"
10+
11+
OPENTELEMETRY_BEGIN_NAMESPACE
12+
namespace sdk
13+
{
14+
namespace trace
15+
{
16+
17+
/**
18+
* Factory class for ProbabilitySampler.
19+
*/
20+
class ProbabilitySamplerFactory
21+
{
22+
public:
23+
/**
24+
* Create a ProbabilitySampler.
25+
*/
26+
static std::unique_ptr<Sampler> Create(double ratio);
27+
};
28+
29+
} // namespace trace
30+
} // namespace sdk
31+
OPENTELEMETRY_END_NAMESPACE

sdk/src/configuration/sdk_builder.cc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
#include "opentelemetry/sdk/trace/samplers/always_off_factory.h"
169169
#include "opentelemetry/sdk/trace/samplers/always_on_factory.h"
170170
#include "opentelemetry/sdk/trace/samplers/parent_factory.h"
171+
#include "opentelemetry/sdk/trace/samplers/probability_factory.h"
171172
#include "opentelemetry/sdk/trace/samplers/trace_id_ratio_factory.h"
172173
#include "opentelemetry/sdk/trace/simple_processor_factory.h"
173174
#include "opentelemetry/sdk/trace/tracer_config.h"
@@ -388,7 +389,14 @@ class SamplerBuilder : public opentelemetry::sdk::configuration::SamplerConfigur
388389
const opentelemetry::sdk::configuration::ComposableProbabilitySamplerConfiguration *model)
389390
override
390391
{
391-
sampler = opentelemetry::sdk::trace::TraceIdRatioBasedSamplerFactory::Create(model->ratio);
392+
if (model->ratio > 0.0)
393+
{
394+
sampler = opentelemetry::sdk::trace::ProbabilitySamplerFactory::Create(model->ratio);
395+
}
396+
else
397+
{
398+
sampler = opentelemetry::sdk::trace::AlwaysOffSamplerFactory::Create();
399+
}
392400
}
393401

394402
void VisitComposableParentThreshold(

sdk/src/trace/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ add_library(
2020
samplers/always_off_factory.cc
2121
samplers/parent.cc
2222
samplers/parent_factory.cc
23+
samplers/probability.cc
24+
samplers/probability_factory.cc
2325
samplers/trace_id_ratio.cc
2426
samplers/trace_id_ratio_factory.cc
2527
multi_recordable.cc
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#include <atomic>
5+
#include <cmath>
6+
#include <cstdint>
7+
#include <string>
8+
9+
#include "opentelemetry/nostd/string_view.h"
10+
#include "opentelemetry/sdk/common/global_log_handler.h"
11+
#include "opentelemetry/sdk/trace/sampler.h"
12+
#include "opentelemetry/sdk/trace/samplers/probability.h"
13+
#include "opentelemetry/trace/span_context.h"
14+
#include "opentelemetry/trace/trace_id.h"
15+
#include "opentelemetry/trace/trace_state.h"
16+
#include "opentelemetry/version.h"
17+
18+
namespace trace_api = opentelemetry::trace;
19+
namespace nostd = opentelemetry::nostd;
20+
21+
namespace
22+
{
23+
constexpr char kOtTraceStateKey[] = "ot";
24+
25+
// Rejection threshold for a ratio of zero, 2^56: drops every span.
26+
constexpr uint64_t kMaxThreshold = 1ULL << 56;
27+
28+
/**
29+
* Converts a ratio in [0, 1] to a 56-bit rejection threshold.
30+
*/
31+
uint64_t CalculateRejectionThreshold(double ratio) noexcept
32+
{
33+
// The negated comparison also routes NaN to the never-sample threshold.
34+
if (!(ratio > 0.0))
35+
return kMaxThreshold;
36+
if (ratio >= 1.0)
37+
return 0;
38+
return kMaxThreshold -
39+
static_cast<uint64_t>(std::llround(ratio * static_cast<double>(kMaxThreshold)));
40+
}
41+
42+
bool ParseHex56(nostd::string_view hex, uint64_t *value) noexcept
43+
{
44+
if (hex.size() != 14)
45+
return false;
46+
uint64_t result = 0;
47+
for (char c : hex)
48+
{
49+
result <<= 4;
50+
if (c >= '0' && c <= '9')
51+
result |= static_cast<uint64_t>(c - '0');
52+
else if (c >= 'a' && c <= 'f')
53+
result |= static_cast<uint64_t>(c - 'a' + 10);
54+
else
55+
return false;
56+
}
57+
*value = result;
58+
return true;
59+
}
60+
61+
/**
62+
* Encodes a 56-bit threshold as lowercase hex with trailing zeros removed,
63+
* e.g. 2^55 becomes "8" and 0 becomes "0".
64+
*/
65+
std::string EncodeThreshold(uint64_t threshold)
66+
{
67+
if (threshold == 0)
68+
return "0";
69+
static const char kHex[] = "0123456789abcdef";
70+
std::string hex(14, '0');
71+
for (int i = 13; i >= 0; --i)
72+
{
73+
hex[i] = kHex[threshold & 0xf];
74+
threshold >>= 4;
75+
}
76+
hex.erase(hex.find_last_not_of('0') + 1);
77+
return hex;
78+
}
79+
80+
uint64_t RandomnessFromTraceId(const trace_api::TraceId &trace_id) noexcept
81+
{
82+
static_assert(trace_api::TraceId::kSize >= 7, "TraceID must be at least 7 bytes long.");
83+
84+
const uint8_t *data = trace_id.Id().data();
85+
uint64_t result = 0;
86+
for (size_t i = trace_api::TraceId::kSize - 7; i < trace_api::TraceId::kSize; ++i)
87+
{
88+
result = (result << 8) | data[i];
89+
}
90+
return result;
91+
}
92+
93+
// Returns true when the ot value carries a valid rv sub-key.
94+
bool ExplicitRandomness(nostd::string_view ot_value, uint64_t *randomness) noexcept
95+
{
96+
while (!ot_value.empty())
97+
{
98+
size_t end = ot_value.find(';');
99+
nostd::string_view sub_key = ot_value.substr(0, end);
100+
ot_value = end == nostd::string_view::npos ? "" : ot_value.substr(end + 1);
101+
102+
if (sub_key.substr(0, 3) == "rv:")
103+
return ParseHex56(sub_key.substr(3), randomness);
104+
}
105+
return false;
106+
}
107+
108+
// Sub-keys that are empty, lack a key:value separator, or duplicate th are dropped.
109+
std::string SetThresholdSubKey(nostd::string_view ot_value, const std::string &threshold)
110+
{
111+
std::string result;
112+
bool replaced = false;
113+
while (!ot_value.empty())
114+
{
115+
size_t end = ot_value.find(';');
116+
nostd::string_view sub_key = ot_value.substr(0, end);
117+
ot_value = end == nostd::string_view::npos ? "" : ot_value.substr(end + 1);
118+
119+
if (sub_key.empty() || sub_key.find(':') == nostd::string_view::npos)
120+
continue;
121+
if (sub_key.substr(0, 3) == "th:")
122+
{
123+
if (replaced)
124+
continue;
125+
replaced = true;
126+
if (!result.empty())
127+
result += ';';
128+
result += "th:" + threshold;
129+
continue;
130+
}
131+
if (!result.empty())
132+
result += ';';
133+
result.append(sub_key.data(), sub_key.size());
134+
}
135+
if (!replaced)
136+
{
137+
if (!result.empty())
138+
result += ';';
139+
result += "th:" + threshold;
140+
}
141+
return result;
142+
}
143+
} // namespace
144+
145+
OPENTELEMETRY_BEGIN_NAMESPACE
146+
namespace sdk
147+
{
148+
namespace trace
149+
{
150+
ProbabilitySampler::ProbabilitySampler(double ratio)
151+
: threshold_(CalculateRejectionThreshold(ratio))
152+
{
153+
if (!(ratio > 0.0))
154+
ratio = 0.0;
155+
if (ratio > 1.0)
156+
ratio = 1.0;
157+
description_ = "ProbabilitySampler{" + std::to_string(ratio) + "}";
158+
}
159+
160+
SamplingResult ProbabilitySampler::ShouldSample(
161+
const trace_api::SpanContext &parent_context,
162+
trace_api::TraceId trace_id,
163+
nostd::string_view /*name*/,
164+
trace_api::SpanKind /*span_kind*/,
165+
const opentelemetry::common::KeyValueIterable & /*attributes*/,
166+
const trace_api::SpanContextKeyValueIterable & /*links*/) noexcept
167+
{
168+
if (threshold_ == kMaxThreshold)
169+
return {Decision::DROP, nullptr, {}};
170+
171+
auto parent_trace_state = parent_context.trace_state();
172+
std::string ot_value;
173+
bool has_ot = parent_trace_state->Get(kOtTraceStateKey, ot_value);
174+
175+
uint64_t randomness = 0;
176+
if (!ExplicitRandomness(ot_value, &randomness))
177+
{
178+
if (parent_context.IsValid() && !parent_context.trace_flags().IsRandom())
179+
{
180+
static std::atomic<bool> warned{false};
181+
if (!warned.exchange(true))
182+
{
183+
OTEL_INTERNAL_LOG_WARN(
184+
"ProbabilitySampler presumes TraceID randomness, but the W3C random trace flag is "
185+
"not set. Upgrade the caller to W3C Trace Context Level 2.");
186+
}
187+
}
188+
randomness = RandomnessFromTraceId(trace_id);
189+
}
190+
191+
if (randomness < threshold_)
192+
return {Decision::DROP, nullptr, {}};
193+
194+
std::string new_ot_value = SetThresholdSubKey(ot_value, EncodeThreshold(threshold_));
195+
if (!trace_api::TraceState::IsValidValue(new_ot_value))
196+
return {Decision::RECORD_AND_SAMPLE, nullptr, {}};
197+
198+
if (!has_ot)
199+
{
200+
size_t entry_count = 0;
201+
parent_trace_state->GetAllEntries([&entry_count](nostd::string_view, nostd::string_view) {
202+
++entry_count;
203+
return true;
204+
});
205+
if (entry_count >= trace_api::TraceState::kMaxKeyValuePairs)
206+
return {Decision::RECORD_AND_SAMPLE, nullptr, {}};
207+
}
208+
209+
auto trace_state =
210+
has_ot ? parent_trace_state->Delete(kOtTraceStateKey)->Set(kOtTraceStateKey, new_ot_value)
211+
: parent_trace_state->Set(kOtTraceStateKey, new_ot_value);
212+
213+
return {Decision::RECORD_AND_SAMPLE, nullptr, trace_state};
214+
}
215+
216+
nostd::string_view ProbabilitySampler::GetDescription() const noexcept
217+
{
218+
return description_;
219+
}
220+
} // namespace trace
221+
} // namespace sdk
222+
OPENTELEMETRY_END_NAMESPACE
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#include "opentelemetry/sdk/trace/samplers/probability_factory.h"
5+
#include "opentelemetry/sdk/trace/samplers/probability.h"
6+
#include "opentelemetry/version.h"
7+
8+
OPENTELEMETRY_BEGIN_NAMESPACE
9+
namespace sdk
10+
{
11+
namespace trace
12+
{
13+
14+
std::unique_ptr<Sampler> ProbabilitySamplerFactory::Create(double ratio)
15+
{
16+
std::unique_ptr<Sampler> sampler(new ProbabilitySampler(ratio));
17+
return sampler;
18+
}
19+
20+
} // namespace trace
21+
} // namespace sdk
22+
OPENTELEMETRY_END_NAMESPACE

0 commit comments

Comments
 (0)