Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,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();
}
Comment on lines +392 to +399
}

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
223 changes: 223 additions & 0 deletions sdk/src/trace/samplers/probability.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// 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/shared_ptr.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
{
namespace
{
// Removes the inherited (now stale) "th" sub-key, keeping the other ot sub-keys.
nostd::shared_ptr<trace_api::TraceState> EraseThreshold(
const nostd::shared_ptr<trace_api::TraceState> &parent_trace_state,
const std::string &ot_value)
{
OtelTraceState ot_state = OtelTraceState::Parse(ot_value);
ot_state.has_threshold = false;
std::string stripped = ot_state.Serialize();
auto trace_state = parent_trace_state->Delete(kOtTraceStateKey);
if (!stripped.empty())
trace_state = trace_state->Set(kOtTraceStateKey, stripped);
return trace_state;
}
} // namespace

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);
Comment on lines +165 to +167

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)
{
return {Decision::DROP, nullptr, EraseThreshold(parent_trace_state, ot_value)};
}

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, EraseThreshold(parent_trace_state, ot_value)};

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