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
69fn draw_is_sampled(draw: f64, rate: f64) -> bool {
70 draw < rate
71}
72
73pub fn should_sample(signal: Signal, key: Option<&str>) -> Result<bool, TelemetryError> {
74 let policy = get_sampling_policy(signal)?;
75 let rate = key
76 .and_then(|value| policy.overrides.get(value).copied())
77 .unwrap_or(policy.default_rate);
78
79 if rate >= 1.0 {
80 return Ok(true);
81 }
82 if rate <= 0.0 {
83 increment_dropped(signal, 1);
84 return Ok(false);
85 }
86
87 let keep = draw_is_sampled(rand::random::<f64>(), rate);
88 if !keep {
89 increment_dropped(signal, 1);
90 }
91 Ok(keep)
92}
93
94pub fn _reset_sampling_for_tests() {
95 *crate::_lock::lock(policies()) = BTreeMap::from([
96 (Signal::Logs, SamplingPolicy::default()),
97 (Signal::Traces, SamplingPolicy::default()),
98 (Signal::Metrics, SamplingPolicy::default()),
99 ]);
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::health::{_reset_health_for_tests, get_health_snapshot};
106 use crate::testing::acquire_test_state_lock;
107
108 #[test]
109 fn sampling_test_draw_boundary_is_strict() {
110 assert!(draw_is_sampled(0.49, 0.5));
111 assert!(!draw_is_sampled(0.5, 0.5));
112 assert!(!draw_is_sampled(0.51, 0.5));
113 }
114
115 #[test]
116 fn sampling_test_boundary_rates_and_reset_helper() {
117 let _guard = acquire_test_state_lock();
118 _reset_sampling_for_tests();
119 set_sampling_policy(
120 Signal::Logs,
121 SamplingPolicy {
122 default_rate: 0.0,
123 overrides: BTreeMap::new(),
124 },
125 )
126 .expect("policy should set");
127 assert!(!should_sample(Signal::Logs, None).expect("sampling should work"));
128
129 set_sampling_policy(
130 Signal::Logs,
131 SamplingPolicy {
132 default_rate: 1.0,
133 overrides: BTreeMap::new(),
134 },
135 )
136 .expect("policy should set");
137 assert!(should_sample(Signal::Logs, None).expect("sampling should work"));
138
139 set_sampling_policy(
140 Signal::Logs,
141 SamplingPolicy {
142 default_rate: 0.25,
143 overrides: BTreeMap::from([("special".to_string(), 0.75)]),
144 },
145 )
146 .expect("policy should set");
147 _reset_sampling_for_tests();
148 let reset = get_sampling_policy(Signal::Logs).expect("policy should exist");
149 assert_eq!(reset.default_rate, 1.0);
150 assert!(reset.overrides.is_empty());
151 }
152
153 #[test]
154 fn sampling_test_fractional_default_rate_rolls_per_call() {
155 let _guard = acquire_test_state_lock();
156 _reset_sampling_for_tests();
157 _reset_health_for_tests();
158 set_sampling_policy(
159 Signal::Logs,
160 SamplingPolicy {
161 default_rate: 0.5,
162 overrides: BTreeMap::new(),
163 },
164 )
165 .expect("policy should set");
166
167 let mut kept = 0;
168 let mut dropped = 0;
169 for _ in 0..256 {
170 if should_sample(Signal::Logs, None).expect("sampling should work") {
171 kept += 1;
172 } else {
173 dropped += 1;
174 }
175 }
176
177 assert!(kept > 0, "fractional sampling should keep some events");
178 assert!(dropped > 0, "fractional sampling should drop some events");
179 assert_eq!(
180 get_health_snapshot().dropped_logs,
181 dropped as u64,
182 "dropped_logs counter must match the number of sampling rejections \
183 (kills `if !keep` -> `if keep` mutation)"
184 );
185 }
186
187 #[test]
188 fn sampling_test_fractional_override_rate_rolls_per_call_for_same_key() {
189 let _guard = acquire_test_state_lock();
190 _reset_sampling_for_tests();
191 _reset_health_for_tests();
192 set_sampling_policy(
193 Signal::Logs,
194 SamplingPolicy {
195 default_rate: 1.0,
196 overrides: BTreeMap::from([("special".to_string(), 0.5)]),
197 },
198 )
199 .expect("policy should set");
200
201 let mut kept = 0;
202 let mut dropped = 0;
203 for _ in 0..256 {
204 if should_sample(Signal::Logs, Some("special")).expect("sampling should work") {
205 kept += 1;
206 } else {
207 dropped += 1;
208 }
209 }
210
211 assert!(
212 kept > 0,
213 "fractional override sampling should keep some events"
214 );
215 assert!(
216 dropped > 0,
217 "fractional override sampling should drop some events for the same key"
218 );
219 }
220
221 #[test]
222 fn sampling_test_override_boundaries_use_matching_key() {
223 let _guard = acquire_test_state_lock();
224 _reset_sampling_for_tests();
225 _reset_health_for_tests();
226 let before = get_health_snapshot().dropped_logs;
227 set_sampling_policy(
228 Signal::Logs,
229 SamplingPolicy {
230 default_rate: 0.0,
231 overrides: BTreeMap::from([("special".to_string(), 1.0)]),
232 },
233 )
234 .expect("policy should set");
235
236 assert!(should_sample(Signal::Logs, Some("special")).expect("sampling should work"));
237 assert!(!should_sample(Signal::Logs, Some("other")).expect("sampling should work"));
238 let after = get_health_snapshot().dropped_logs;
239 assert_eq!(after - before, 1);
240 }
241
242 #[test]
243 fn sampling_test_unknown_policy_errors_when_internal_state_is_missing() {
244 let _guard = acquire_test_state_lock();
245 crate::_lock::lock(policies()).clear();
246
247 let err = get_sampling_policy(Signal::Logs).expect_err("missing policy must error");
248 assert!(err.message.contains("unknown signal"));
249
250 let err = should_sample(Signal::Logs, None).expect_err("missing policy must bubble up");
251 assert!(err.message.contains("unknown signal"));
252
253 _reset_sampling_for_tests();
254 }
255}