1use std::{
2 collections::HashMap,
3 fmt,
4 hash::{Hash, Hasher},
5 num::NonZeroU64,
6};
7
8use vector_lib::{
9 config::LegacyKey,
10 lookup::{OwnedTargetPath, lookup_v2::OptionalValuePath},
11 sampling::RatioSampler,
12};
13
14use crate::{
15 conditions::Condition,
16 event::{Event, Value},
17 internal_events::SampleEventDiscarded,
18 sinks::prelude::TemplateRenderingError,
19 template::UnconfinedTemplate,
20 transforms::{FunctionTransform, OutputBuffer},
21};
22
23#[derive(Clone, Debug)]
27pub enum SampleMode {
28 Rate {
29 rate: u64,
30 counters: HashMap<Option<String>, u64>,
31 },
32 Ratio {
33 ratio: f64,
34 samplers: HashMap<Option<String>, RatioSampler>,
35 hash_ratio_threshold: u64,
36 },
37}
38
39impl SampleMode {
40 pub fn new_rate(rate: u64) -> Self {
41 Self::Rate {
42 rate,
43 counters: HashMap::default(),
44 }
45 }
46
47 pub fn new_ratio(ratio: f64) -> Self {
48 Self::Ratio {
49 ratio,
50 samplers: HashMap::default(),
51 hash_ratio_threshold: (ratio * (u64::MAX as u128) as f64) as u64,
59 }
60 }
61
62 fn increment(&mut self, group_by_key: Option<String>, value: Option<&Value>) -> bool {
63 let threshold_exceeded = match self {
64 Self::Rate { rate, counters } => {
65 let counter_value = counters.entry(group_by_key).or_default();
66 let old_counter_value = *counter_value;
67 *counter_value += 1;
68 old_counter_value % *rate == 0
69 }
70 Self::Ratio {
71 ratio, samplers, ..
72 } => samplers
73 .entry(group_by_key)
74 .or_insert_with(|| RatioSampler::new(*ratio))
75 .sample(),
76 };
77 if let Some(value) = value {
78 self.hash_within_ratio(value.to_string_lossy().as_bytes())
79 } else {
80 threshold_exceeded
81 }
82 }
83
84 fn hash_within_ratio(&self, value: &[u8]) -> bool {
85 let hash = seahash::hash(value);
86 match self {
87 Self::Rate { rate, .. } => hash.is_multiple_of(*rate),
88 Self::Ratio {
89 hash_ratio_threshold,
90 ..
91 } => hash <= *hash_ratio_threshold,
92 }
93 }
94}
95
96enum EventSampleMode {
97 Ratio(f64),
98 Rate(NonZeroU64),
99}
100
101impl EventSampleMode {
102 fn sample_rate_label(&self) -> String {
103 match self {
104 Self::Ratio(ratio) => ratio.to_string(),
105 Self::Rate(rate) => rate.to_string(),
106 }
107 }
108}
109
110#[derive(Clone, Default)]
111pub struct DynamicSampleFields {
112 pub ratio_field: Option<String>,
113 pub rate_field: Option<String>,
114}
115
116impl fmt::Display for SampleMode {
117 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
118 match self {
121 Self::Rate { rate, .. } => write!(f, "{rate}"),
122 Self::Ratio { ratio, .. } => write!(f, "{ratio}"),
123 }
124 }
125}
126
127#[derive(Clone)]
128pub enum SampleKeySource {
129 Static {
130 key_field: Option<String>,
131 group_by: Option<UnconfinedTemplate>,
132 },
133 Dynamic {
134 fields: DynamicSampleFields,
135 group_by: Option<UnconfinedTemplate>,
136 },
137}
138
139#[derive(Clone)]
140pub struct Sample {
141 name: String,
142 static_mode: SampleMode,
143 key_source: SampleKeySource,
144 dynamic_event_counters: HashMap<Option<String>, u64>,
145 exclude: Option<Condition>,
146 sample_rate_key: OptionalValuePath,
147}
148
149impl Sample {
150 #![allow(dead_code)]
153 pub fn new(
154 name: String,
155 static_mode: SampleMode,
156 key_field: Option<String>,
157 group_by: Option<UnconfinedTemplate>,
158 exclude: Option<Condition>,
159 sample_rate_key: OptionalValuePath,
160 ) -> Self {
161 Self::new_with_source(
162 name,
163 static_mode,
164 SampleKeySource::Static {
165 key_field,
166 group_by,
167 },
168 exclude,
169 sample_rate_key,
170 )
171 }
172
173 pub fn new_with_dynamic(
174 name: String,
175 static_mode: SampleMode,
176 fields: DynamicSampleFields,
177 group_by: Option<UnconfinedTemplate>,
178 exclude: Option<Condition>,
179 sample_rate_key: OptionalValuePath,
180 ) -> Self {
181 Self::new_with_source(
182 name,
183 static_mode,
184 SampleKeySource::Dynamic { fields, group_by },
185 exclude,
186 sample_rate_key,
187 )
188 }
189
190 fn new_with_source(
191 name: String,
192 static_mode: SampleMode,
193 key_source: SampleKeySource,
194 exclude: Option<Condition>,
195 sample_rate_key: OptionalValuePath,
196 ) -> Self {
197 Self {
198 name,
199 static_mode,
200 key_source,
201 dynamic_event_counters: HashMap::default(),
202 exclude,
203 sample_rate_key,
204 }
205 }
206
207 #[cfg(test)]
208 pub fn ratio(&self) -> f64 {
209 match &self.static_mode {
210 SampleMode::Rate { rate, .. } => 1.0f64 / *rate as f64,
211 SampleMode::Ratio { ratio, .. } => *ratio,
212 }
213 }
214
215 fn dynamic_sample_hash(group_by_key: Option<&str>, counter: u64) -> u64 {
216 let mut hasher = seahash::SeaHasher::new();
217 group_by_key.hash(&mut hasher);
218 counter.hash(&mut hasher);
219 hasher.finish()
220 }
221
222 fn sample_with_dynamic_ratio(&mut self, ratio: f64, group_by_key: Option<String>) -> bool {
223 let counter_value = self
224 .dynamic_event_counters
225 .entry(group_by_key.clone())
226 .or_default();
227 let old_counter_value = *counter_value;
228 *counter_value += 1;
229
230 let hash = Self::dynamic_sample_hash(group_by_key.as_deref(), old_counter_value);
231 let hash_ratio_threshold = (ratio * (u64::MAX as u128) as f64) as u64;
232 hash <= hash_ratio_threshold
233 }
234
235 fn event_ratio(&self, event: &Event) -> Option<f64> {
236 let ratio_field = match &self.key_source {
237 SampleKeySource::Dynamic { fields, .. } => fields.ratio_field.as_ref()?,
238 SampleKeySource::Static { .. } => return None,
239 };
240
241 let value = self.get_event_value(event, ratio_field.as_str())?;
242
243 let ratio = match value {
244 Value::Integer(value) => *value as f64,
245 Value::Float(value) => value.into_inner(),
246 Value::Bytes(bytes) => std::str::from_utf8(bytes).ok()?.parse::<f64>().ok()?,
247 _ => return None,
248 };
249
250 (ratio > 0.0 && ratio <= 1.0).then_some(ratio)
251 }
252
253 fn event_rate(&self, event: &Event) -> Option<NonZeroU64> {
254 let rate_field = match &self.key_source {
255 SampleKeySource::Dynamic { fields, .. } => fields.rate_field.as_ref()?,
256 SampleKeySource::Static { .. } => return None,
257 };
258
259 let value = self.get_event_value(event, rate_field.as_str())?;
260
261 match value {
262 Value::Integer(value) => u64::try_from(*value).ok().and_then(NonZeroU64::new),
263 Value::Bytes(bytes) => std::str::from_utf8(bytes).ok()?.parse::<NonZeroU64>().ok(),
264 _ => None,
265 }
266 }
267
268 fn get_event_value<'a>(&self, event: &'a Event, path: &str) -> Option<&'a Value> {
269 match event {
270 Event::Log(event) => event.parse_path_and_get_value(path).ok().flatten(),
271 Event::Trace(event) => event.parse_path_and_get_value(path).ok().flatten(),
272 Event::Metric(_) => panic!("component can never receive metric events"),
273 }
274 }
275
276 fn event_sample_mode(&self, event: &Event) -> Option<EventSampleMode> {
277 self.event_ratio(event)
278 .map(EventSampleMode::Ratio)
279 .or_else(|| self.event_rate(event).map(EventSampleMode::Rate))
280 }
281
282 fn sample_with_dynamic_rate(&mut self, rate: NonZeroU64, group_by_key: Option<String>) -> bool {
283 let counter_value = self
284 .dynamic_event_counters
285 .entry(group_by_key.clone())
286 .or_default();
287 let old_counter_value = *counter_value;
288 *counter_value += 1;
289 let hash = Self::dynamic_sample_hash(group_by_key.as_deref(), old_counter_value);
290
291 hash.is_multiple_of(rate.get())
292 }
293
294 fn group_by_key(&self, event: &Event) -> Option<String> {
295 let group_by = match &self.key_source {
296 SampleKeySource::Static { group_by, .. } => group_by.as_ref()?,
297 SampleKeySource::Dynamic { group_by, .. } => group_by.as_ref()?,
298 };
299
300 match event {
301 Event::Log(event) => group_by.render_string(event),
302 Event::Trace(event) => group_by.render_string(event),
303 Event::Metric(_) => panic!("component can never receive metric events"),
304 }
305 .map_err(|error| {
306 emit!(TemplateRenderingError {
307 error,
308 field: Some("group_by"),
309 drop_event: false,
310 })
311 })
312 .ok()
313 }
314
315 fn static_key_value<'a>(&self, event: &'a Event) -> Option<&'a Value> {
316 let key_field = match &self.key_source {
317 SampleKeySource::Static { key_field, .. } => key_field.as_ref()?,
318 SampleKeySource::Dynamic { .. } => return None,
319 };
320
321 self.get_event_value(event, key_field)
322 }
323}
324
325impl FunctionTransform for Sample {
326 fn transform(&mut self, output: &mut OutputBuffer, event: Event) {
327 let mut event = {
328 if let Some(condition) = self.exclude.as_ref() {
329 let (result, event) = condition.check(event);
330 if result {
331 output.push(event);
332 return;
333 } else {
334 event
335 }
336 } else {
337 event
338 }
339 };
340
341 let group_by_key = self.group_by_key(&event);
342 let value = self.static_key_value(&event);
343
344 let event_sample_mode = self.event_sample_mode(&event);
345 let sample_rate = event_sample_mode
346 .as_ref()
347 .map(EventSampleMode::sample_rate_label)
348 .unwrap_or_else(|| self.static_mode.to_string());
349
350 let should_sample = match event_sample_mode {
351 Some(EventSampleMode::Ratio(ratio)) => {
352 self.sample_with_dynamic_ratio(ratio, group_by_key)
353 }
354 Some(EventSampleMode::Rate(rate)) => self.sample_with_dynamic_rate(rate, group_by_key),
355 None => self.static_mode.increment(group_by_key, value),
356 };
357
358 if should_sample {
359 if let Some(path) = &self.sample_rate_key.path {
360 match event {
361 Event::Log(ref mut event) => {
362 event.namespace().insert_source_metadata(
363 self.name.as_str(),
364 event,
365 Some(LegacyKey::Overwrite(path)),
366 path,
367 sample_rate.clone(),
368 );
369 }
370 Event::Trace(ref mut event) => {
371 event.insert(&OwnedTargetPath::event(path.clone()), sample_rate);
372 }
373 Event::Metric(_) => panic!("component can never receive metric events"),
374 };
375 }
376 output.push(event);
377 } else {
378 emit!(SampleEventDiscarded);
379 }
380 }
381}