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}