Skip to main content

drain_flow/log_group/
mod.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::{borrow::Borrow, collections::HashMap, fmt};
12
13use anyhow::Error;
14use chrono::{DateTime, Utc};
15use tracing::{debug, instrument};
16use uuid::Uuid;
17
18use crate::record::{tokens::Token, Record};
19
20/// Represents a logical grouping of similar log records.
21///
22/// A `LogGroup` is characterized by a base event (a `Record`) and a collection
23/// of example records that match the group's pattern. It also tracks variables
24/// (wildcards) within the log pattern.
25#[derive(Clone, Debug)]
26pub struct LogGroup {
27    /// The unique identifier for this log group.
28    pub id: Uuid,
29    /// The base event or representative record for this log group.
30    event: Record,
31    /// A collection of log records that belong to this group.
32    examples: Vec<Record>,
33    /// A map of variable positions (offset) to their `Token` type within the event pattern.
34    pub variables: HashMap<usize, Token>,
35}
36
37/// Represents a wildcard (variable) found within a log pattern.
38///
39/// It stores the offset (position) of the wildcard within the log line
40/// and the `Token` type of the wildcard.
41#[derive(Clone, Debug, PartialEq)]
42pub struct Wildcard((usize, Token));
43
44impl fmt::Display for Wildcard {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}", self.0 .0)
47    }
48}
49
50impl LogGroup {
51    /// Creates a new `LogGroup` from an initial `Record`.
52    ///
53    /// The provided `event` becomes the base record for the group, and is also
54    /// added as the first example.
55    ///
56    /// # Arguments
57    ///
58    /// * `event` - The initial `Record` that defines this log group.
59    ///
60    /// # Returns
61    ///
62    /// A new `LogGroup` instance.
63    #[instrument(level = "trace", skip(event))]
64    pub fn new(event: Record) -> Self {
65        let id = event.uid;
66        Self {
67            id,
68            examples: vec![event.clone()],
69            event,
70            variables: HashMap::new(),
71        }
72    }
73
74    /// Adds a new example `Record` to the log group.
75    ///
76    /// This method also attempts to discover new variables (wildcards) by comparing
77    /// the new record with the group's base event and updates the group's variable map.
78    ///
79    /// # Arguments
80    ///
81    /// * `rec` - The `Record` to add as an example.
82    #[instrument(level = "trace", skip(self, rec))]
83    pub fn add_example(&mut self, rec: Record) {
84        let vars = self.discover_variables(&rec).unwrap();
85        self.examples.push(rec);
86        if !vars.is_empty() {
87            self.update_variables(vars);
88        }
89    }
90
91    /// Returns a reference to the base event (`Record`) of this log group.
92    ///
93    /// This record represents the generalized pattern of the log group.
94    ///
95    /// # Returns
96    ///
97    /// A reference to the `Record` that is the base event.
98    #[instrument(level = "trace", skip(self))]
99    pub fn event(&self) -> &Record {
100        // This is the original event/base_record
101        &self.event
102    }
103
104    /// Returns a reference to the base record of the log group.
105    ///
106    /// This is an alias for `event()`.
107    ///
108    /// # Returns
109    ///
110    /// A reference to the `Record` that is the base record.
111    pub fn base_record(&self) -> &Record {
112        &self.event
113    }
114
115    /// Returns a slice of the example records stored in this log group.
116    ///
117    /// These are the actual log lines that have been clustered into this group.
118    ///
119    /// # Returns
120    ///
121    /// A slice (`&Vec<Record>`) of the example records.
122    pub fn examples(&self) -> &Vec<Record> {
123        &self.examples
124    }
125
126    /// Compares a given `Record` with the log group's base event to identify variable positions.
127    ///
128    /// Positions where the tokens differ between the record and the base event,
129    /// and are not already identified as variables, are considered new variables.
130    ///
131    /// # Arguments
132    ///
133    /// * `rec` - The `Record` to compare against the base event.
134    ///
135    /// # Returns
136    ///
137    /// A `Result` containing a `Vec<Wildcard>` representing the newly discovered
138    /// variable positions, or an `anyhow::Error` if the comparison fails.
139    #[instrument(level = "trace", skip(self, rec))]
140    pub fn discover_variables(&self, rec: &Record) -> Result<Vec<Wildcard>, Error> {
141        let f = self
142            .event
143            .borrow()
144            .into_iter()
145            .enumerate()
146            .zip(rec.into_iter())
147            .filter(|((idx, event), candidate)| {
148                if self.variables.contains_key(idx) {
149                    // This token has already been identified as a variable
150                    false
151                } else if event != candidate {
152                    debug!(%idx, ?event, ?candidate, "found candidate");
153                    true
154                } else {
155                    false
156                }
157            })
158            .map(|((idx, _event), _candidate)| Wildcard((idx, Token::Wildcard)))
159            .collect::<Vec<_>>();
160        Ok(f)
161    }
162
163    /// Updates the log group's variable map and base event with newly discovered wildcards.
164    ///
165    /// This method is typically called after `discover_variables` to incorporate
166    /// the identified variables into the group's pattern.
167    ///
168    /// # Arguments
169    ///
170    /// * `vars` - A `Vec<Wildcard>` containing the variables to update.
171    #[instrument(level = "trace", skip(self, vars))]
172    fn update_variables(&mut self, vars: Vec<Wildcard>) {
173        for var in vars {
174            // Assume we got vars from discover_variables so it has already checked against this map
175            self.variables.insert(var.0 .0, var.0 .1.clone());
176            // Update the tokens in the base event as well
177            let (offset, _) = self.event.inner.inner[var.0 .0].clone();
178            self.event.inner.inner[var.0 .0] = (offset, var.0 .1);
179        }
180    }
181
182    /// Returns the total number of example records stored in this `LogGroup`.
183    ///
184    /// # Returns
185    ///
186    /// The number of examples as a `usize`.
187    #[instrument(level = "trace", skip_all)]
188    pub fn len(&self) -> usize {
189        self.examples.len()
190    }
191
192    /// Checks if the log group contains any example records.
193    ///
194    /// # Returns
195    ///
196    /// `true` if the log group has no examples, `false` otherwise.
197    #[instrument(level = "trace", skip_all)]
198    pub fn is_empty(&self) -> bool {
199        self.examples.is_empty()
200    }
201
202    /// Returns a vector of references to the example records for this group.
203    ///
204    /// # Returns
205    ///
206    /// A `Vec<&Record>` containing references to all example records.
207    #[instrument(level = "trace", skip_all)]
208    pub fn get_examples(&self) -> Vec<&Record> {
209        self.examples.iter().collect::<Vec<&Record>>()
210    }
211
212    /// Returns the unique identifier (`Uuid`) associated with this `LogGroup`.
213    ///
214    /// This ID is typically the same as the `Uuid` of the `Record` that created the group.
215    ///
216    /// # Returns
217    ///
218    /// The `Uuid` of the log group.
219    #[instrument(level = "trace", skip_all)]
220    pub fn get_id(&self) -> Uuid {
221        self.id
222    }
223
224    /// Returns the creation timestamp of the base event in the `LogGroup` as a `DateTime<Utc>`.
225    ///
226    /// This timestamp is derived from the `Uuid` of the base event.
227    ///
228    /// # Returns
229    ///
230    /// A `DateTime<Utc>` representing the creation time of the base event.
231    #[instrument(level = "trace", skip_all)]
232    pub fn get_time(&self) -> DateTime<Utc> {
233        // Uuid::get_timestamp returns Option<Timestamp>
234        // Timestamp::to_unix returns (i64, u32)
235        // Ksuid::get_time returns DateTime<Utc>
236        // For now, let's assume we want to keep the DateTime<Utc> type
237        // This will require more significant changes if we need to extract time directly from Uuid
238        // Uuid::get_timestamp returns Option<Timestamp>
239        // Timestamp::to_unix returns (u64, u32) for UUIDv1
240        // DateTime::from_timestamp expects i64 for seconds.
241        self.event.uid.get_timestamp().map_or(Utc::now(), |ts| {
242            let (secs_u64, nanos) = ts.to_unix();
243            // Convert u64 seconds to i64. This is safe as long as the timestamp is not
244            // extremely far in the future, which is a reasonable assumption for log events.
245            let secs_i64 = secs_u64 as i64;
246            DateTime::from_timestamp(secs_i64, nanos).unwrap_or_else(Utc::now)
247        })
248    }
249}
250
251impl fmt::Display for LogGroup {
252    /// Formats the `LogGroup` for display.
253    ///
254    /// This implementation provides a human-readable summary of the log group,
255    /// including its ID, first seen timestamp, base event, number of examples,
256    /// and number of wildcards.
257    ///
258    /// # Arguments
259    ///
260    /// * `f` - The formatter to write into.
261    ///
262    /// # Returns
263    ///
264    /// A `fmt::Result` indicating success or failure of the formatting operation.
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        write!(
267            f,
268            "LogGroup ID: {}\nFirst Seen: {}\nEvent: {}\n{} examples and {} wildcards\n",
269            self.event.uid,  // Changed from serialize()
270            self.get_time(), // Changed from self.event.uid.get_time() to use the struct's method
271            self.event,
272            self.examples.len(),
273            self.variables.len()
274        )
275    }
276}
277
278#[cfg(test)]
279mod should {
280    use spectral::prelude::*;
281
282    use super::Wildcard;
283    use crate::{
284        log_group::LogGroup,
285        record::{tokens::Token, Record},
286    };
287
288    #[test]
289    fn test_discover_variables() {
290        let rec1 = Record::new("Common prefix Common prefix Common prefix 1234".to_string());
291        let lg = LogGroup::new(rec1);
292        let rec2 = Record::new("Common prefix Common prefix Common prefix 3456".to_string());
293        let vars = lg.discover_variables(&rec2);
294        assert_that(&vars).is_ok_containing(vec![Wildcard((6, Token::Wildcard))]);
295    }
296
297    #[test]
298    fn test_update_variables() {
299        let r1 = Record::new("Common Prefix Common Prefix Common Prefix 6789".to_string());
300        let r2 = Record::new("Common Prefix Common Prefix Common Prefix 827364".to_string());
301        let mut lg = LogGroup::new(r1);
302
303        let vars = lg.discover_variables(&r2).unwrap();
304        lg.update_variables(vars);
305        assert_that(&lg.variables).contains_key(6);
306    }
307}