1use std::collections::BTreeMap;
7use std::sync::{Mutex, OnceLock};
8
9use crate::errors::TelemetryError;
10use crate::health::increment_dropped;
11
12#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub enum Signal {
14 Logs,
15 Traces,
16 Metrics,
17}
18
19#[derive(Clone, Debug, PartialEq)]
20pub struct SamplingPolicy {
21 pub default_rate: f64,
22 pub overrides: BTreeMap<String, f64>,
23}
24
25impl Default for SamplingPolicy {
26 fn default() -> Self {
27 Self {
28 default_rate: 1.0,
29 overrides: BTreeMap::new(),
30 }
31 }
32}
33
34static POLICIES: OnceLock<Mutex<BTreeMap<Signal, SamplingPolicy>>> = OnceLock::new();
35
36fn policies() -> &'static Mutex<BTreeMap<Signal, SamplingPolicy>> {
37 POLICIES.get_or_init(|| {
38 Mutex::new(BTreeMap::from([
39 (Signal::Logs, SamplingPolicy::default()),
40 (Signal::Traces, SamplingPolicy::default()),
41 (Signal::Metrics, SamplingPolicy::default()),
42 ]))
43 })
44}
45
46pub fn set_sampling_policy(
47 signal: Signal,
48 policy: SamplingPolicy,
49) -> Result<SamplingPolicy, TelemetryError> {
50 let normalized = SamplingPolicy {
51 default_rate: policy.default_rate.clamp(0.0, 1.0),
52 overrides: policy
53 .overrides
54 .into_iter()
55 .map(|(key, rate)| (key, rate.clamp(0.0, 1.0)))
56 .collect(),
57 };
58 crate::_lock::lock(policies()).insert(signal, normalized.clone());
59 Ok(normalized)
60}
61
62pub fn get_sampling_policy(signal: Signal) -> Result<SamplingPolicy, TelemetryError> {
63 crate::_lock::lock(policies())
64 .get(&signal)
65 .cloned()
66 .ok_or_else(|| TelemetryError::new("unknown signal"))
67}
68
69pub fn should_sample(signal: Signal, key: Option<&str>) -> Result<bool, TelemetryError> {
70 let policy = get_sampling_policy(signal)?;
71 let rate = key
72 .and_then(|value| policy.overrides.get(value).copied())
73 .unwrap_or(policy.default_rate);
74
75 if rate >= 1.0 {
76 return Ok(true);
77 }
78 if rate <= 0.0 {
79 increment_dropped(signal, 1);
80 return Ok(false);
81 }
82
83 let keep = rand::random::<f64>() < rate;
84 if !keep {
85 increment_dropped(signal, 1);
86 }
87 Ok(keep)
88}
89
90pub fn _reset_sampling_for_tests() {
91 *crate::_lock::lock(policies()) = BTreeMap::from([
92 (Signal::Logs, SamplingPolicy::default()),
93 (Signal::Traces, SamplingPolicy::default()),
94 (Signal::Metrics, SamplingPolicy::default()),
95 ]);
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::health::{_reset_health_for_tests, get_health_snapshot};
102 use crate::testing::acquire_test_state_lock;
103
104 #[test]
105 fn sampling_test_boundary_rates_and_reset_helper() {
106 let _guard = acquire_test_state_lock();
107 _reset_sampling_for_tests();
108 set_sampling_policy(
109 Signal::Logs,
110 SamplingPolicy {
111 default_rate: 0.0,
112 overrides: BTreeMap::new(),
113 },
114 )
115 .expect("policy should set");
116 assert!(!should_sample(Signal::Logs, None).expect("sampling should work"));
117
118 set_sampling_policy(
119 Signal::Logs,
120 SamplingPolicy {
121 default_rate: 1.0,
122 overrides: BTreeMap::new(),
123 },
124 )
125 .expect("policy should set");
126 assert!(should_sample(Signal::Logs, None).expect("sampling should work"));
127
128 set_sampling_policy(
129 Signal::Logs,
130 SamplingPolicy {
131 default_rate: 0.25,
132 overrides: BTreeMap::from([("special".to_string(), 0.75)]),
133 },
134 )
135 .expect("policy should set");
136 _reset_sampling_for_tests();
137 let reset = get_sampling_policy(Signal::Logs).expect("policy should exist");
138 assert_eq!(reset.default_rate, 1.0);
139 assert!(reset.overrides.is_empty());
140 }
141
142 #[test]
143 fn sampling_test_fractional_default_rate_rolls_per_call() {
144 let _guard = acquire_test_state_lock();
145 _reset_sampling_for_tests();
146 _reset_health_for_tests();
147 set_sampling_policy(
148 Signal::Logs,
149 SamplingPolicy {
150 default_rate: 0.5,
151 overrides: BTreeMap::new(),
152 },
153 )
154 .expect("policy should set");
155
156 let mut kept = 0;
157 let mut dropped = 0;
158 for _ in 0..256 {
159 if should_sample(Signal::Logs, None).expect("sampling should work") {
160 kept += 1;
161 } else {
162 dropped += 1;
163 }
164 }
165
166 assert!(kept > 0, "fractional sampling should keep some events");
167 assert!(dropped > 0, "fractional sampling should drop some events");
168 assert_eq!(
169 get_health_snapshot().dropped_logs,
170 dropped as u64,
171 "dropped_logs counter must match the number of sampling rejections \
172 (kills `if !keep` -> `if keep` mutation)"
173 );
174 }
175
176 #[test]
177 fn sampling_test_fractional_override_rate_rolls_per_call_for_same_key() {
178 let _guard = acquire_test_state_lock();
179 _reset_sampling_for_tests();
180 _reset_health_for_tests();
181 set_sampling_policy(
182 Signal::Logs,
183 SamplingPolicy {
184 default_rate: 1.0,
185 overrides: BTreeMap::from([("special".to_string(), 0.5)]),
186 },
187 )
188 .expect("policy should set");
189
190 let mut kept = 0;
191 let mut dropped = 0;
192 for _ in 0..256 {
193 if should_sample(Signal::Logs, Some("special")).expect("sampling should work") {
194 kept += 1;
195 } else {
196 dropped += 1;
197 }
198 }
199
200 assert!(
201 kept > 0,
202 "fractional override sampling should keep some events"
203 );
204 assert!(
205 dropped > 0,
206 "fractional override sampling should drop some events for the same key"
207 );
208 }
209
210 #[test]
211 fn sampling_test_override_boundaries_use_matching_key() {
212 let _guard = acquire_test_state_lock();
213 _reset_sampling_for_tests();
214 _reset_health_for_tests();
215 let before = get_health_snapshot().dropped_logs;
216 set_sampling_policy(
217 Signal::Logs,
218 SamplingPolicy {
219 default_rate: 0.0,
220 overrides: BTreeMap::from([("special".to_string(), 1.0)]),
221 },
222 )
223 .expect("policy should set");
224
225 assert!(should_sample(Signal::Logs, Some("special")).expect("sampling should work"));
226 assert!(!should_sample(Signal::Logs, Some("other")).expect("sampling should work"));
227 let after = get_health_snapshot().dropped_logs;
228 assert_eq!(after - before, 1);
229 }
230
231 #[test]
232 fn sampling_test_unknown_policy_errors_when_internal_state_is_missing() {
233 let _guard = acquire_test_state_lock();
234 crate::_lock::lock(policies()).clear();
235
236 let err = get_sampling_policy(Signal::Logs).expect_err("missing policy must error");
237 assert!(err.message.contains("unknown signal"));
238
239 let err = should_sample(Signal::Logs, None).expect_err("missing policy must bubble up");
240 assert!(err.message.contains("unknown signal"));
241
242 _reset_sampling_for_tests();
243 }
244}