Skip to content
Open
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ Increment the:
ParentThreshold, RuleBased)
[#4028](https://github.com/open-telemetry/opentelemetry-cpp/issues/4028)

* [SDK] Implement the ProbabilitySampler
[#4135](https://github.com/open-telemetry/opentelemetry-cpp/pull/4135)

* [BUILD] Fix protobuf build failure
[#4154](https://github.com/open-telemetry/opentelemetry-cpp/pull/4154)

Expand Down
3 changes: 2 additions & 1 deletion docs/public/sdk/GettingStarted.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,13 @@ Sampler
^^^^^^^

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

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

.. code:: cpp

Expand Down
59 changes: 59 additions & 0 deletions sdk/include/opentelemetry/sdk/trace/samplers/probability.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <stdint.h>
#include <string>

#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/sdk/trace/sampler.h"
#include "opentelemetry/trace/span_metadata.h"
#include "opentelemetry/trace/trace_id.h"
#include "opentelemetry/version.h"

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace trace
{
/**
* The ProbabilitySampler computes and returns a decision based on the
* span's 56-bit randomness value and the configured ratio, following the
* consistent probability sampling specification.
*/
class ProbabilitySampler : public Sampler
{
public:
/**
* @param ratio a required value, 1.0 >= ratio >= 0.0. If the randomness
* value of the span is greater than or equal to the rejection threshold
* derived from the ratio, ShouldSample will return RECORD_AND_SAMPLE.
*/
explicit ProbabilitySampler(double ratio);

/**
* @return Returns either RECORD_AND_SAMPLE or DROP based on the comparison
* of the span's randomness value against the configured rejection
* threshold. The parent SampledFlag is ignored.
*/
SamplingResult ShouldSample(
const opentelemetry::trace::SpanContext &parent_context,
opentelemetry::trace::TraceId trace_id,
nostd::string_view /*name*/,
opentelemetry::trace::SpanKind /*span_kind*/,
const opentelemetry::common::KeyValueIterable & /*attributes*/,
const opentelemetry::trace::SpanContextKeyValueIterable & /*links*/) noexcept override;

/**
* @return Description MUST be ProbabilitySampler{0.000100}
*/
nostd::string_view GetDescription() const noexcept override;

private:
std::string description_;
const uint64_t threshold_;
};
} // namespace trace
} // namespace sdk
OPENTELEMETRY_END_NAMESPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <memory>

#include "opentelemetry/sdk/trace/sampler.h"
#include "opentelemetry/version.h"

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace trace
{

/**
* Factory class for ProbabilitySampler.
*/
class ProbabilitySamplerFactory
{
public:
/**
* Create a ProbabilitySampler.
*/
static std::unique_ptr<Sampler> Create(double ratio);
};

} // namespace trace
} // namespace sdk
OPENTELEMETRY_END_NAMESPACE
10 changes: 9 additions & 1 deletion sdk/src/configuration/sdk_builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
#include "opentelemetry/sdk/trace/samplers/always_off_factory.h"
#include "opentelemetry/sdk/trace/samplers/always_on_factory.h"
#include "opentelemetry/sdk/trace/samplers/parent_factory.h"
#include "opentelemetry/sdk/trace/samplers/probability_factory.h"
#include "opentelemetry/sdk/trace/samplers/trace_id_ratio_factory.h"
#include "opentelemetry/sdk/trace/simple_processor_factory.h"
#include "opentelemetry/sdk/trace/tracer_config.h"
Expand Down Expand Up @@ -388,7 +389,14 @@ class SamplerBuilder : public opentelemetry::sdk::configuration::SamplerConfigur
const opentelemetry::sdk::configuration::ComposableProbabilitySamplerConfiguration *model)
override
{
sampler = opentelemetry::sdk::trace::TraceIdRatioBasedSamplerFactory::Create(model->ratio);
if (model->ratio > 0.0)
{
sampler = opentelemetry::sdk::trace::ProbabilitySamplerFactory::Create(model->ratio);
}
else
{
sampler = opentelemetry::sdk::trace::AlwaysOffSamplerFactory::Create();
}
}

