Skip to main content

vector_core/event/
discriminant.rs

1use std::{
2    fmt,
3    hash::{Hash, Hasher},
4};
5
6use super::{LogEvent, ObjectMap, Value};
7
8// TODO: if we had `Value` implement `Eq` and `Hash`, the implementation here
9// would be much easier. The issue is with `f64` type. We should consider using
10// a newtype for `f64` there that'd implement `Eq` and `Hash` if it's safe, for
11// example `NormalF64`, and guard the values with `val.is_normal() == true`
12// invariant.
13// See also: https://internals.rust-lang.org/t/f32-f64-should-implement-hash/5436/32
14
15/// An event discriminant identifies a distinguishable subset of events.
16/// Intended for dissecting streams of events to sub-streams, for instance to
17/// be able to allocate a buffer per sub-stream.
18/// Implements `PartialEq`, `Eq` and `Hash` to enable use as a `HashMap` key.
19#[derive(Debug, Clone)]
20pub struct Discriminant {
21    values: Vec<Option<Value>>,
22}
23
24impl Discriminant {
25    /// Create a new Discriminant from the `LogEvent` and an ordered slice of
26    /// fields to include into a discriminant value.
27    pub fn from_log_event(event: &LogEvent, discriminant_fields: &[impl AsRef<str>]) -> Self {
28        let values: Vec<Option<Value>> = discriminant_fields
29            .iter()
30            .map(|discriminant_field| {
31                event
32                    .parse_path_and_get_value(discriminant_field.as_ref())
33                    .ok()
34                    .flatten()
35                    .cloned()
36            })
37            .collect();
38        Self { values }
39    }
40}
41
42impl PartialEq for Discriminant {
43    fn eq(&self, other: &Self) -> bool {
44        self.values
45            .iter()
46            .zip(other.values.iter())
47            .all(|(this, other)| match (this, other) {
48                (None, None) => true,
49                (Some(this), Some(other)) => value_eq(this, other),
50                _ => false,
51            })
52    }
53}
54
55impl Eq for Discriminant {}
56
57// Equality check for discriminant purposes.
58fn value_eq(this: &Value, other: &Value) -> bool {
59    match (this, other) {
60        // Trivial.
61        (Value::Bytes(this), Value::Bytes(other)) => this.eq(other),
62        (Value::Boolean(this), Value::Boolean(other)) => this.eq(other),
63        (Value::Integer(this), Value::Integer(other)) => this.eq(other),
64        (Value::Timestamp(this), Value::Timestamp(other)) => this.eq(other),
65        (Value::Null, Value::Null) => true,
66        // Non-trivial.
67        (Value::Float(this), Value::Float(other)) => f64_eq(this.into_inner(), other.into_inner()),
68        (Value::Array(this), Value::Array(other)) => array_eq(this, other),
69        (Value::Object(this), Value::Object(other)) => map_eq(this, other),
70        // Type mismatch.
71        _ => false,
72    }
73}
74
75// Does an f64 comparison that is suitable for discriminant purposes.
76fn f64_eq(this: f64, other: f64) -> bool {
77    if this.is_nan() && other.is_nan() {
78        return true;
79    }
80    if this != other {
81        return false;
82    }
83    if (this.is_sign_positive() && other.is_sign_negative())
84        || (this.is_sign_negative() && other.is_sign_positive())
85    {
86        return false;
87    }
88    true
89}
90
91fn array_eq(this: &[Value], other: &[Value]) -> bool {
92    if this.len() != other.len() {
93        return false;
94    }
95
96    this.iter()
97        .zip(other.iter())
98        .all(|(first, second)| value_eq(first, second))
99}
100
101fn map_eq(this: &ObjectMap, other: &ObjectMap) -> bool {
102    if this.len() != other.len() {
103        return false;
104    }
105
106    this.iter()
107        .zip(other.iter())
108        .all(|((key1, value1), (key2, value2))| key1 == key2 && value_eq(value1, value2))
109}
110
111impl Hash for Discriminant {
112    fn hash<H: Hasher>(&self, state: &mut H) {
113        for value in &self.values {
114            match value {
115                Some(value) => {
116                    state.write_u8(1);
117                    hash_value(state, value);
118                }
119                None => state.write_u8(0),
120            }
121        }
122    }
123}
124
125// Hashes value for discriminant purposes.
126fn hash_value<H: Hasher>(hasher: &mut H, value: &Value) {
127    match value {
128        // Trivial.
129        Value::Bytes(val) => val.hash(hasher),
130        Value::Regex(val) => val.as_bytes_slice().hash(hasher),
131        Value::Boolean(val) => val.hash(hasher),
132        Value::Integer(val) => val.hash(hasher),
133        Value::Timestamp(val) => val.hash(hasher),
134        // Non-trivial.
135        Value::Float(val) => hash_f64(hasher, val.into_inner()),
136        Value::Array(val) => hash_array(hasher, val),
137        Value::Object(val) => hash_map(hasher, val),
138        Value::Null => hash_null(hasher),
139    }
140}
141
142// Does f64 hashing that is suitable for discriminant purposes.
143fn hash_f64<H: Hasher>(hasher: &mut H, value: f64) {
144    hasher.write(&value.to_ne_bytes());
145}
146
147fn hash_array<H: Hasher>(hasher: &mut H, array: &[Value]) {
148    for val in array {
149        hash_value(hasher, val);
150    }
151}
152
153fn hash_map<H: Hasher>(hasher: &mut H, map: &ObjectMap) {
154    for (key, val) in map {
155        hasher.write(key.as_bytes());
156        hash_value(hasher, val);
157    }
158}
159
160fn hash_null<H: Hasher>(hasher: &mut H) {
161    hasher.write_u8(0);
162}
163
164impl fmt::Display for Discriminant {
165    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
166        for (i, value) in self.values.iter().enumerate() {
167            if i != 0 {
168                write!(fmt, "-")?;
169            }
170            if let Some(value) = value {
171                value.fmt(fmt)?;
172            } else {
173                fmt.write_str("none")?;
174            }
175        }
176        Ok(())
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use std::collections::{HashMap, hash_map::DefaultHasher};
183
184    use super::*;
185    use crate::event::LogEvent;
186    use vrl::event_path;
187
188    fn hash<H: Hash>(hash: H) -> u64 {
189        let mut hasher = DefaultHasher::new();
190        hash.hash(&mut hasher);
191        hasher.finish()
192    }
193
194    #[test]
195    fn equal() {
196        let mut event_1 = LogEvent::default();
197        event_1.insert(event_path!("hostname"), "localhost");
198        event_1.insert(event_path!("irrelevant"), "not even used");
199        let mut event_2 = event_1.clone();
200        event_2.insert(
201            event_path!("irrelevant"),
202            "does not matter if it's different",
203        );
204
205        let discriminant_fields = vec!["hostname".to_string(), "container_id".to_string()];
206
207        let discriminant_1 = Discriminant::from_log_event(&event_1, &discriminant_fields);
208        let discriminant_2 = Discriminant::from_log_event(&event_2, &discriminant_fields);
209
210        assert_eq!(discriminant_1, discriminant_2);
211        assert_eq!(hash(discriminant_1), hash(discriminant_2));
212    }
213
214    #[test]
215    fn not_equal() {
216        let mut event_1 = LogEvent::default();
217        event_1.insert(event_path!("hostname"), "localhost");
218        event_1.insert(event_path!("container_id"), "abc");
219        let mut event_2 = event_1.clone();
220        event_2.insert(event_path!("container_id"), "def");
221
222        let discriminant_fields = vec!["hostname".to_string(), "container_id".to_string()];
223
224        let discriminant_1 = Discriminant::from_log_event(&event_1, &discriminant_fields);
225        let discriminant_2 = Discriminant::from_log_event(&event_2, &discriminant_fields);
226
227        assert_ne!(discriminant_1, discriminant_2);
228        assert_ne!(hash(discriminant_1), hash(discriminant_2));
229    }
230
231    #[test]
232    fn field_order() {
233        let mut event_1 = LogEvent::default();
234        event_1.insert(event_path!("a"), "a");
235        event_1.insert(event_path!("b"), "b");
236        let mut event_2 = LogEvent::default();
237        event_2.insert(event_path!("b"), "b");
238        event_2.insert(event_path!("a"), "a");
239
240        let discriminant_fields = vec!["a".to_string(), "b".to_string()];
241
242        let discriminant_1 = Discriminant::from_log_event(&event_1, &discriminant_fields);
243        let discriminant_2 = Discriminant::from_log_event(&event_2, &discriminant_fields);
244
245        assert_eq!(discriminant_1, discriminant_2);
246        assert_eq!(hash(discriminant_1), hash(discriminant_2));
247    }
248
249    #[test]
250    fn map_values_key_order() {
251        let mut event_1 = LogEvent::default();
252        event_1.insert(event_path!("nested", "a"), "a");
253        event_1.insert(event_path!("nested", "b"), "b");
254        let mut event_2 = LogEvent::default();
255        event_2.insert(event_path!("nested", "b"), "b");
256        event_2.insert(event_path!("nested", "a"), "a");
257
258        let discriminant_fields = vec!["nested".to_string()];
259
260        let discriminant_1 = Discriminant::from_log_event(&event_1, &discriminant_fields);
261        let discriminant_2 = Discriminant::from_log_event(&event_2, &discriminant_fields);
262
263        assert_eq!(discriminant_1, discriminant_2);
264        assert_eq!(hash(discriminant_1), hash(discriminant_2));
265    }
266
267    #[test]
268    fn array_values_insertion_order() {
269        let mut event_1 = LogEvent::default();
270        event_1.insert(event_path!("array", 0isize), "a");
271        event_1.insert(event_path!("array", 1isize), "b");
272        let mut event_2 = LogEvent::default();
273        event_2.insert(event_path!("array", 1isize), "b");
274        event_2.insert(event_path!("array", 0isize), "a");
275
276        let discriminant_fields = vec!["array".to_string()];
277
278        let discriminant_1 = Discriminant::from_log_event(&event_1, &discriminant_fields);
279        let discriminant_2 = Discriminant::from_log_event(&event_2, &discriminant_fields);
280
281        assert_eq!(discriminant_1, discriminant_2);
282        assert_eq!(hash(discriminant_1), hash(discriminant_2));
283    }
284
285    #[test]
286    fn map_values_matter_1() {
287        let mut event_1 = LogEvent::default();
288        event_1.insert(event_path!("nested", "a"), "a"); // `nested` is a `Value::Map`
289        let event_2 = LogEvent::default(); // empty event
290
291        let discriminant_fields = vec!["nested".to_string()];
292
293        let discriminant_1 = Discriminant::from_log_event(&event_1, &discriminant_fields);
294        let discriminant_2 = Discriminant::from_log_event(&event_2, &discriminant_fields);
295
296        assert_ne!(discriminant_1, discriminant_2);
297        assert_ne!(hash(discriminant_1), hash(discriminant_2));
298    }
299
300    #[test]
301    fn map_values_matter_2() {
302        let mut event_1 = LogEvent::default();
303        event_1.insert(event_path!("nested", "a"), "a"); // `nested` is a `Value::Map`
304        let mut event_2 = LogEvent::default();
305        event_2.insert(event_path!("nested"), "x"); // `nested` is a `Value::String`
306
307        let discriminant_fields = vec!["nested".to_string()];
308
309        let discriminant_1 = Discriminant::from_log_event(&event_1, &discriminant_fields);
310        let discriminant_2 = Discriminant::from_log_event(&event_2, &discriminant_fields);
311
312        assert_ne!(discriminant_1, discriminant_2);
313        assert_ne!(hash(discriminant_1), hash(discriminant_2));
314    }
315
316    #[test]
317    fn with_hash_map() {
318        #[allow(clippy::mutable_key_type)]
319        let mut map: HashMap<Discriminant, usize> = HashMap::new();
320
321        let event_stream_1 = {
322            let mut event = LogEvent::default();
323            event.insert(event_path!("hostname"), "a.test");
324            event.insert(event_path!("container_id"), "abc");
325            event
326        };
327
328        let event_stream_2 = {
329            let mut event = LogEvent::default();
330            event.insert(event_path!("hostname"), "b.test");
331            event.insert(event_path!("container_id"), "def");
332            event
333        };
334
335        let event_stream_3 = {
336            // no `hostname` or `container_id`
337            LogEvent::default()
338        };
339
340        let discriminant_fields = vec!["hostname".to_string(), "container_id".to_string()];
341
342        let mut process_event = |event| {
343            let discriminant = Discriminant::from_log_event(&event, &discriminant_fields);
344            *map.entry(discriminant).and_modify(|e| *e += 1).or_insert(0)
345        };
346
347        {
348            let mut event = event_stream_1.clone();
349            event.insert(event_path!("message"), "a");
350            assert_eq!(process_event(event), 0);
351        }
352
353        {
354            let mut event = event_stream_1.clone();
355            event.insert(event_path!("message"), "b");
356            event.insert(event_path!("irrelevant"), "c");
357            assert_eq!(process_event(event), 1);
358        }
359
360        {
361            let mut event = event_stream_2.clone();
362            event.insert(event_path!("message"), "d");
363            assert_eq!(process_event(event), 0);
364        }
365
366        {
367            let mut event = event_stream_2.clone();
368            event.insert(event_path!("message"), "e");
369            event.insert(event_path!("irrelevant"), "d");
370            assert_eq!(process_event(event), 1);
371        }
372
373        {
374            let mut event = event_stream_3.clone();
375            event.insert(event_path!("message"), "f");
376            assert_eq!(process_event(event), 0);
377        }
378
379        {
380            let mut event = event_stream_3.clone();
381            event.insert(event_path!("message"), "g");
382            event.insert(event_path!("irrelevant"), "d");
383            assert_eq!(process_event(event), 1);
384        }
385
386        // Now assert the amount of events processed per discriminant.
387        assert_eq!(process_event(event_stream_1), 2);
388        assert_eq!(process_event(event_stream_2), 2);
389        assert_eq!(process_event(event_stream_3), 2);
390    }
391
392    #[test]
393    fn test_display() {
394        let mut event = LogEvent::default();
395        event.insert(event_path!("hostname"), "localhost");
396        event.insert(event_path!("container_id"), 1);
397
398        let discriminant = Discriminant::from_log_event(
399            &event,
400            &["hostname".to_string(), "container_id".to_string()],
401        );
402        assert_eq!(format!("{discriminant}"), "\"localhost\"-1");
403
404        let discriminant =
405            Discriminant::from_log_event(&event, &["hostname".to_string(), "service".to_string()]);
406        assert_eq!(format!("{discriminant}"), "\"localhost\"-none");
407    }
408}