use tower_rules::{FieldFilter, FederationPolicy, FieldOp, HealthSignal, LevelFilter,
RatePredicate, ScopeFilter, StringFilter,
};
use crate::event::{health_signal_name, Event, EventScope, Level};
#[derive(Debug, Clone, PartialEq)]
pub enum ScryerFilter {
Fields(FieldSet),
And(Vec<ScryerFilter>),
Or(Vec<ScryerFilter>),
Not(Box<ScryerFilter>),
Any,
HealthSignal(HealthSignal),
}
#[derive(Debug, Clone, PartialEq)]
pub struct FieldSet {
pub scope: ScopeFilter,
pub level: Option<LevelFilter>,
pub target: Option<StringFilter>,
pub fields: Vec<FieldFilter>,
pub rate: Option<RatePredicate>,
pub federation: FederationPolicy,
}
impl ScryerFilter {
pub fn matches(&self, event: &Event) -> bool {
match self {
ScryerFilter::Any => true,
ScryerFilter::HealthSignal(signal) => {
event.scope == EventScope::Health
&& event.target
== format!("scryer.health.{}", health_signal_name(*signal))
}
ScryerFilter::Fields(fs) => fs.matches(event),
ScryerFilter::And(children) => children.iter().all(|f| f.matches(event)),
ScryerFilter::Or(children) => children.iter().any(|f| f.matches(event)),
ScryerFilter::Not(child) => !child.matches(event),
}
}
}
impl FieldSet {
fn matches(&self, event: &Event) -> bool {
if !scope_matches(&self.scope, &event.scope) {
return false;
}
if let Some(min) = self.level {
if event.level < level_filter_to_level(min) {
return false;
}
}
if let Some(target_filter) = &self.target {
if !string_filter_matches(target_filter, &event.target) {
return false;
}
}
for ff in &self.fields {
if !field_filter_matches(ff, event) {
return false;
}
}
true
}
}
fn scope_matches(filter: &ScopeFilter, scope: &EventScope) -> bool {
match filter {
ScopeFilter::Any => true,
ScopeFilter::Federated => true, ScopeFilter::TaskRun { id: None } => matches!(scope, EventScope::TaskRun(_)),
ScopeFilter::TaskRun { id: Some(want) } => {
matches!(scope, EventScope::TaskRun(got) if got == want)
}
ScopeFilter::Service { ident: None } => matches!(scope, EventScope::Service(_)),
ScopeFilter::Service { ident: Some(want) } => {
matches!(scope, EventScope::Service(got) if got == want)
}
ScopeFilter::Forge { id: None } => matches!(scope, EventScope::Forge(_)),
ScopeFilter::Forge { id: Some(want) } => {
matches!(scope, EventScope::Forge(got) if got == want)
}
}
}
fn level_filter_to_level(f: LevelFilter) -> Level {
match f {
LevelFilter::Debug => Level::Debug,
LevelFilter::Info => Level::Info,
LevelFilter::Warn => Level::Warn,
LevelFilter::Error => Level::Error,
}
}
pub(crate) fn string_filter_matches(filter: &StringFilter, s: &str) -> bool {
match filter {
StringFilter::Exact { value } => s == value,
StringFilter::Glob { pattern } => glob_matches(pattern, s),
StringFilter::Regex { pattern } => {
regex::Regex::new(pattern).map(|re| re.is_match(s)).unwrap_or(false)
}
}
}
fn glob_matches(pattern: &str, s: &str) -> bool {
glob_matches_inner(pattern.as_bytes(), s.as_bytes())
}
fn glob_matches_inner(pat: &[u8], s: &[u8]) -> bool {
match (pat.split_first(), s.split_first()) {
(None, None) => true,
(None, Some(_)) => false,
(Some((&b'*', rest_pat)), _) => {
(0..=s.len()).any(|i| glob_matches_inner(rest_pat, &s[i..]))
}
(Some((&b'?', rest_pat)), Some((_, rest_s))) => {
glob_matches_inner(rest_pat, rest_s)
}
(Some((pc, rest_pat)), Some((sc, rest_s))) if pc == sc => {
glob_matches_inner(rest_pat, rest_s)
}
_ => false,
}
}
fn field_filter_matches(ff: &FieldFilter, event: &Event) -> bool {
let value = resolve_path(&event.fields, &ff.path);
match &ff.op {
FieldOp::Exists => value.is_some(),
FieldOp::Eq { value: want } => value.map(|v| v == want).unwrap_or(false),
FieldOp::Contains { substring } => value
.and_then(|v| v.as_str())
.map(|s| s.contains(substring.as_str()))
.unwrap_or(false),
FieldOp::Matches { filter } => value
.and_then(|v| v.as_str())
.map(|s| string_filter_matches(filter, s))
.unwrap_or(false),
}
}
fn resolve_path<'a>(
fields: &'a std::collections::HashMap<String, serde_json::Value>,
path: &str,
) -> Option<&'a serde_json::Value> {
let mut parts = path.splitn(2, '.');
let key = parts.next()?;
let value = fields.get(key)?;
match parts.next() {
None => Some(value),
Some(rest) => resolve_json_path(value, rest),
}
}
fn resolve_json_path<'a>(value: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
let mut parts = path.splitn(2, '.');
let key = parts.next()?;
let child = value.get(key)?;
match parts.next() {
None => Some(child),
Some(rest) => resolve_json_path(child, rest),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glob_star_matches_segment() {
assert!(glob_matches("database.*", "database.query_slow"));
assert!(glob_matches("database.*", "database.connection_failed"));
assert!(!glob_matches("database.*", "redis.timeout"));
}
#[test]
fn glob_star_matches_empty() {
assert!(glob_matches("raft*", "raft"));
assert!(glob_matches("raft*", "raft.term_change"));
}
#[test]
fn glob_question_mark() {
assert!(glob_matches("ra?t", "raft"));
assert!(!glob_matches("ra?t", "rat"));
}
#[test]
fn glob_exact_no_wildcards() {
assert!(glob_matches("raft.quorum_lost", "raft.quorum_lost"));
assert!(!glob_matches("raft.quorum_lost", "raft.quorum_los"));
}
}