void VisitComposableParentThreshold(
Expand Down
2 changes: 2 additions & 0 deletions sdk/src/trace/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ add_library(
samplers/always_off_factory.cc
samplers/parent.cc
samplers/parent_factory.cc
samplers/probability.cc
samplers/probability_factory.cc
samplers/trace_id_ratio.cc
samplers/trace_id_ratio_factory.cc
samplers/ot_trace_state.cc
Expand Down
211 changes: 211 additions & 0 deletions sdk/src/trace/samplers/probability.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#include <stddef.h>
#include <atomic>
#include <cstdint>
#include <map>
#include <memory>
#include <string>

#include "opentelemetry/nostd/function_ref.h"
#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/sdk/common/global_log_handler.h"
#include "opentelemetry/sdk/trace/sampler.h"
#include "opentelemetry/sdk/trace/samplers/probability.h"
#include "opentelemetry/trace/span_context.h"
#include "opentelemetry/trace/trace_flags.h"
#include "opentelemetry/trace/trace_id.h"
#include "opentelemetry/trace/trace_state.h"
#include "opentelemetry/version.h"

#include "ot_trace_state.h"

namespace trace_api = opentelemetry::trace;
namespace nostd = opentelemetry::nostd;

namespace
{
// Clamps ratio to [0, 1]. NaN and negatives map to 0 (never-sample).
double ClampProbability(double ratio) noexcept
{
if (!(ratio > 0.0))
return 0.0;
if (ratio > 1.0)
return 1.0;
return ratio;
}

bool ParseHex56(nostd::string_view hex, uint64_t *value) noexcept
{
if (hex.size() != 14)
return false;
uint64_t result = 0;
for (char c : hex)
{
result <<= 4;
if (c >= '0' && c <= '9')
result |= static_cast<uint64_t>(c - '0');
else if (c >= 'a' && c <= 'f')
result |= static_cast<uint64_t>(c - 'a' + 10);
else
return false;
}
*value = result;
return true;
}

/**
* Encodes a 56-bit threshold as lowercase hex with trailing zeros removed,
* e.g. 2^55 becomes "8" and 0 becomes "0".
*/
std::string EncodeThreshold(uint64_t threshold)
{
if (threshold == 0)
return "0";
static const char kHex[] = "0123456789abcdef";
std::string hex(14, '0');
for (int i = 13; i >= 0; --i)
{
hex[i] = kHex[threshold & 0xf];
threshold >>= 4;
}
hex.erase(hex.find_last_not_of('0') + 1);
return hex;
}

// Returns true when the ot value carries a valid rv sub-key.
bool ExplicitRandomness(nostd::string_view ot_value, uint64_t *randomness) noexcept
{
while (!ot_value.empty())
{
size_t end = ot_value.find(';');
nostd::string_view sub_key = ot_value.substr(0, end);
ot_value = end == nostd::string_view::npos ? "" : ot_value.substr(end + 1);

if (sub_key.substr(0, 3) == "rv:")
return ParseHex56(sub_key.substr(3), randomness);
}
return false;
}

// Sub-keys that are empty, lack a key:value separator, or duplicate th are dropped.
std::string SetThresholdSubKey(nostd::string_view ot_value, const std::string &threshold)
{
std::string result;
bool replaced = false;
while (!ot_value.empty())
{
size_t end = ot_value.find(';');
nostd::string_view sub_key = ot_value.substr(0, end);
ot_value = end == nostd::string_view::npos ? "" : ot_value.substr(end + 1);

if (sub_key.empty() || sub_key.find(':') == nostd::string_view::npos)
continue;
if (sub_key.substr(0, 3) == "th:")
{
if (replaced)
continue;
replaced = true;
if (!result.empty())
result += ';';
result += "th:" + threshold;
continue;
}
if (!result.empty())
result += ';';
result.append(sub_key.data(), sub_key.size());
}
if (!replaced)
{
if (!result.empty())
result += ';';
result += "th:" + threshold;
}
return result;
}
} // namespace

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace trace
{
ProbabilitySampler::ProbabilitySampler(double ratio)
: description_("ProbabilitySampler{" + std::to_string(ClampProbability(ratio)) + "}"),
threshold_(CalculateThreshold(ClampProbability(ratio)))
{}

SamplingResult ProbabilitySampler::ShouldSample(
const trace_api::SpanContext &parent_context,
trace_api::TraceId trace_id,
nostd::string_view /*name*/,
trace_api::SpanKind /*span_kind*/,
const opentelemetry::common::KeyValueIterable & /*attributes*/,
const trace_api::SpanContextKeyValueIterable & /*links*/) noexcept
{
auto parent_trace_state = parent_context.trace_state();
std::string ot_value;
bool has_ot = parent_trace_state->Get(kOtTraceStateKey, ot_value);

bool drop = threshold_ == kMaxThreshold;
if (!drop)
{
uint64_t randomness = 0;
if (!ExplicitRandomness(ot_value, &randomness))
{
if (parent_context.IsValid() && !parent_context.trace_flags().IsRandom())
{
static std::atomic<bool> warned{false};

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.

Non-blocking: probably sample on the warning instead of just showing the first warning.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Kept it once-per-process to stay off the per-span hot path.

if (!warned.exchange(true))
{
OTEL_INTERNAL_LOG_WARN(
"ProbabilitySampler presumes TraceID randomness, but the W3C random trace flag is "
"not set. Upgrade the caller to W3C Trace Context Level 2.");
}
}
randomness = GetRandomnessFromTraceId(trace_id);
}
drop = randomness < threshold_;
}

if (drop)
{
OtelTraceState ot_state = OtelTraceState::Parse(ot_value);
ot_state.has_threshold = false;
std::string dropped_ot = ot_state.Serialize();
auto trace_state = parent_trace_state->Delete(kOtTraceStateKey);
if (!dropped_ot.empty())
trace_state = trace_state->Set(kOtTraceStateKey, dropped_ot);
return {Decision::DROP, nullptr, trace_state};
}

std::string new_ot_value = SetThresholdSubKey(ot_value, EncodeThreshold(threshold_));
if (!trace_api::TraceState::IsValidValue(new_ot_value))

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.

Same stale-th class of issue you fixed for the drop path can still occur on the sample path: when IsValidValue(new_ot_value) is false or the tracestate is already full, we return an empty trace_state, so the child inherits the parent's (possibly stale) ot=th. Should we strip the parent th (reusing the drop-path OtelTraceState logic) before returning RECORD_AND_SAMPLE in those branches?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes. Reused the drop-path strip on the invalid-ot branch. The full branch is !has_ot, so nothing stale there. Added a test.

return {Decision::RECORD_AND_SAMPLE, nullptr, {}};

if (!has_ot)
{
size_t entry_count = 0;
parent_trace_state->GetAllEntries([&entry_count](nostd::string_view, nostd::string_view) {
++entry_count;
return true;
});
if (entry_count >= trace_api::TraceState::kMaxKeyValuePairs)
return {Decision::RECORD_AND_SAMPLE, nullptr, {}};
}

auto trace_state =
has_ot ? parent_trace_state->Delete(kOtTraceStateKey)->Set(kOtTraceStateKey, new_ot_value)
: parent_trace_state->Set(kOtTraceStateKey, new_ot_value);

return {Decision::RECORD_AND_SAMPLE, nullptr, trace_state};
}

nostd::string_view ProbabilitySampler::GetDescription() const noexcept
{
return description_;
}
} // namespace trace
} // namespace sdk
OPENTELEMETRY_END_NAMESPACE
22 changes: 22 additions & 0 deletions sdk/src/trace/samplers/probability_factory.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#include "opentelemetry/sdk/trace/samplers/probability_factory.h"
#include "opentelemetry/sdk/trace/samplers/probability.h"
#include "opentelemetry/version.h"

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace trace
{

std::unique_ptr<Sampler> ProbabilitySamplerFactory::Create(double ratio)
{
std::unique_ptr<Sampler> sampler(new ProbabilitySampler(ratio));
return sampler;
}

} // namespace trace
} // namespace sdk
OPENTELEMETRY_END_NAMESPACE
Loading
Loading