Skip to main content

vector/transforms/sample/
transform.rs

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/// Exists only for backwards compatibility purposes so that the value of sample_rate_key is
24/// consistent after the internal implementation of the Sample class was modified to work in terms
25/// of percentages
26#[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            // Supports the 'key_field' option, assuming an equal distribution of values for a given
52            // field, hashing its contents this component should output events according to the
53            // configured ratio.
54            //
55            // To do one option would be to convert the hash to a number between 0 and 1 and compare
56            // to the ratio. However to address issues with precision, here the ratio is scaled to
57            // meet the width of the type of the hash.
58            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        // Avoids the print of an additional '.0' which was not performed in the previous
119        // implementation
120        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    // This function is dead code when the feature flag `transforms-impl-sample` is specified but not
151    // `transforms-sample`.
152    #![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}