|
| 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