use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SamplingDecision {
RecordAndSample,
NotRecord,
}
impl SamplingDecision {
pub fn is_sampled(&self) -> bool {
matches!(self, SamplingDecision::RecordAndSample)
}
pub fn as_trace_flags(&self) -> &'static str {
match self {
SamplingDecision::RecordAndSample => "01",
SamplingDecision::NotRecord => "00",
}
}
}
pub trait Sampler: Send + Sync {
fn should_sample(&self, trace_id: &str, parent_sampled: Option<bool>) -> SamplingDecision;
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone, Default)]
pub struct AlwaysOnSampler;
impl AlwaysOnSampler {
pub fn new() -> Self {
Self
}
}
impl Sampler for AlwaysOnSampler {
fn should_sample(&self, _trace_id: &str, _parent_sampled: Option<bool>) -> SamplingDecision {
SamplingDecision::RecordAndSample
}
fn name(&self) -> &'static str {
"always_on"
}
}
#[derive(Debug, Clone, Default)]
pub struct AlwaysOffSampler;
impl AlwaysOffSampler {
pub fn new() -> Self {
Self
}
}
impl Sampler for AlwaysOffSampler {
fn should_sample(&self, _trace_id: &str, _parent_sampled: Option<bool>) -> SamplingDecision {
SamplingDecision::NotRecord
}
fn name(&self) -> &'static str {
"always_off"
}
}
pub struct TraceIdRatioSampler {
ratio: f64,
threshold: u64,
total: AtomicU64,
sampled: AtomicU64,
}
impl TraceIdRatioSampler {
pub fn new(ratio: f64) -> Self {
assert!(
(0.0..=1.0).contains(&ratio),
"sampling ratio must be in [0.0, 1.0], got {ratio}"
);
Self {
ratio,
threshold: (ratio * u64::MAX as f64) as u64,
total: AtomicU64::new(0),
sampled: AtomicU64::new(0),
}
}
pub fn ratio(&self) -> f64 {
self.ratio
}
pub fn total_decisions(&self) -> u64 {
self.total.load(Ordering::Relaxed)
}
pub fn sampled_count(&self) -> u64 {
self.sampled.load(Ordering::Relaxed)
}
pub fn actual_rate(&self) -> f64 {
let total = self.total_decisions();
if total == 0 {
return 0.0;
}
self.sampled_count() as f64 / total as f64
}
fn hash_trace_id(trace_id: &str) -> u64 {
const FNV_OFFSET: u64 = 14695981039346656037;
const FNV_PRIME: u64 = 1099511628211;
let mut hash = FNV_OFFSET;
for byte in trace_id.as_bytes() {
hash ^= *byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash ^= hash >> 33;
hash = hash.wrapping_mul(0xff51_afd7_ed55_8ccd);
hash ^= hash >> 33;
hash = hash.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
hash ^= hash >> 33;
hash
}
}
impl Sampler for TraceIdRatioSampler {
fn should_sample(&self, trace_id: &str, _parent_sampled: Option<bool>) -> SamplingDecision {
self.total.fetch_add(1, Ordering::Relaxed);
let hash = Self::hash_trace_id(trace_id);
if hash <= self.threshold {
self.sampled.fetch_add(1, Ordering::Relaxed);
SamplingDecision::RecordAndSample
} else {
SamplingDecision::NotRecord
}
}
fn name(&self) -> &'static str {
"trace_id_ratio"
}
}
pub struct ParentBasedSampler {
root: Box<dyn Sampler>,
}
impl ParentBasedSampler {
pub fn new(root: Box<dyn Sampler>) -> Self {
Self { root }
}
pub fn always_on_root() -> Self {
Self::new(Box::new(AlwaysOnSampler::new()))
}
pub fn ratio_root(ratio: f64) -> Self {
Self::new(Box::new(TraceIdRatioSampler::new(ratio)))
}
}
impl Sampler for ParentBasedSampler {
fn should_sample(&self, trace_id: &str, parent_sampled: Option<bool>) -> SamplingDecision {
match parent_sampled {
None => self.root.should_sample(trace_id, None),
Some(sampled) => {
if sampled {
SamplingDecision::RecordAndSample
} else {
SamplingDecision::NotRecord
}
}
}
}
fn name(&self) -> &'static str {
"parent_based"
}
}
#[derive(Debug, Clone, Default)]
pub struct Baggage {
entries: HashMap<String, String>,
}
impl Baggage {
pub fn new() -> Self {
Self::default()
}
pub fn from_pairs(
pairs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> Self {
let mut baggage = Self::new();
for (k, v) in pairs {
baggage.set(k, v);
}
baggage
}
pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.entries.insert(key.into(), value.into());
}
pub fn get(&self, key: &str) -> Option<&str> {
self.entries.get(key).map(|s| s.as_str())
}
pub fn remove(&mut self, key: &str) -> Option<String> {
self.entries.remove(key)
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn keys(&self) -> Vec<&str> {
self.entries.keys().map(|s| s.as_str()).collect()
}
pub fn to_header(&self) -> String {
let mut pairs: Vec<(String, String)> = self
.entries
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
pairs
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(",")
}
pub fn from_header(header: &str) -> Self {
let mut baggage = Self::new();
for entry in header.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
if let Some(eq_pos) = entry.find('=') {
let key = entry[..eq_pos].trim().to_string();
let value = entry[eq_pos + 1..].trim().to_string();
if !key.is_empty() && !value.is_empty() {
baggage.entries.insert(key, value);
}
}
}
baggage
}
pub fn merge(&mut self, other: &Baggage) {
for (k, v) in &other.entries {
self.entries.insert(k.clone(), v.clone());
}
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
pub struct BaggagePropagator;
impl BaggagePropagator {
pub fn new() -> Self {
Self
}
pub fn inject(&self, baggage: &Baggage, headers: &mut HashMap<String, String>) {
let header_value = baggage.to_header();
if !header_value.is_empty() {
headers.insert("baggage".to_string(), header_value);
}
}
pub fn extract(&self, headers: &HashMap<String, String>) -> Baggage {
headers
.get("baggage")
.map(|h| Baggage::from_header(h))
.unwrap_or_default()
}
}
impl Default for BaggagePropagator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct BatchConfig {
pub max_batch_size: usize,
pub export_interval_ms: u64,
pub max_queue_size: usize,
}
impl Default for BatchConfig {
fn default() -> Self {
Self {
max_batch_size: 512,
export_interval_ms: 5000,
max_queue_size: 2048,
}
}
}
pub struct BatchSpanExporter {
config: BatchConfig,
queue: Arc<RwLock<Vec<crate::Span>>>,
exported: Arc<RwLock<Vec<Vec<crate::Span>>>>,
dropped: AtomicU64,
}
impl BatchSpanExporter {
pub fn new(config: BatchConfig) -> Self {
Self {
config,
queue: Arc::new(RwLock::new(Vec::new())),
exported: Arc::new(RwLock::new(Vec::new())),
dropped: AtomicU64::new(0),
}
}
pub fn enqueue(&self, span: crate::Span) {
let mut queue = self.queue.write();
if queue.len() >= self.config.max_queue_size {
queue.remove(0);
self.dropped.fetch_add(1, Ordering::Relaxed);
}
queue.push(span);
}
pub fn flush_batch(&self) -> usize {
let mut queue = self.queue.write();
if queue.len() < self.config.max_batch_size {
return 0;
}
let batch: Vec<crate::Span> = queue.drain(..self.config.max_batch_size).collect();
let count = batch.len();
let mut exported = self.exported.write();
exported.push(batch);
count
}
pub fn flush_all(&self) -> usize {
let mut queue = self.queue.write();
if queue.is_empty() {
return 0;
}
let batch: Vec<crate::Span> = queue.drain(..).collect();
let count = batch.len();
let mut exported = self.exported.write();
exported.push(batch);
count
}
pub fn exported_batch_count(&self) -> usize {
self.exported.read().len()
}
pub fn exported_span_count(&self) -> usize {
self.exported.read().iter().map(|b| b.len()).sum()
}
pub fn queue_len(&self) -> usize {
self.queue.read().len()
}
pub fn dropped_count(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
pub fn clear(&self) {
self.queue.write().clear();
self.exported.write().clear();
self.dropped.store(0, Ordering::Relaxed);
}
pub fn config(&self) -> &BatchConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sampling_decision_is_sampled() {
assert!(SamplingDecision::RecordAndSample.is_sampled());
assert!(!SamplingDecision::NotRecord.is_sampled());
}
#[test]
fn test_sampling_decision_as_trace_flags() {
assert_eq!(SamplingDecision::RecordAndSample.as_trace_flags(), "01");
assert_eq!(SamplingDecision::NotRecord.as_trace_flags(), "00");
}
#[test]
fn test_sampling_decision_equality() {
assert_eq!(
SamplingDecision::RecordAndSample,
SamplingDecision::RecordAndSample
);
assert_ne!(
SamplingDecision::RecordAndSample,
SamplingDecision::NotRecord
);
}
#[test]
fn test_always_on_sampler_returns_sampled() {
let sampler = AlwaysOnSampler::new();
let decision = sampler.should_sample("trace123", None);
assert_eq!(decision, SamplingDecision::RecordAndSample);
}
#[test]
fn test_always_on_sampler_ignores_parent() {
let sampler = AlwaysOnSampler::new();
assert!(sampler.should_sample("t", Some(false)).is_sampled());
assert!(sampler.should_sample("t", Some(true)).is_sampled());
}
#[test]
fn test_always_on_sampler_name() {
let sampler = AlwaysOnSampler::new();
assert_eq!(sampler.name(), "always_on");
}
#[test]
fn test_always_off_sampler_returns_not_sampled() {
let sampler = AlwaysOffSampler::new();
let decision = sampler.should_sample("trace123", None);
assert_eq!(decision, SamplingDecision::NotRecord);
}
#[test]
fn test_always_off_sampler_name() {
let sampler = AlwaysOffSampler::new();
assert_eq!(sampler.name(), "always_off");
}
#[test]
fn test_trace_id_ratio_sampler_full_sampling() {
let sampler = TraceIdRatioSampler::new(1.0);
for i in 0..100 {
let trace_id = format!("{:032x}", i);
assert!(sampler.should_sample(&trace_id, None).is_sampled());
}
assert_eq!(sampler.sampled_count(), 100);
assert_eq!(sampler.total_decisions(), 100);
}
#[test]
fn test_trace_id_ratio_sampler_zero_sampling() {
let sampler = TraceIdRatioSampler::new(0.0);
for i in 0..100 {
let trace_id = format!("{:032x}", i);
assert!(!sampler.should_sample(&trace_id, None).is_sampled());
}
assert_eq!(sampler.sampled_count(), 0);
}
#[test]
fn test_trace_id_ratio_sampler_deterministic() {
let sampler = TraceIdRatioSampler::new(0.5);
let trace_id = "abcdef0123456789abcdef0123456789";
let d1 = sampler.should_sample(trace_id, None);
let d2 = sampler.should_sample(trace_id, None);
let d3 = sampler.should_sample(trace_id, None);
assert_eq!(d1, d2);
assert_eq!(d2, d3);
}
#[test]
fn test_trace_id_ratio_sampler_half_ratio_approximate() {
let sampler = TraceIdRatioSampler::new(0.5);
for i in 0..10000 {
let trace_id = format!("{:032x}", i);
sampler.should_sample(&trace_id, None);
}
let rate = sampler.actual_rate();
assert!((rate - 0.5).abs() < 0.05, "expected ~0.5, got {rate}");
}
#[test]
fn test_trace_id_ratio_sampler_stats() {
let sampler = TraceIdRatioSampler::new(0.3);
for i in 0..1000 {
let trace_id = format!("{:032x}", i);
sampler.should_sample(&trace_id, None);
}
assert_eq!(sampler.total_decisions(), 1000);
assert!(sampler.sampled_count() > 0);
let rate = sampler.actual_rate();
assert!((rate - 0.3).abs() < 0.05, "expected ~0.3, got {rate}");
}
#[test]
fn test_trace_id_ratio_sampler_name() {
let sampler = TraceIdRatioSampler::new(1.0);
assert_eq!(sampler.name(), "trace_id_ratio");
}
#[test]
fn test_trace_id_ratio_sampler_ratio_accessor() {
let sampler = TraceIdRatioSampler::new(0.75);
assert!((sampler.ratio() - 0.75).abs() < 1e-9);
}
#[test]
#[should_panic(expected = "sampling ratio must be in [0.0, 1.0]")]
fn test_trace_id_ratio_sampler_invalid_ratio_high() {
TraceIdRatioSampler::new(1.5);
}
#[test]
#[should_panic(expected = "sampling ratio must be in [0.0, 1.0]")]
fn test_trace_id_ratio_sampler_invalid_ratio_negative() {
TraceIdRatioSampler::new(-0.1);
}
#[test]
fn test_parent_based_root_uses_inner_sampler() {
let sampler = ParentBasedSampler::ratio_root(1.0);
assert!(sampler.should_sample("trace1", None).is_sampled());
}
#[test]
fn test_parent_based_follows_sampled_parent() {
let sampler = ParentBasedSampler::always_on_root();
assert!(sampler.should_sample("t", Some(true)).is_sampled());
}
#[test]
fn test_parent_based_follows_unsampled_parent() {
let sampler = ParentBasedSampler::always_on_root();
assert!(!sampler.should_sample("t", Some(false)).is_sampled());
}
#[test]
fn test_parent_based_name() {
let sampler = ParentBasedSampler::always_on_root();
assert_eq!(sampler.name(), "parent_based");
}
#[test]
fn test_baggage_new_empty() {
let b = Baggage::new();
assert!(b.is_empty());
assert_eq!(b.len(), 0);
}
#[test]
fn test_baggage_set_and_get() {
let mut b = Baggage::new();
b.set("user_id", "12345");
assert_eq!(b.get("user_id"), Some("12345"));
assert_eq!(b.get("missing"), None);
}
#[test]
fn test_baggage_set_overwrites() {
let mut b = Baggage::new();
b.set("key", "v1");
b.set("key", "v2");
assert_eq!(b.get("key"), Some("v2"));
assert_eq!(b.len(), 1);
}
#[test]
fn test_baggage_remove() {
let mut b = Baggage::new();
b.set("key", "value");
assert_eq!(b.remove("key"), Some("value".to_string()));
assert!(b.is_empty());
assert_eq!(b.remove("key"), None);
}
#[test]
fn test_baggage_from_pairs() {
let b = Baggage::from_pairs([("a", "1"), ("b", "2")]);
assert_eq!(b.len(), 2);
assert_eq!(b.get("a"), Some("1"));
assert_eq!(b.get("b"), Some("2"));
}
#[test]
fn test_baggage_to_header_single() {
let mut b = Baggage::new();
b.set("key", "value");
assert_eq!(b.to_header(), "key=value");
}
#[test]
fn test_baggage_to_header_multiple_sorted() {
let mut b = Baggage::new();
b.set("zebra", "1");
b.set("alpha", "2");
assert_eq!(b.to_header(), "alpha=2,zebra=1");
}
#[test]
fn test_baggage_to_header_empty() {
let b = Baggage::new();
assert_eq!(b.to_header(), "");
}
#[test]
fn test_baggage_from_header_single() {
let b = Baggage::from_header("key=value");
assert_eq!(b.get("key"), Some("value"));
}
#[test]
fn test_baggage_from_header_multiple() {
let b = Baggage::from_header("a=1,b=2,c=3");
assert_eq!(b.len(), 3);
assert_eq!(b.get("a"), Some("1"));
assert_eq!(b.get("b"), Some("2"));
assert_eq!(b.get("c"), Some("3"));
}
#[test]
fn test_baggage_from_header_with_spaces() {
let b = Baggage::from_header(" key = value1 , b = value2 ");
assert_eq!(b.get("key"), Some("value1"));
assert_eq!(b.get("b"), Some("value2"));
}
#[test]
fn test_baggage_from_header_empty() {
let b = Baggage::from_header("");
assert!(b.is_empty());
}
#[test]
fn test_baggage_from_header_ignores_malformed() {
let b = Baggage::from_header("valid=1,invalid,=nokey,novalue=,good=2");
assert_eq!(b.get("valid"), Some("1"));
assert_eq!(b.get("good"), Some("2"));
assert_eq!(b.len(), 2);
}
#[test]
fn test_baggage_roundtrip() {
let mut original = Baggage::new();
original.set("user_id", "12345");
original.set("request_id", "abc");
original.set("locale", "zh-CN");
let header = original.to_header();
let parsed = Baggage::from_header(&header);
assert_eq!(parsed.len(), original.len());
for key in original.keys() {
assert_eq!(parsed.get(key), original.get(key));
}
}
#[test]
fn test_baggage_merge() {
let mut b1 = Baggage::new();
b1.set("a", "1");
b1.set("b", "2");
let mut b2 = Baggage::new();
b2.set("b", "override");
b2.set("c", "3");
b1.merge(&b2);
assert_eq!(b1.get("a"), Some("1"));
assert_eq!(b1.get("b"), Some("override"));
assert_eq!(b1.get("c"), Some("3"));
}
#[test]
fn test_baggage_clear() {
let mut b = Baggage::new();
b.set("a", "1");
b.set("b", "2");
b.clear();
assert!(b.is_empty());
}
#[test]
fn test_baggage_keys() {
let mut b = Baggage::new();
b.set("x", "1");
b.set("y", "2");
let mut keys = b.keys();
keys.sort();
assert_eq!(keys, vec!["x", "y"]);
}
#[test]
fn test_propagator_inject_and_extract() {
let propagator = BaggagePropagator::new();
let mut baggage = Baggage::new();
baggage.set("user_id", "123");
baggage.set("locale", "en");
let mut headers = HashMap::new();
propagator.inject(&baggage, &mut headers);
assert!(headers.contains_key("baggage"));
let extracted = propagator.extract(&headers);
assert_eq!(extracted.get("user_id"), Some("123"));
assert_eq!(extracted.get("locale"), Some("en"));
}
#[test]
fn test_propagator_extract_empty_headers() {
let propagator = BaggagePropagator::new();
let headers = HashMap::new();
let baggage = propagator.extract(&headers);
assert!(baggage.is_empty());
}
#[test]
fn test_propagator_inject_empty_baggage() {
let propagator = BaggagePropagator::new();
let baggage = Baggage::new();
let mut headers = HashMap::new();
propagator.inject(&baggage, &mut headers);
assert!(!headers.contains_key("baggage"));
}
#[test]
fn test_propagator_roundtrip_multiple_entries() {
let propagator = BaggagePropagator::new();
let mut original = Baggage::new();
original.set("a", "1");
original.set("b", "2");
original.set("c", "3");
let mut headers = HashMap::new();
propagator.inject(&original, &mut headers);
let extracted = propagator.extract(&headers);
assert_eq!(extracted.len(), 3);
assert_eq!(extracted.get("a"), Some("1"));
assert_eq!(extracted.get("b"), Some("2"));
assert_eq!(extracted.get("c"), Some("3"));
}
fn make_span(id: &str) -> crate::Span {
crate::Span::new("trace", id, "operation")
}
#[test]
fn test_batch_exporter_new_empty() {
let exporter = BatchSpanExporter::new(BatchConfig::default());
assert_eq!(exporter.queue_len(), 0);
assert_eq!(exporter.exported_batch_count(), 0);
assert_eq!(exporter.exported_span_count(), 0);
assert_eq!(exporter.dropped_count(), 0);
}
#[test]
fn test_batch_exporter_enqueue() {
let exporter = BatchSpanExporter::new(BatchConfig::default());
exporter.enqueue(make_span("span1"));
exporter.enqueue(make_span("span2"));
assert_eq!(exporter.queue_len(), 2);
}
#[test]
fn test_batch_exporter_flush_batch_below_threshold() {
let config = BatchConfig {
max_batch_size: 10,
..Default::default()
};
let exporter = BatchSpanExporter::new(config);
exporter.enqueue(make_span("s1"));
exporter.enqueue(make_span("s2"));
let exported = exporter.flush_batch();
assert_eq!(exported, 0);
assert_eq!(exporter.queue_len(), 2);
}
#[test]
fn test_batch_exporter_flush_batch_at_threshold() {
let config = BatchConfig {
max_batch_size: 3,
..Default::default()
};
let exporter = BatchSpanExporter::new(config);
exporter.enqueue(make_span("s1"));
exporter.enqueue(make_span("s2"));
exporter.enqueue(make_span("s3"));
let exported = exporter.flush_batch();
assert_eq!(exported, 3);
assert_eq!(exporter.queue_len(), 0);
assert_eq!(exporter.exported_batch_count(), 1);
assert_eq!(exporter.exported_span_count(), 3);
}
#[test]
fn test_batch_exporter_flush_all() {
let exporter = BatchSpanExporter::new(BatchConfig::default());
exporter.enqueue(make_span("s1"));
exporter.enqueue(make_span("s2"));
let exported = exporter.flush_all();
assert_eq!(exported, 2);
assert_eq!(exporter.queue_len(), 0);
assert_eq!(exporter.exported_batch_count(), 1);
}
#[test]
fn test_batch_exporter_flush_all_empty() {
let exporter = BatchSpanExporter::new(BatchConfig::default());
let exported = exporter.flush_all();
assert_eq!(exported, 0);
}
#[test]
fn test_batch_exporter_drops_when_queue_full() {
let config = BatchConfig {
max_batch_size: 100, max_queue_size: 3,
..Default::default()
};
let exporter = BatchSpanExporter::new(config);
exporter.enqueue(make_span("s1"));
exporter.enqueue(make_span("s2"));
exporter.enqueue(make_span("s3"));
exporter.enqueue(make_span("s4"));
assert_eq!(exporter.queue_len(), 3);
assert_eq!(exporter.dropped_count(), 1);
}
#[test]
fn test_batch_exporter_multiple_batches() {
let config = BatchConfig {
max_batch_size: 2,
..Default::default()
};
let exporter = BatchSpanExporter::new(config);
for i in 0..6 {
exporter.enqueue(make_span(&format!("s{i}")));
}
let mut total_exported = 0;
loop {
let n = exporter.flush_batch();
if n == 0 {
break;
}
total_exported += n;
}
assert_eq!(total_exported, 6);
assert_eq!(exporter.exported_batch_count(), 3);
}
#[test]
fn test_batch_exporter_clear() {
let exporter = BatchSpanExporter::new(BatchConfig::default());
exporter.enqueue(make_span("s1"));
exporter.flush_all();
exporter.clear();
assert_eq!(exporter.queue_len(), 0);
assert_eq!(exporter.exported_batch_count(), 0);
assert_eq!(exporter.dropped_count(), 0);
}
#[test]
fn test_batch_config_default() {
let config = BatchConfig::default();
assert_eq!(config.max_batch_size, 512);
assert_eq!(config.export_interval_ms, 5000);
assert_eq!(config.max_queue_size, 2048);
}
#[test]
fn test_batch_exporter_config_accessor() {
let config = BatchConfig {
max_batch_size: 42,
..Default::default()
};
let exporter = BatchSpanExporter::new(config);
assert_eq!(exporter.config().max_batch_size, 42);
}
}