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}