Skip to content

Commit

Permalink
Add event matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
9999years committed Aug 28, 2023
1 parent 03bbaf5 commit 42da07a
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 10 deletions.
21 changes: 11 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test-harness/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ publish = false
backoff = { version = "0.4.0", default-features = false }
itertools = "0.11.0"
miette = { version = "5.9.0", features = ["fancy"] }
regex = "1.9.4"
serde = { version = "1.0.186", features = ["derive"] }
serde_json = "1.0.105"
tokio = { version = "1.28.2", features = ["full", "tracing"] }
Expand Down
4 changes: 4 additions & 0 deletions test-harness/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ pub use tracing_json::Event;

mod tracing_reader;

mod matcher;
pub use matcher::IntoMatcher;
pub use matcher::Matcher;

pub mod fs;
167 changes: 167 additions & 0 deletions test-harness/src/matcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use miette::IntoDiagnostic;
use regex::Regex;

use crate::Event;

/// An [`Event`] matcher.
pub struct Matcher {
message: Regex,
target: Option<String>,
spans: Vec<String>,
}

impl Matcher {
/// Construct a query for events with messages matching the given regex.
pub fn message(message_regex: &str) -> miette::Result<Self> {
let message = Regex::new(message_regex).into_diagnostic()?;
Ok(Self {
message,
target: None,
spans: Vec::new(),
})
}

/// Construct a query for new span events, denoted by a `new` message.
pub fn span_new() -> Self {
// This regex will never fail to parse.
Self::message("new").unwrap()
}

/// Construct a query for span close events, denoted by a `close` message.
pub fn span_close() -> Self {
// This regex will never fail to parse.
Self::message("close").unwrap()
}

/// Require that matching events be in a span with the given name.
///
/// Note that this will overwrite any previously-set spans.
pub fn in_span(mut self, span: &str) -> Self {
self.spans.clear();
self.spans.push(span.to_owned());
self
}

/// Require that matching events be in spans with the given names.
///
/// Spans are listed from the inside out; that is, a call to `in_spans(["a", "b", "c"])` will
/// require that events be emitted from a span `a` directly nested in a span
/// `b` directly nested in a span `c`.
///
/// Note that this will overwrite any previously-set spans.
pub fn in_spans(mut self, spans: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
self.spans = spans.into_iter().map(|s| s.as_ref().to_owned()).collect();
self
}

/// Require that matching events be emitted from the given module as recorded by the event's
/// `target` field.
///
/// Note that this requires the module name to match exactly; child modules will not be
/// matched.
pub fn from_module(mut self, module: &str) -> Self {
self.target = Some(module.to_owned());
self
}

/// Determines if this query matches the given event.
pub fn matches(&self, event: &Event) -> bool {
if !self.message.is_match(&event.message) {
return false;
}

if !self.spans.is_empty() {
let mut spans = event.spans();
for expected_name in &self.spans {
match spans.next() {
Some(actual_span) => {
if &actual_span.name != expected_name {
return false;
}
}
None => {
// We expected another span but the event doesn't have one.
return false;
}
}
}
}

if let Some(target) = &self.target {
if target != &event.target {
return false;
}
}

true
}
}

/// A type that can be converted into a `Matcher` and used for searching log events.
pub trait IntoMatcher {
/// Convert the object into a `Matcher`.
fn into_matcher(self) -> miette::Result<Matcher>;
}

impl IntoMatcher for Matcher {
fn into_matcher(self) -> miette::Result<Matcher> {
Ok(self)
}
}

impl IntoMatcher for &str {
fn into_matcher(self) -> miette::Result<Matcher> {
Matcher::message(self)
}
}

#[cfg(test)]
mod tests {
use tracing::Level;

use crate::tracing_json::Span;

use super::*;

#[test]
fn test_matcher_message() {
let matcher = r"ghci started in \d+\.\d+s".into_matcher().unwrap();
assert!(matcher.matches(&Event {
timestamp: "2023-08-25T22:14:30.067641Z".to_owned(),
level: Level::INFO,
message: "ghci started in 2.44s".to_owned(),
fields: Default::default(),
target: "ghcid_ng::ghci".to_owned(),
span: Some(Span {
name: "ghci".to_owned(),
rest: Default::default()
}),
spans: vec![Span {
name: "ghci".to_owned(),
rest: Default::default()
},]
}));
}

#[test]
fn test_matcher_spans_and_target() {
let matcher = Matcher::span_close()
.from_module("ghcid_ng::ghci")
.in_spans(["reload", "on_action"]);
assert!(matcher.matches(&Event {
timestamp: "2023-08-25T22:14:30.993920Z".to_owned(),
level: Level::DEBUG,
message: "close".to_owned(),
fields: Default::default(),
target: "ghcid_ng::ghci".to_owned(),
span: Some(Span {
name: "reload".to_owned(),
rest: Default::default()
}),
spans: vec![Span {
name: "on_action".to_owned(),
rest: Default::default()
},]
}));
}
}

0 comments on commit 42da07a

Please sign in to comment.