agentsight_capture/analyzers/
http_filter.rs1use super::filter_base::{FilterBase, FilterExpr, MetricsStrategy};
5use super::{Analyzer, AnalyzerError};
6use crate::runners::EventStream;
7use async_trait::async_trait;
8use serde_json::Value;
9
10static GLOBAL_METRICS: super::filter_metrics::MetricsSlot = std::sync::OnceLock::new();
11
12pub fn print_global_http_filter_metrics() {
13 super::filter_metrics::print("HTTPFilter", &GLOBAL_METRICS);
14}
15
16pub struct HTTPFilter {
17 base: FilterBase<HttpFilterExpr>,
18}
19
20#[derive(Debug, Clone)]
21pub struct HttpFilterExpr {
22 parsed: FilterNode,
23}
24
25#[derive(Debug, Clone)]
26enum FilterNode {
27 And(Vec<FilterNode>),
28 Or(Vec<FilterNode>),
29 Condition {
30 target: String,
31 field: String,
32 operator: String,
33 value: String,
34 },
35 Empty,
36}
37
38impl FilterExpr for HttpFilterExpr {
39 fn evaluate(&self, data: &Value) -> bool {
40 evaluate_node(&self.parsed, data)
41 }
42}
43
44impl HTTPFilter {
45 pub fn with_patterns(patterns: Vec<String>) -> Self {
46 Self {
47 base: FilterBase::new("http_parser", MetricsStrategy::SetPerEvent, &GLOBAL_METRICS)
48 .with_patterns(patterns, HttpFilterExpr::parse),
49 }
50 }
51}
52
53#[async_trait]
54impl Analyzer for HTTPFilter {
55 async fn process(&mut self, stream: EventStream) -> Result<EventStream, AnalyzerError> {
56 self.base.process(stream).await
57 }
58}
59
60impl HttpFilterExpr {
63 pub fn parse(expression: &str) -> Self {
64 let trimmed = expression.trim();
65 if trimmed.is_empty() {
66 return Self {
67 parsed: FilterNode::Empty,
68 };
69 }
70 Self {
71 parsed: parse_or(trimmed),
72 }
73 }
74}
75
76fn parse_or(expr: &str) -> FilterNode {
77 let parts: Vec<&str> = expr.split('|').map(str::trim).collect();
78 if parts.len() > 1 {
79 FilterNode::Or(parts.into_iter().map(parse_and).collect())
80 } else {
81 parse_and(expr)
82 }
83}
84
85fn parse_and(expr: &str) -> FilterNode {
86 let parts: Vec<&str> = expr.split('&').map(str::trim).collect();
87 if parts.len() > 1 {
88 FilterNode::And(parts.into_iter().map(parse_single).collect())
89 } else {
90 parse_single(expr)
91 }
92}
93
94fn parse_single(cond: &str) -> FilterNode {
95 let cond = cond.trim();
96 if !cond.contains('=') {
97 return FilterNode::Condition {
98 target: "request".into(),
99 field: "path".into(),
100 operator: "contains".into(),
101 value: cond.into(),
102 };
103 }
104 let Some((key, value)) = cond.split_once('=') else {
105 return FilterNode::Empty;
106 };
107 let (key, value) = (key.trim(), value.trim());
108
109 if let Some((target_raw, field)) = key.split_once('.') {
110 let (target, operator) = match target_raw.trim() {
111 "request" | "req" => {
112 let op = match field.trim() {
113 "path_prefix" | "path_starts_with" => "prefix",
114 "path_contains" | "path_includes" => "contains",
115 _ => "exact",
116 };
117 ("request", op)
118 }
119 "response" | "resp" | "res" => ("response", "exact"),
120 _ => ("request", "exact"),
121 };
122 FilterNode::Condition {
123 target: target.into(),
124 field: field.trim().into(),
125 operator: operator.into(),
126 value: value.into(),
127 }
128 } else {
129 let operator = match key {
130 "path_prefix" | "path_starts_with" => "prefix",
131 "path_contains" | "path_includes" => "contains",
132 _ => "exact",
133 };
134 FilterNode::Condition {
135 target: "request".into(),
136 field: key.into(),
137 operator: operator.into(),
138 value: value.into(),
139 }
140 }
141}
142
143fn evaluate_node(node: &FilterNode, data: &Value) -> bool {
146 match node {
147 FilterNode::Empty => false,
148 FilterNode::And(cs) => cs.iter().all(|c| evaluate_node(c, data)),
149 FilterNode::Or(cs) => cs.iter().any(|c| evaluate_node(c, data)),
150 FilterNode::Condition {
151 target,
152 field,
153 operator,
154 value,
155 } => {
156 let msg_type = data
157 .get("message_type")
158 .and_then(|v| v.as_str())
159 .unwrap_or("");
160 let matches_target = match target.as_str() {
161 "request" => msg_type == "request",
162 "response" => msg_type == "response",
163 _ => false,
164 };
165 if !matches_target {
166 return false;
167 }
168 if target == "request" {
169 eval_request(field, operator, value, data)
170 } else {
171 eval_response(field, value, data)
172 }
173 }
174 }
175}
176
177fn eval_request(field: &str, operator: &str, value: &str, data: &Value) -> bool {
178 match field {
179 "method" | "verb" => {
180 let m = data.get("method").and_then(|v| v.as_str()).unwrap_or("");
181 m.eq_ignore_ascii_case(value)
182 }
183 "path" | "path_exact" => {
184 let p = data.get("path").and_then(|v| v.as_str()).unwrap_or("");
185 match operator {
186 "prefix" => p.starts_with(value),
187 "contains" => p.contains(value),
188 _ => p == value,
189 }
190 }
191 "path_prefix" | "path_starts_with" => data
192 .get("path")
193 .and_then(|v| v.as_str())
194 .unwrap_or("")
195 .starts_with(value),
196 "path_contains" | "path_includes" => data
197 .get("path")
198 .and_then(|v| v.as_str())
199 .unwrap_or("")
200 .contains(value),
201 "host" | "hostname" => header_val(data, "host").unwrap_or("") == value,
202 "body" | "body_contains" => data
203 .get("body")
204 .and_then(|v| v.as_str())
205 .unwrap_or("")
206 .contains(value),
207 _ => {
208 let path = data.get("path").and_then(|v| v.as_str()).unwrap_or("");
209 path.split_once('?')
210 .map(|(_, q)| q.contains(&format!("{field}={value}")))
211 .unwrap_or(false)
212 }
213 }
214}
215
216fn eval_response(field: &str, value: &str, data: &Value) -> bool {
217 match field {
218 "status_code" | "status" | "code" => {
219 let sc = data
220 .get("status_code")
221 .and_then(|v| v.as_u64())
222 .unwrap_or(0);
223 value.parse::<u64>().ok().is_some_and(|v| sc == v)
224 }
225 "status_text" | "status_message" => {
226 let t = data
227 .get("status_text")
228 .and_then(|v| v.as_str())
229 .unwrap_or("");
230 t.to_lowercase().contains(&value.to_lowercase())
231 }
232 "content_type" | "content-type" => header_val(data, "content-type")
233 .unwrap_or("")
234 .contains(value),
235 "body" | "body_contains" => data
236 .get("body")
237 .and_then(|v| v.as_str())
238 .unwrap_or("")
239 .contains(value),
240 _ => header_val(data, field).unwrap_or("").contains(value),
241 }
242}
243
244fn header_val<'a>(data: &'a Value, name: &str) -> Option<&'a str> {
245 data.get("headers")?.get(name)?.as_str()
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use serde_json::json;
252
253 #[test]
254 fn test_expression_parsing() {
255 let expr = HttpFilterExpr::parse("request.path=/health");
256 assert!(expr.evaluate(&json!({"message_type": "request", "path": "/health"})));
257 assert!(!expr.evaluate(&json!({"message_type": "request", "path": "/api"})));
258 }
259
260 #[test]
261 fn test_request_filtering() {
262 let f = HttpFilterExpr::parse("request.method=GET");
263 assert!(f.evaluate(&json!({"message_type": "request", "method": "GET"})));
264 assert!(!f.evaluate(&json!({"message_type": "request", "method": "POST"})));
265 }
266
267 #[test]
268 fn test_response_filtering() {
269 let f = HttpFilterExpr::parse("response.status_code=404");
270 assert!(f.evaluate(&json!({"message_type": "response", "status_code": 404})));
271 assert!(!f.evaluate(&json!({"message_type": "response", "status_code": 200})));
272 }
273
274 #[test]
275 fn test_complex_expressions() {
276 let f = HttpFilterExpr::parse("request.method=GET | response.status_code=404");
277 assert!(f.evaluate(&json!({"message_type": "request", "method": "GET"})));
278 assert!(f.evaluate(&json!({"message_type": "response", "status_code": 404})));
279 assert!(!f.evaluate(&json!({"message_type": "request", "method": "POST"})));
280 }
281}