Skip to content

Commit 210adcb

Browse files
committed
reuse OtelTraceState for ot value handling in the probability sampler
Signed-off-by: ayush-that <ayush1337@hotmail.com>
1 parent 297388e commit 210adcb

2 files changed

Lines changed: 48 additions & 140 deletions

File tree

sdk/src/trace/samplers/probability.cc

Lines changed: 28 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
// Copyright The OpenTelemetry Authors
22
// SPDX-License-Identifier: Apache-2.0
33

4-
#include <stddef.h>
54
#include <atomic>
65
#include <cstdint>
76
#include <map>
8-
#include <memory>
97
#include <string>
108

11-
#include "opentelemetry/nostd/function_ref.h"
129
#include "opentelemetry/nostd/shared_ptr.h"
1310
#include "opentelemetry/nostd/string_view.h"
1411
#include "opentelemetry/sdk/common/global_log_handler.h"
@@ -23,7 +20,6 @@
2320
#include "ot_trace_state.h"
2421

2522
namespace trace_api = opentelemetry::trace;
26-
namespace nostd = opentelemetry::nostd;
2723

2824
namespace
2925
{
@@ -36,118 +32,13 @@ double ClampProbability(double ratio) noexcept
3632
return 1.0;
3733
return ratio;
3834
}
39-
40-
bool ParseHex56(nostd::string_view hex, uint64_t *value) noexcept
41-
{
42-
if (hex.size() != 14)
43-
return false;
44-
uint64_t result = 0;
45-
for (char c : hex)
46-
{
47-
result <<= 4;
48-
if (c >= '0' && c <= '9')
49-
result |= static_cast<uint64_t>(c - '0');
50-
else if (c >= 'a' && c <= 'f')
51-
result |= static_cast<uint64_t>(c - 'a' + 10);
52-
else
53-
return false;
54-
}
55-
*value = result;
56-
return true;
57-
}
58-
59-
/**
60-
* Encodes a 56-bit threshold as lowercase hex with trailing zeros removed,
61-
* e.g. 2^55 becomes "8" and 0 becomes "0".
62-
*/
63-
std::string EncodeThreshold(uint64_t threshold)
64-
{
65-
if (threshold == 0)
66-
return "0";
67-
static const char kHex[] = "0123456789abcdef";
68-
std::string hex(14, '0');
69-
for (int i = 13; i >= 0; --i)
70-
{
71-
hex[i] = kHex[threshold & 0xf];
72-
threshold >>= 4;
73-
}
74-
hex.erase(hex.find_last_not_of('0') + 1);
75-
return hex;
76-
}
77-
78-
// Returns true when the ot value carries a valid rv sub-key.
79-
bool ExplicitRandomness(nostd::string_view ot_value, uint64_t *randomness) noexcept
80-
{
81-
while (!ot_value.empty())
82-
{
83-
size_t end = ot_value.find(';');
84-
nostd::string_view sub_key = ot_value.substr(0, end);
85-
ot_value = end == nostd::string_view::npos ? "" : ot_value.substr(end + 1);
86-
87-
if (sub_key.substr(0, 3) == "rv:")
88-
return ParseHex56(sub_key.substr(3), randomness);
89-
}
90-
return false;
91-
}
92-
93-
// Sub-keys that are empty, lack a key:value separator, or duplicate th are dropped.
94-
std::string SetThresholdSubKey(nostd::string_view ot_value, const std::string &threshold)
95-
{
96-
std::string result;
97-
bool replaced = false;
98-
while (!ot_value.empty())
99-
{
100-
size_t end = ot_value.find(';');
101-
nostd::string_view sub_key = ot_value.substr(0, end);
102-
ot_value = end == nostd::string_view::npos ? "" : ot_value.substr(end + 1);
103-
104-
if (sub_key.empty() || sub_key.find(':') == nostd::string_view::npos)
105-
continue;
106-
if (sub_key.substr(0, 3) == "th:")
107-
{
108-
if (replaced)
109-
continue;
110-
replaced = true;
111-
if (!result.empty())
112-
result += ';';
113-
result += "th:" + threshold;
114-
continue;
115-
}
116-
if (!result.empty())
117-
result += ';';
118-
result.append(sub_key.data(), sub_key.size());
119-
}
120-
if (!replaced)
121-
{
122-
if (!result.empty())
123-
result += ';';
124-
result += "th:" + threshold;
125-
}
126-
return result;
127-
}
12835
} // namespace
12936

