Skip to content

Commit 4685f2b

Browse files
committed
[SDK] Implement the ProbabilitySampler
Fixes #4127.
1 parent c14bf11 commit 4685f2b

11 files changed

Lines changed: 768 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ Increment the:
101101
ParentThreshold, RuleBased)
102102
[#4028](https://github.com/open-telemetry/opentelemetry-cpp/issues/4028)
103103

104+
* [SDK] Implement the ProbabilitySampler
105+
[#4135](https://github.com/open-telemetry/opentelemetry-cpp/pull/4135)
106+
104107
## [1.27.0] 2026-05-13
105108

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

0 commit comments

Comments
 (0)