use std::{borrow::Borrow, collections::HashMap, fmt};
use anyhow::Error;
use chrono::{DateTime, Utc};
use tracing::{debug, instrument};
use uuid::Uuid;
use crate::record::{tokens::Token, Record};
#[derive(Clone, Debug)]
pub struct LogGroup {
pub id: Uuid,
event: Record,
examples: Vec<Record>,
pub variables: HashMap<usize, Token>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Wildcard((usize, Token));
impl fmt::Display for Wildcard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0 .0)
}
}
impl LogGroup {
#[instrument(level = "trace", skip(event))]
pub fn new(event: Record) -> Self {
let id = event.uid;
Self {
id,
examples: vec![event.clone()],
event,
variables: HashMap::new(),
}
}
#[instrument(level = "trace", skip(self, rec))]
pub fn add_example(&mut self, rec: Record) {
let vars = self.discover_variables(&rec).unwrap();
self.examples.push(rec);
if !vars.is_empty() {
self.update_variables(vars);
}
}
#[instrument(level = "trace", skip(self))]
pub fn event(&self) -> &Record {
&self.event
}
pub fn base_record(&self) -> &Record {
&self.event
}
pub fn examples(&self) -> &Vec<Record> {
&self.examples
}
#[instrument(level = "trace", skip(self, rec))]
pub fn discover_variables(&self, rec: &Record) -> Result<Vec<Wildcard>, Error> {
let f = self
.event
.borrow()
.into_iter()
.enumerate()
.zip(rec.into_iter())
.filter(|((idx, event), candidate)| {
if self.variables.contains_key(idx) {
false
} else if event != candidate {
debug!(%idx, ?event, ?candidate, "found candidate");
true
} else {
false
}
})
.map(|((idx, _event), _candidate)| Wildcard((idx, Token::Wildcard)))
.collect::<Vec<_>>();
Ok(f)
}
#[instrument(level = "trace", skip(self, vars))]
fn update_variables(&mut self, vars: Vec<Wildcard>) {
for var in vars {
self.variables.insert(var.0 .0, var.0 .1.clone());
let (offset, _) = self.event.inner.inner[var.0 .0].clone();
self.event.inner.inner[var.0 .0] = (offset, var.0 .1);
}
}
#[instrument(level = "trace", skip_all)]
pub fn len(&self) -> usize {
self.examples.len()
}
#[instrument(level = "trace", skip_all)]
pub fn is_empty(&self) -> bool {
self.examples.is_empty()
}
#[instrument(level = "trace", skip_all)]
pub fn get_examples(&self) -> Vec<&Record> {
self.examples.iter().collect::<Vec<&Record>>()
}
#[instrument(level = "trace", skip_all)]
pub fn get_id(&self) -> Uuid {
self.id
}
#[instrument(level = "trace", skip_all)]
pub fn get_time(&self) -> DateTime<Utc> {
self.event.uid.get_timestamp().map_or(Utc::now(), |ts| {
let (secs_u64, nanos) = ts.to_unix();
let secs_i64 = secs_u64 as i64;
DateTime::from_timestamp(secs_i64, nanos).unwrap_or_else(Utc::now)
})
}
}
impl fmt::Display for LogGroup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"LogGroup ID: {}\nFirst Seen: {}\nEvent: {}\n{} examples and {} wildcards\n",
self.event.uid, self.get_time(), self.event,
self.examples.len(),
self.variables.len()
)
}
}
#[cfg(test)]
mod should {
use spectral::prelude::*;
use super::Wildcard;
use crate::{
log_group::LogGroup,
record::{tokens::Token, Record},
};
#[test]
fn test_discover_variables() {
let rec1 = Record::new("Common prefix Common prefix Common prefix 1234".to_string());
let lg = LogGroup::new(rec1);
let rec2 = Record::new("Common prefix Common prefix Common prefix 3456".to_string());
let vars = lg.discover_variables(&rec2);
assert_that(&vars).is_ok_containing(vec![Wildcard((6, Token::Wildcard))]);
}
#[test]
fn test_update_variables() {
let r1 = Record::new("Common Prefix Common Prefix Common Prefix 6789".to_string());
let r2 = Record::new("Common Prefix Common Prefix Common Prefix 827364".to_string());
let mut lg = LogGroup::new(r1);
let vars = lg.discover_variables(&r2).unwrap();
lg.update_variables(vars);
assert_that(&lg.variables).contains_key(6);
}
}