13037
OPENTELEMETRY_BEGIN_NAMESPACE
13138
namespace sdk
13239
{
13340
namespace trace
13441
{
135-
namespace
136-
{
137-
// Removes the inherited (now stale) "th" sub-key, keeping the other ot sub-keys.
138-
nostd::shared_ptr<trace_api::TraceState> EraseThreshold(
139-
const nostd::shared_ptr<trace_api::TraceState> &parent_trace_state,
140-
const std::string &ot_value)
141-
{
142-
OtelTraceState ot_state = OtelTraceState::Parse(ot_value);
143-
ot_state.has_threshold = false;
144-
std::string stripped = ot_state.Serialize();
145-
auto trace_state = parent_trace_state->Delete(kOtTraceStateKey);
146-
if (!stripped.empty())
147-
trace_state = trace_state->Set(kOtTraceStateKey, stripped);
148-
return trace_state;
149-
}
150-
} // namespace
15142

15243
ProbabilitySampler::ProbabilitySampler(double ratio)
15344
: description_("ProbabilitySampler{" + std::to_string(ClampProbability(ratio)) + "}"),
@@ -163,14 +54,26 @@ SamplingResult ProbabilitySampler::ShouldSample(
16354
const trace_api::SpanContextKeyValueIterable & /*links*/) noexcept
16455
{
16556
auto parent_trace_state = parent_context.trace_state();
57+
if (!parent_trace_state)
58+
{
59+
parent_trace_state = trace_api::TraceState::GetDefault();
60+
}
61+
16662
std::string ot_value;
167-
bool has_ot = parent_trace_state->Get(kOtTraceStateKey, ot_value);
63+
parent_trace_state->Get(kOtTraceStateKey, ot_value);
64+
OtelTraceState ot_state = OtelTraceState::Parse(ot_value);
16865

16966
bool drop = threshold_ == kMaxThreshold;
17067
if (!drop)
17168
{
17269
uint64_t randomness = 0;
173-
if (!ExplicitRandomness(ot_value, &randomness))
70+
if (ot_state.has_random_value)
71+
{
72+
// An explicit "rv" from an upstream Level 2 participant wins over the
73+
// trace id, keeping the sampling decision consistent across the trace.
74+
randomness = ot_state.random_value;
75+
}
76+
else
17477
{
17578
if (parent_context.IsValid() && !parent_context.trace_flags().IsRandom())
17679
{
@@ -187,31 +90,27 @@ SamplingResult ProbabilitySampler::ShouldSample(
18790
drop = randomness < threshold_;
18891
}
18992

93+
// Record the effective threshold when sampling; a dropped span carries no
94+
// probability, so its inherited (now stale) "th" must be erased. The "rv"
95+
// sub-key and any other "ot" sub-keys are preserved by OtelTraceState.
19096
if (drop)
19197
{
192-
return {Decision::DROP, nullptr, EraseThreshold(parent_trace_state, ot_value)};
98+
ot_state.has_threshold = false;
19399
}
194-
195-
std::string new_ot_value = SetThresholdSubKey(ot_value, EncodeThreshold(threshold_));
196-
if (!trace_api::TraceState::IsValidValue(new_ot_value))
197-
return {Decision::RECORD_AND_SAMPLE, nullptr, EraseThreshold(parent_trace_state, ot_value)};
198-
199-
if (!has_ot)
100+
else
200101
{
201-
size_t entry_count = 0;
202-
parent_trace_state->GetAllEntries([&entry_count](nostd::string_view, nostd::string_view) {
203-
++entry_count;
204-
return true;
205-
});
206-
if (entry_count >= trace_api::TraceState::kMaxKeyValuePairs)
207-
return {Decision::RECORD_AND_SAMPLE, nullptr, {}};
102+
ot_state.has_threshold = true;
103+
ot_state.threshold = threshold_;
208104
}
209105

210-
auto trace_state =
211-
has_ot ? parent_trace_state->Delete(kOtTraceStateKey)->Set(kOtTraceStateKey, new_ot_value)
212-
: parent_trace_state->Set(kOtTraceStateKey, new_ot_value);
106+
std::string new_ot_value = ot_state.Serialize();
107+
auto trace_state = parent_trace_state->Delete(kOtTraceStateKey);
108+
if (!new_ot_value.empty())
109+
{
110+
trace_state = trace_state->Set(kOtTraceStateKey, new_ot_value);
111+
}
213112

214-
return {Decision::RECORD_AND_SAMPLE, nullptr, trace_state};
113+
return {drop ? Decision::DROP : Decision::RECORD_AND_SAMPLE, nullptr, trace_state};
215114
}
216115

217116
nostd::string_view ProbabilitySampler::GetDescription() const noexcept

sdk/test/trace/probability_sampler_test.cc

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ TEST(ProbabilitySampler, ExplicitRandomnessTakesPrecedence)
177177

178178
std::string ot_value;
179179
ASSERT_TRUE(sampling_result.trace_state->Get("ot", ot_value));
180-
ASSERT_EQ("rv:ffffffffffffff;th:8", ot_value);
180+
ASSERT_EQ("th:8;rv:ffffffffffffff", ot_value);
181181

182182
// An rv of zero drops even when the trace id randomness would sample.
183183
trace_state = trace_api::TraceState::FromHeader("ot=rv:00000000000000");
@@ -302,7 +302,7 @@ TEST(ProbabilitySampler, MalformedSubKeysAreDropped)
302302
trace_api::SpanId span_id{span_id_buffer};
303303

304304
// Empty, key-only, and duplicate th sub-keys are dropped from the output.
305-
for (auto p : {std::make_pair("ot=a:1;;rv:ffffffffffffff", "a:1;rv:ffffffffffffff;th:8"),
305+
for (auto p : {std::make_pair("ot=a:1;;rv:ffffffffffffff", "th:8;rv:ffffffffffffff;a:1"),
306306
std::make_pair("ot=foo", "th:8"), std::make_pair("ot=th:1;th:2", "th:8")})
307307
{
308308
auto trace_state = trace_api::TraceState::FromHeader(p.first);
@@ -328,7 +328,7 @@ TEST(ProbabilitySampler, FullTraceStateKeepsParentTraceState)
328328
trace_api::SpanId span_id{span_id_buffer};
329329

330330
// 32 entries and no ot entry: there is no room to add th, so the span is
331-
// sampled but the trace state is left untouched.
331+
// sampled and the parent entries are preserved without an ot entry.
332332
std::string header = "k0=0";
333333
for (int i = 1; i < 32; ++i)
334334
{
@@ -340,10 +340,17 @@ TEST(ProbabilitySampler, FullTraceStateKeepsParentTraceState)
340340
auto sampling_result = SampleWithContext(s1, context, TraceIdWithRandomness(0xffffffffffffff));
341341

342342
ASSERT_EQ(Decision::RECORD_AND_SAMPLE, sampling_result.decision);
343-
ASSERT_EQ(nullptr, sampling_result.trace_state);
343+
ASSERT_NE(nullptr, sampling_result.trace_state);
344+
345+
std::string ot_value;
346+
ASSERT_FALSE(sampling_result.trace_state->Get("ot", ot_value));
347+
348+
std::string k0_value;
349+
ASSERT_TRUE(sampling_result.trace_state->Get("k0", k0_value));
350+
ASSERT_EQ("0", k0_value);
344351
}
345352

346-
TEST(ProbabilitySampler, OversizedValueKeepsParentTraceState)
353+
TEST(ProbabilitySampler, OversizedSubKeyDroppedToRecordThreshold)
347354
{
348355
ProbabilitySampler s1(0.5);
349356

@@ -352,7 +359,8 @@ TEST(ProbabilitySampler, OversizedValueKeepsParentTraceState)
352359
uint8_t span_id_buffer[trace_api::SpanId::kSize] = {1};
353360
trace_api::SpanId span_id{span_id_buffer};
354361

355-
// The oversized ot value is re-emitted unchanged because it has no th to erase.
362+
// A sampled span must record th; the foreign sub-key would push the ot value
363+
// past the 256 char limit, so it is dropped to make room for th.
356364
std::string ot_value = "a:" + std::string(253, 'b');
357365
auto trace_state = trace_api::TraceState::FromHeader("ot=" + ot_value);
358366
trace_api::SpanContext context(trace_id, span_id, trace_api::TraceFlags{0}, false, trace_state);
@@ -364,19 +372,20 @@ TEST(ProbabilitySampler, OversizedValueKeepsParentTraceState)
364372

365373
std::string result_ot;
366374
ASSERT_TRUE(sampling_result.trace_state->Get("ot", result_ot));
367-
ASSERT_EQ(ot_value, result_ot);
375+
ASSERT_EQ("th:8", result_ot);
368376
}
369377

370-
TEST(ProbabilitySampler, OversizedValueErasesStaleThreshold)
378+
TEST(ProbabilitySampler, OversizedSubKeyDroppedReplacesStaleThreshold)
371379
{
372-
// ratio 0.1's 17-char th overflows the 256-char limit, so the stale th is erased.
373380
ProbabilitySampler s1(0.1);
374381

375382
uint8_t trace_id_buffer[trace_api::TraceId::kSize] = {1};
376383
trace_api::TraceId trace_id{trace_id_buffer};
377384
uint8_t span_id_buffer[trace_api::SpanId::kSize] = {1};
378385
trace_api::SpanId span_id{span_id_buffer};
379386

387+
// The inherited th is replaced by this sampler's th; the foreign sub-key
388+
// overflows the 256 char limit and is dropped, keeping the fresh th.
380389
std::string rest = "x:" + std::string(245, 'b');
381390
auto trace_state = trace_api::TraceState::FromHeader("ot=th:8;" + rest);
382391
trace_api::SpanContext context(trace_id, span_id, trace_api::TraceFlags{0}, false, trace_state);
@@ -388,8 +397,8 @@ TEST(ProbabilitySampler, OversizedValueErasesStaleThreshold)
388397

389398
std::string result_ot;
390399
ASSERT_TRUE(sampling_result.trace_state->Get("ot", result_ot));
391-
ASSERT_EQ(rest, result_ot);
392-
ASSERT_EQ(std::string::npos, result_ot.find("th:"));
400+
ASSERT_NE(std::string::npos, result_ot.find("th:"));
401+
ASSERT_EQ(std::string::npos, result_ot.find("x:"));
393402
}
394403

395404
TEST(ProbabilitySampler, IgnoresParentSampledFlag)

0 commit comments

Comments
 (0)