Skip to main content

drain_flow/drains/
simple.rs

1// Copyright Nicholas Harring. All rights reserved.
2//
3// This program is free software: you can redistribute it and/or modify it under
4// the terms of the Server Side Public License, version 1, as published by MongoDB, Inc.
5// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
6// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
7// See the Server Side Public License for more details. You should have received a copy of the
8// Server Side Public License along with this program.
9// If not, see <http://www.mongodb.com/licensing/server-side-public-license>.
10
11use std::{collections::HashMap, fmt, sync::Arc};
12
13use anyhow::{anyhow, Error};
14use fraction::{BigInt, FromPrimitive, Ratio};
15use joinery::{Joinable, JoinableIterator};
16use lazy_static::lazy_static;
17use parking_lot::RwLock;
18use regex::Regex;
19use string_interner::{DefaultSymbol, StringInterner};
20use tracing::instrument;
21
22use crate::{drains::api::Drain, log_group::LogGroup, record::Record};
23
24lazy_static! {
25    pub(crate) static ref INTERNER: Arc<RwLock<StringInterner<string_interner::backend::BucketBackend>>> =
26        Arc::new(RwLock::new(StringInterner::<
27            string_interner::backend::BucketBackend,
28        >::new()));
29}
30#[derive(Debug, Clone)]
31pub struct SingleLayer {
32    /// Regular expressions defining the domain patterns to be replaced in log lines.
33    pub domain: Vec<Regex>,
34    /// The core storage for log groups, organized by log line length and first token.
35    base_layer: HashMap<usize, HashMap<DefaultSymbol, Vec<LogGroup>>>,
36    /// The similarity threshold used to determine if a new log line matches an existing `LogGroup`.
37    pub threshold: Ratio<BigInt>,
38    /// A shared string interner for efficient storage and comparison of log tokens.
39    strings: Arc<RwLock<StringInterner<string_interner::backend::BucketBackend>>>,
40}
41
42impl SingleLayer {
43    /// Creates a new `SingleLayer` drain instance.
44    ///
45    /// # Arguments
46    ///
47    /// * `domain` - A vector of strings, each representing a regular expression
48    ///   pattern to be used for preprocessing log lines. These patterns are replaced
49    ///   with a wildcard token before further processing.
50    ///
51    /// # Returns
52    ///
53    /// A `Result` containing the new `SingleLayer` instance on success, or an
54    /// `anyhow::Error` if any of the provided domain patterns are invalid regular expressions.
55    #[instrument(skip(domain))]
56    pub fn new(domain: Vec<String>) -> Result<Self, Error> {
57        let patterns = domain
58            .iter()
59            .map(|s| Regex::new(s))
60            .collect::<Result<Vec<Regex>, regex::Error>>()?;
61        Ok(Self {
62            domain: patterns,
63            base_layer: HashMap::new(),
64            threshold: Ratio::from_float::<f32>(0.5).expect("0.5 converts into a ratio"),
65            strings: INTERNER.clone(),
66        })
67    }
68
69    /// Sets the similarity threshold for the drain.
70    ///
71    /// This threshold determines how similar a new log line must be to an existing
72    /// `LogGroup`'s event template to be considered a match. The similarity is
73    /// calculated as a ratio of matching tokens to total tokens.
74    ///
75    /// # Arguments
76    ///
77    /// * `numerator` - The numerator of the similarity ratio.
78    /// * `denominator` - The denominator of the similarity ratio.
79    ///
80    /// # Returns
81    ///
82    /// `Ok(())` on success, or an `anyhow::Error` if the numerator or denominator
83    /// cannot be converted into `BigInt`.
84    #[instrument(skip(self))]
85    pub fn set_threshold(&mut self, numerator: u64, denominator: u64) -> Result<(), Error> {
86        let numer = BigInt::from_u64(numerator)
87            .ok_or_else(|| anyhow!("unable to make numerator from {}", numerator))?;
88        let denom = BigInt::from_u64(denominator)
89            .ok_or_else(|| anyhow!("unable to make denominator from {}", denominator))?;
90        let new_ratio = Ratio::new(numer, denom);
91        self.threshold = new_ratio;
92        Ok(())
93    }
94
95    /// Iterates over all `LogGroup`s stored within the drain.
96    ///
97    /// This method provides a flattened view of all log groups, regardless of their
98    /// internal organization (by length and first token).
99    ///
100    /// # Returns
101    ///
102    /// A `Vec<Vec<&LogGroup>>` containing references to all log groups.
103    #[instrument(skip(self), level = "trace")]
104    fn iter_groups(&self) -> Vec<Vec<&LogGroup>> {
105        let mut results: Vec<Vec<&LogGroup>> = Vec::new();
106        for length in self.base_layer.keys() {
107            let mut groups = vec![];
108            for (_, grp) in self.base_layer.get(length).unwrap().iter() {
109                for g in grp {
110                    groups.push(g);
111                }
112            }
113            results.push(groups);
114        }
115        results
116    }
117
118    /// Resolves a `DefaultSymbol` back into its original string representation.
119    ///
120    /// This is a utility method that uses the internal string interner to retrieve
121    /// the string associated with a given symbol.
122    ///
123    /// # Arguments
124    ///
125    /// * `sym` - The `DefaultSymbol` to resolve.
126    ///
127    /// # Returns
128    ///
129    /// A `String` representation of the symbol.
130    ///
131    /// # Panics
132    ///
133    /// Panics if the symbol cannot be resolved, which should not happen under normal
134    /// operation as symbols are only created from interned strings.
135    #[instrument(skip(self), level = "trace")]
136    pub fn resolve(&self, sym: DefaultSymbol) -> String {
137        self.strings
138            .read()
139            .resolve(sym)
140            .expect("symbols must resolve")
141            .to_owned()
142    }
143}
144
145impl Drain for SingleLayer {
146    /// Processes a single log line, attempting to match it against existing log groups.
147    ///
148    /// If the line is empty, it is ignored. Otherwise, it is converted into a `Record`.
149    /// The method then attempts to find a matching `LogGroup` based on the record's
150    /// length and first token. If a sufficiently similar `LogGroup` is found (based on
151    /// the `threshold`), the new record is added as an example to that group. If no
152    /// match is found, a new `LogGroup` is created for the record.
153    ///
154    /// # Arguments
155    ///
156    /// * `line` - The log line string to process.
157    ///
158    /// # Returns
159    ///
160    /// * `Ok(true)` if a new `LogGroup` was created.
161    /// * `Ok(false)` if the line was added to an existing `LogGroup`.
162    /// * `Err(anyhow::Error)` if an error occurred during processing.
163    #[instrument(skip(self, line))]
164    fn process_line(&mut self, line: String) -> Result<bool, Error> {
165        if line.is_empty() {
166            return Ok(false);
167        }
168        let new_record = Record::new(line);
169        let length = new_record.len();
170        let first = new_record.first().expect("records have first tokens");
171        if let Some(second_layer) = self.base_layer.get_mut(&length) {
172            match second_layer.get_mut(&first) {
173                Some(log_groups) => {
174                    let (score, offset) = log_groups.iter_mut().enumerate().fold(
175                        (
176                            0, // best score
177                            0, // index of best score LogGroup
178                        ),
179                        |mut acc, elem| {
180                            let score = new_record.clone().calc_sim_score(elem.1.event());
181                            if score > acc.0 {
182                                acc = (score, elem.0); // overwrite state with new values
183                            }
184                            acc
185                        },
186                    );
187                    let score_ratio =
188                        Ratio::<BigInt>::new(BigInt::from(score), BigInt::from(length));
189                    if score_ratio > self.threshold {
190                        // add this record's uid to the list of examples for the log group
191                        log_groups[offset].add_example(new_record);
192                        Ok(false)
193                    } else {
194                        log_groups.push(LogGroup::new(new_record));
195                        Ok(true)
196                    }
197                }
198                None => {
199                    second_layer.insert(first, vec![LogGroup::new(new_record)]);
200                    Ok(true)
201                }
202            }
203        } else {
204            self.base_layer.insert(length, HashMap::new());
205            let second_layer = self
206                .base_layer
207                .get_mut(&length)
208                .expect("We just inserted this map");
209            second_layer.insert(first, vec![LogGroup::new(new_record)]);
210            Ok(true)
211        }
212    }
213
214    /// Collects all `LogGroup`s currently stored in the drain.
215    ///
216    /// This method flattens the internal hierarchical storage of log groups
217    /// into a single vector.
218    ///
219    /// # Returns
220    ///
221    /// A `Vec<LogGroup>` containing clones of all log groups.
222    fn collect_log_groups(&self) -> Vec<LogGroup> {
223        self.iter_groups().into_iter().flatten().cloned().collect()
224    }
225}
226
227impl fmt::Display for SingleLayer {
228    /// Formats the `SingleLayer` drain for display.
229    ///
230    /// This implementation provides a human-readable representation of the drain,
231    /// including its domain patterns, similarity threshold, and a list of all
232    /// contained log groups.
233    ///
234    /// # Arguments
235    ///
236    /// * `f` - The formatter to write into.
237    ///
238    /// # Returns
239    ///
240    /// A `fmt::Result` indicating success or failure of the formatting operation.
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        let base = format!(
243            "SimpleDrain\nDomain Patterns: {:?}\nSimilarity Threshold: {}\n",
244            self.domain, self.threshold
245        );
246        let lg = "Log Groups:\n".to_string();
247        let groups = self
248            .iter_groups()
249            .iter()
250            .flatten()
251            .map(std::string::ToString::to_string)
252            .collect::<Vec<String>>();
253        let group_str = groups.iter().join_with("\n");
254        write!(f, "{}", [base, lg, group_str.to_string()].join_concat())
255    }
256}
257
258#[cfg(test)]
259mod should {
260    use spectral::prelude::*;
261    use tracing_test::traced_test;
262
263    use crate::drains::{api::Drain, simple::SingleLayer}; // Removed <BucketBackend> for now, will add if compiler complains
264
265    #[traced_test]
266    #[test]
267    fn test_new_drain() {
268        let drain = SingleLayer::new(vec![]);
269        assert_that(&drain).is_ok();
270    }
271
272    #[traced_test]
273    #[test]
274    fn test_set_threshold() {
275        let mut drain = SingleLayer::new(vec![]).unwrap();
276        let res = drain.set_threshold(100, 200);
277        assert_that(&res).is_ok();
278    }
279
280    #[traced_test]
281    #[test]
282    fn test_single_process_line() {
283        let mut drain = SingleLayer::new(vec![]).unwrap();
284        let line_1 = "Message send failed to remote host: foo.bar.com".to_string();
285        let res = drain.process_line(line_1);
286        assert_that(&res).is_ok_containing(true);
287    }
288
289    #[traced_test]
290    #[test]
291    fn test_multiple_process_line() {
292        let mut drain = SingleLayer::new(vec![]).unwrap();
293        let line_1 = "Message send failed to remote host: foo.bar.com".to_string();
294        let line_2 = "Message send failed to remote host: bork.bork.com".to_string();
295        let line_3 = "Unknown error received from peer".to_string();
296        let res = drain.process_line(line_1);
297        assert_that(&res).is_ok_containing(true);
298        let res = drain.process_line(line_2);
299        assert_that(&res).is_ok_containing(false);
300        let res = drain.process_line(line_3);
301        assert_that!(res).is_ok_containing(true);
302    }
303
304    #[traced_test]
305    #[test]
306    fn test_iter_groups() {
307        let line_1 = "This is a sequence".to_string();
308        let line_2 = "Another different order of words".to_string();
309        let line_3 = "Finally one last unique set of character runs".to_string();
310        let mut drain = SingleLayer::new(vec![]).unwrap();
311        Drain::process_line(&mut drain, line_1).unwrap();
312        Drain::process_line(&mut drain, line_2).unwrap();
313        Drain::process_line(&mut drain, line_3).unwrap();
314        let groups = drain.collect_log_groups();
315        assert_that(&groups).has_length(3);
316    }
317}