// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Matching engine: a linear scan over all rules per event, with regexes //! compiled once. The seed set is ~60 shallow rules — no indexing until a //! profile says otherwise. use super::model::{Matcher, Rule}; use super::rule_set::RuleSet; use crate::model::{EventKind, LogEvent}; use regex::Regex; enum CompiledMatcher { MessagePrefix(String), MessageContains(String), MessageRegex(Regex), FieldEquals { name: String, value: String }, TargetPrefix(String), IsPanic, MinLevel(crate::model::LogLevel), All(Vec), Any(Vec), } fn compile(matcher: &Matcher) -> CompiledMatcher { match matcher { Matcher::MessagePrefix(p) => CompiledMatcher::MessagePrefix(p.clone()), Matcher::MessageContains(c) => CompiledMatcher::MessageContains(c.clone()), // RuleSet::new already verified the regex compiles; this cannot fail here. Matcher::MessageRegex(re) => CompiledMatcher::MessageRegex(Regex::new(re).expect("validated by RuleSet::new")), Matcher::FieldEquals { name, value } => CompiledMatcher::FieldEquals { name: name.clone(), value: value.clone(), }, Matcher::TargetPrefix(p) => CompiledMatcher::TargetPrefix(p.clone()), Matcher::IsPanic => CompiledMatcher::IsPanic, Matcher::MinLevel(level) => CompiledMatcher::MinLevel(*level), Matcher::All(inner) => CompiledMatcher::All(inner.iter().map(compile).collect()), Matcher::Any(inner) => CompiledMatcher::Any(inner.iter().map(compile).collect()), } } fn eval(matcher: &CompiledMatcher, event: &LogEvent) -> bool { match matcher { CompiledMatcher::MessagePrefix(p) => event.message.starts_with(p.as_str()), CompiledMatcher::MessageContains(c) => event.message.contains(c.as_str()), CompiledMatcher::MessageRegex(re) => re.is_match(&event.message), CompiledMatcher::FieldEquals { name, value } => event.field_display(name).as_deref() == Some(value.as_str()), CompiledMatcher::TargetPrefix(p) => event.target.as_deref().is_some_and(|t| t.starts_with(p.as_str())), CompiledMatcher::IsPanic => event.kind == EventKind::Panic, CompiledMatcher::MinLevel(level) => event.level.is_some_and(|l| l >= *level), CompiledMatcher::All(inner) => inner.iter().all(|m| eval(m, event)), CompiledMatcher::Any(inner) => inner.iter().any(|m| eval(m, event)), } } pub struct RuleEngine { set: RuleSet, compiled: Vec, } impl RuleEngine { pub fn new(set: RuleSet) -> Self { let compiled = set.rules().iter().map(|r| compile(&r.matcher)).collect(); Self { set, compiled } } /// Indexes (aligned with [`Self::rules`]) of every rule the event hits. /// An event may hit several rules; arbitration happens at report time. pub fn matches(&self, event: &LogEvent) -> Vec { self.compiled .iter() .enumerate() .filter(|(_, m)| eval(m, event)) .map(|(idx, _)| idx) .collect() } pub fn rules(&self) -> &[Rule] { self.set.rules() } }