1use tower_rules::{FieldFilter, FederationPolicy, FieldOp, HealthSignal, LevelFilter,
11 RatePredicate, ScopeFilter, StringFilter,
12};
13
14use crate::event::{health_signal_name, Event, EventScope, Level};
15
16#[derive(Debug, Clone, PartialEq)]
23pub enum ScryerFilter {
24 Fields(FieldSet),
26 And(Vec<ScryerFilter>),
28 Or(Vec<ScryerFilter>),
30 Not(Box<ScryerFilter>),
32 Any,
34 HealthSignal(HealthSignal),
37}
38
39#[derive(Debug, Clone, PartialEq)]
45pub struct FieldSet {
46 pub scope: ScopeFilter,
47 pub level: Option<LevelFilter>,
48 pub target: Option<StringFilter>,
49 pub fields: Vec<FieldFilter>,
50 pub rate: Option<RatePredicate>,
53 pub federation: FederationPolicy,
55}
56
57impl ScryerFilter {
60 pub fn matches(&self, event: &Event) -> bool {
66 match self {
67 ScryerFilter::Any => true,
68 ScryerFilter::HealthSignal(signal) => {
69 event.scope == EventScope::Health
70 && event.target
71 == format!("scryer.health.{}", health_signal_name(*signal))
72 }
73 ScryerFilter::Fields(fs) => fs.matches(event),
74 ScryerFilter::And(children) => children.iter().all(|f| f.matches(event)),
75 ScryerFilter::Or(children) => children.iter().any(|f| f.matches(event)),
76 ScryerFilter::Not(child) => !child.matches(event),
77 }
78 }
79}
80
81impl FieldSet {
82 fn matches(&self, event: &Event) -> bool {
83 if !scope_matches(&self.scope, &event.scope) {
84 return false;
85 }
86 if let Some(min) = self.level {
87 if event.level < level_filter_to_level(min) {
88 return false;
89 }
90 }
91 if let Some(target_filter) = &self.target {
92 if !string_filter_matches(target_filter, &event.target) {
93 return false;
94 }
95 }
96 for ff in &self.fields {
97 if !field_filter_matches(ff, event) {
98 return false;
99 }
100 }
101 true
102 }
104}
105
106fn scope_matches(filter: &ScopeFilter, scope: &EventScope) -> bool {
109 match filter {
110 ScopeFilter::Any => true,
111 ScopeFilter::Federated => true, ScopeFilter::TaskRun { id: None } => matches!(scope, EventScope::TaskRun(_)),
113 ScopeFilter::TaskRun { id: Some(want) } => {
114 matches!(scope, EventScope::TaskRun(got) if got == want)
115 }
116 ScopeFilter::Service { ident: None } => matches!(scope, EventScope::Service(_)),
117 ScopeFilter::Service { ident: Some(want) } => {
118 matches!(scope, EventScope::Service(got) if got == want)
119 }
120 ScopeFilter::Forge { id: None } => matches!(scope, EventScope::Forge(_)),
121 ScopeFilter::Forge { id: Some(want) } => {
122 matches!(scope, EventScope::Forge(got) if got == want)
123 }
124 }
125}
126
127fn level_filter_to_level(f: LevelFilter) -> Level {
130 match f {
131 LevelFilter::Debug => Level::Debug,
132 LevelFilter::Info => Level::Info,
133 LevelFilter::Warn => Level::Warn,
134 LevelFilter::Error => Level::Error,
135 }
136}
137
138pub(crate) fn string_filter_matches(filter: &StringFilter, s: &str) -> bool {
141 match filter {
142 StringFilter::Exact { value } => s == value,
143 StringFilter::Glob { pattern } => glob_matches(pattern, s),
144 StringFilter::Regex { pattern } => {
145 regex::Regex::new(pattern).map(|re| re.is_match(s)).unwrap_or(false)
146 }
147 }
148}
149
150fn glob_matches(pattern: &str, s: &str) -> bool {
153 glob_matches_inner(pattern.as_bytes(), s.as_bytes())
154}
155
156fn glob_matches_inner(pat: &[u8], s: &[u8]) -> bool {
157 match (pat.split_first(), s.split_first()) {
158 (None, None) => true,
159 (None, Some(_)) => false,
160 (Some((&b'*', rest_pat)), _) => {
161 (0..=s.len()).any(|i| glob_matches_inner(rest_pat, &s[i..]))
163 }
164 (Some((&b'?', rest_pat)), Some((_, rest_s))) => {
165 glob_matches_inner(rest_pat, rest_s)
166 }
167 (Some((pc, rest_pat)), Some((sc, rest_s))) if pc == sc => {
168 glob_matches_inner(rest_pat, rest_s)
169 }
170 _ => false,
171 }
172}
173
174fn field_filter_matches(ff: &FieldFilter, event: &Event) -> bool {
177 let value = resolve_path(&event.fields, &ff.path);
178 match &ff.op {
179 FieldOp::Exists => value.is_some(),
180 FieldOp::Eq { value: want } => value.map(|v| v == want).unwrap_or(false),
181 FieldOp::Contains { substring } => value
182 .and_then(|v| v.as_str())
183 .map(|s| s.contains(substring.as_str()))
184 .unwrap_or(false),
185 FieldOp::Matches { filter } => value
186 .and_then(|v| v.as_str())
187 .map(|s| string_filter_matches(filter, s))
188 .unwrap_or(false),
189 }
190}
191
192fn resolve_path<'a>(
197 fields: &'a std::collections::HashMap<String, serde_json::Value>,
198 path: &str,
199) -> Option<&'a serde_json::Value> {
200 let mut parts = path.splitn(2, '.');
201 let key = parts.next()?;
202 let value = fields.get(key)?;
203 match parts.next() {
204 None => Some(value),
205 Some(rest) => resolve_json_path(value, rest),
206 }
207}
208
209fn resolve_json_path<'a>(value: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
211 let mut parts = path.splitn(2, '.');
212 let key = parts.next()?;
213 let child = value.get(key)?;
214 match parts.next() {
215 None => Some(child),
216 Some(rest) => resolve_json_path(child, rest),
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn glob_star_matches_segment() {
226 assert!(glob_matches("database.*", "database.query_slow"));
227 assert!(glob_matches("database.*", "database.connection_failed"));
228 assert!(!glob_matches("database.*", "redis.timeout"));
229 }
230
231 #[test]
232 fn glob_star_matches_empty() {
233 assert!(glob_matches("raft*", "raft"));
234 assert!(glob_matches("raft*", "raft.term_change"));
235 }
236
237 #[test]
238 fn glob_question_mark() {
239 assert!(glob_matches("ra?t", "raft"));
240 assert!(!glob_matches("ra?t", "rat"));
241 }
242
243 #[test]
244 fn glob_exact_no_wildcards() {
245 assert!(glob_matches("raft.quorum_lost", "raft.quorum_lost"));
246 assert!(!glob_matches("raft.quorum_lost", "raft.quorum_los"));
247 }
248}