use std::sync::atomic::{AtomicUsize, Ordering};
use crate::filters::{FilterDecision, StyleId};
use crate::parser::DisplayParts;
use crate::types::{FilterDef, FilterType};
pub const FIELD_PREFIX: &str = "@field:";
#[derive(Debug, Clone)]
pub struct FieldFilter {
pub field: String,
pub pattern: String,
pub decision: FilterDecision,
}
#[derive(Debug, Clone)]
pub struct FieldFilterStyle {
pub field_filter: FieldFilter,
pub style_id: StyleId,
pub match_only: bool,
}
pub fn parse_field_filter(expr: &str) -> Result<(String, String), String> {
let colon = expr
.find(':')
.ok_or_else(|| format!("field filter must be 'key:value', got: {expr}"))?;
let key = &expr[..colon];
let value = &expr[colon + 1..];
if key.is_empty() {
return Err("field name must not be empty".to_string());
}
if value.is_empty() {
return Err("field value must not be empty".to_string());
}
Ok((key.to_string(), value.to_string()))
}
pub fn extract_field_filters_ordered(filter_defs: &[FilterDef]) -> Vec<(String, String)> {
filter_defs
.iter()
.filter(|d| d.enabled)
.filter_map(|d| {
let expr = d.pattern.strip_prefix(FIELD_PREFIX)?;
parse_field_filter(expr).ok()
})
.collect()
}
pub fn count_field_filter_matches(
filters: &[(String, String)],
parts: Option<&DisplayParts<'_>>,
counts: &[AtomicUsize],
) {
let Some(parts) = parts else { return };
for (i, (field, pattern)) in filters.iter().enumerate() {
if resolve_field(field, parts)
.map(|v| v.contains(pattern.as_str()))
.unwrap_or(false)
&& let Some(c) = counts.get(i)
{
c.fetch_add(1, Ordering::Relaxed);
}
}
}
pub fn extract_field_filters(filter_defs: &[FilterDef]) -> (Vec<FieldFilter>, Vec<FieldFilter>) {
let mut includes = Vec::new();
let mut excludes = Vec::new();
for def in filter_defs {
if !def.enabled {
continue;
}
let Some(expr) = def.pattern.strip_prefix(FIELD_PREFIX) else {
continue;
};
let Ok((field, pattern)) = parse_field_filter(expr) else {
continue;
};
let decision = match def.filter_type {
FilterType::Include => FilterDecision::Include,
FilterType::Exclude => FilterDecision::Exclude,
};
let ff = FieldFilter {
field,
pattern,
decision,
};
match def.filter_type {
FilterType::Include => includes.push(ff),
FilterType::Exclude => excludes.push(ff),
}
}
(includes, excludes)
}
pub(crate) fn resolve_field<'a>(field: &str, parts: &'a DisplayParts<'a>) -> Option<&'a str> {
if let Some(span_key) = field.strip_prefix("span.") {
let span = parts.span.as_ref()?;
if span_key == "name" {
return Some(span.name);
}
return span
.fields
.iter()
.find(|(k, _)| *k == span_key)
.map(|(_, v)| *v);
}
if let Some(fields_key) = field.strip_prefix("fields.") {
return parts
.extra_fields
.iter()
.find(|(k, _)| *k == fields_key)
.map(|(_, v)| *v);
}
match field {
"level" | "lvl" => parts.level,
"timestamp" | "ts" | "time" => parts.timestamp,
"target" => parts.target,
"message" | "msg" => parts.message,
other => parts
.extra_fields
.iter()
.find(|(k, _)| *k == other)
.map(|(_, v)| *v),
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FieldVote {
Match,
Miss,
PassThrough,
}
pub fn any_field_exclude_matches(
excludes: &[FieldFilter],
parts: Option<&DisplayParts<'_>>,
) -> bool {
let Some(parts) = parts else {
return false; };
excludes.iter().any(|ff| {
resolve_field(&ff.field, parts)
.map(|v| v.contains(ff.pattern.as_str()))
.unwrap_or(false) })
}
pub fn field_include_vote(includes: &[FieldFilter], parts: Option<&DisplayParts<'_>>) -> FieldVote {
if includes.is_empty() {
return FieldVote::PassThrough;
}
let Some(parts) = parts else {
return FieldVote::PassThrough; };
for ff in includes {
if resolve_field(&ff.field, parts).is_some_and(|v| v.contains(ff.pattern.as_str())) {
return FieldVote::Match;
}
}
FieldVote::Miss
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::DisplayParts;
use crate::types::{FilterDef, FilterType};
#[test]
fn test_parse_field_filter_valid() {
let (k, v) = parse_field_filter("level:error").unwrap();
assert_eq!(k, "level");
assert_eq!(v, "error");
}
#[test]
fn test_parse_field_filter_colon_in_value() {
let (k, v) = parse_field_filter("level:info:extra").unwrap();
assert_eq!(k, "level");
assert_eq!(v, "info:extra");
}
#[test]
fn test_parse_field_filter_missing_colon() {
assert!(parse_field_filter("levelonly").is_err());
}
#[test]
fn test_parse_field_filter_empty_key() {
assert!(parse_field_filter(":error").is_err());
}
#[test]
fn test_parse_field_filter_empty_value() {
assert!(parse_field_filter("level:").is_err());
}
fn make_parts<'a>(
level: Option<&'a str>,
timestamp: Option<&'a str>,
message: Option<&'a str>,
target: Option<&'a str>,
extra: Vec<(&'a str, &'a str)>,
) -> DisplayParts<'a> {
DisplayParts {
level,
timestamp,
target,
message,
extra_fields: extra,
..Default::default()
}
}
fn inc(field: &str, pattern: &str) -> FieldFilter {
FieldFilter {
field: field.to_string(),
pattern: pattern.to_string(),
decision: FilterDecision::Include,
}
}
fn exc(field: &str, pattern: &str) -> FieldFilter {
FieldFilter {
field: field.to_string(),
pattern: pattern.to_string(),
decision: FilterDecision::Exclude,
}
}
#[test]
fn test_exclude_match_hides() {
let parts = make_parts(Some("debug"), None, None, None, vec![]);
assert!(any_field_exclude_matches(
&[exc("level", "debug")],
Some(&parts)
));
}
#[test]
fn test_exclude_no_match_passes() {
let parts = make_parts(Some("info"), None, None, None, vec![]);
assert!(!any_field_exclude_matches(
&[exc("level", "debug")],
Some(&parts)
));
}
#[test]
fn test_exclude_parts_none_passthrough() {
assert!(!any_field_exclude_matches(&[exc("level", "debug")], None));
}
#[test]
fn test_exclude_field_absent_passthrough() {
let parts = make_parts(None, None, None, None, vec![]);
assert!(!any_field_exclude_matches(
&[exc("level", "debug")],
Some(&parts)
));
}
#[test]
fn test_include_match_vote() {
let parts = make_parts(Some("error"), None, None, None, vec![]);
assert_eq!(
field_include_vote(&[inc("level", "error")], Some(&parts)),
FieldVote::Match
);
}
#[test]
fn test_include_no_match_vote_miss() {
let parts = make_parts(Some("info"), None, None, None, vec![]);
assert_eq!(
field_include_vote(&[inc("level", "error")], Some(&parts)),
FieldVote::Miss
);
}
#[test]
fn test_include_parts_none_passthrough() {
assert_eq!(
field_include_vote(&[inc("level", "error")], None),
FieldVote::PassThrough
);
}
#[test]
fn test_include_field_absent_is_miss() {
let parts = make_parts(None, None, None, None, vec![]);
assert_eq!(
field_include_vote(&[inc("level", "error")], Some(&parts)),
FieldVote::Miss
);
}
#[test]
fn test_two_includes_any_match() {
let parts = make_parts(Some("error"), None, None, Some("api"), vec![]);
assert_eq!(
field_include_vote(
&[inc("level", "error"), inc("target", "auth")],
Some(&parts)
),
FieldVote::Match
);
}
#[test]
fn test_two_includes_neither_match() {
let parts = make_parts(Some("info"), None, None, Some("api"), vec![]);
assert_eq!(
field_include_vote(
&[inc("level", "error"), inc("target", "auth")],
Some(&parts)
),
FieldVote::Miss
);
}
#[test]
fn test_extra_field_by_key_match() {
let parts = make_parts(None, None, None, None, vec![("component", "auth")]);
assert_eq!(
field_include_vote(&[inc("component", "auth")], Some(&parts)),
FieldVote::Match
);
}
#[test]
fn test_extra_field_by_key_miss() {
let parts = make_parts(None, None, None, None, vec![("component", "auth")]);
assert_eq!(
field_include_vote(&[inc("component", "api")], Some(&parts)),
FieldVote::Miss
);
}
#[test]
fn test_alias_lvl() {
let parts = make_parts(Some("warn"), None, None, None, vec![]);
assert_eq!(
field_include_vote(&[inc("lvl", "warn")], Some(&parts)),
FieldVote::Match
);
}
#[test]
fn test_alias_ts() {
let parts = make_parts(None, Some("2024-01-01"), None, None, vec![]);
assert_eq!(
field_include_vote(&[inc("ts", "2024")], Some(&parts)),
FieldVote::Match
);
}
#[test]
fn test_alias_msg() {
let parts = make_parts(None, None, Some("hello world"), None, vec![]);
assert_eq!(
field_include_vote(&[inc("msg", "hello")], Some(&parts)),
FieldVote::Match
);
}
fn make_parts_with_span<'a>(
extra: Vec<(&'a str, &'a str)>,
span_fields: Vec<(&'a str, &'a str)>,
) -> DisplayParts<'a> {
use crate::parser::SpanInfo;
DisplayParts {
span: Some(SpanInfo {
name: "req",
fields: span_fields,
}),
extra_fields: extra,
..Default::default()
}
}
#[test]
fn test_span_dotted_path_match() {
let parts = make_parts_with_span(vec![], vec![("method", "GET")]);
assert_eq!(
field_include_vote(&[inc("span.method", "GET")], Some(&parts)),
FieldVote::Match
);
}
#[test]
fn test_span_name_field_resolves() {
let parts = make_parts_with_span(vec![], vec![("method", "GET")]);
assert_eq!(resolve_field("span.name", &parts), Some("req"));
}
#[test]
fn test_span_dotted_path_miss() {
let parts = make_parts_with_span(vec![], vec![("method", "POST")]);
assert_eq!(
field_include_vote(&[inc("span.method", "GET")], Some(&parts)),
FieldVote::Miss
);
}
#[test]
fn test_span_dotted_path_absent_key() {
let parts = make_parts_with_span(vec![], vec![("uri", "/")]);
assert_eq!(
field_include_vote(&[inc("span.method", "GET")], Some(&parts)),
FieldVote::Miss
);
}
#[test]
fn test_fields_dotted_path_match() {
let parts = make_parts(None, None, None, None, vec![("order_id", "42")]);
assert_eq!(
field_include_vote(&[inc("fields.order_id", "42")], Some(&parts)),
FieldVote::Match
);
}
#[test]
fn test_fields_dotted_path_miss() {
let parts = make_parts(None, None, None, None, vec![("order_id", "99")]);
assert_eq!(
field_include_vote(&[inc("fields.order_id", "42")], Some(&parts)),
FieldVote::Miss
);
}
fn make_def(id: usize, pattern: &str, filter_type: FilterType, enabled: bool) -> FilterDef {
FilterDef {
id,
pattern: pattern.to_string(),
filter_type,
enabled,
color_config: None,
}
}
#[test]
fn test_extract_disabled_skipped() {
let defs = vec![make_def(
1,
"@field:level:error",
FilterType::Include,
false,
)];
let (inc, exc) = extract_field_filters(&defs);
assert!(inc.is_empty());
assert!(exc.is_empty());
}
#[test]
fn test_extract_non_field_prefix_skipped() {
let defs = vec![make_def(1, "level=error", FilterType::Include, true)];
let (inc, exc) = extract_field_filters(&defs);
assert!(inc.is_empty());
assert!(exc.is_empty());
}
#[test]
fn test_extract_malformed_skipped() {
let defs = vec![make_def(1, "@field:levelonly", FilterType::Include, true)];
let (inc, exc) = extract_field_filters(&defs);
assert!(inc.is_empty());
assert!(exc.is_empty());
}
#[test]
fn test_extract_include_exclude_split() {
let defs = vec![
make_def(1, "@field:level:error", FilterType::Include, true),
make_def(2, "@field:level:debug", FilterType::Exclude, true),
];
let (inc, exc) = extract_field_filters(&defs);
assert_eq!(inc.len(), 1);
assert_eq!(exc.len(), 1);
assert_eq!(inc[0].field, "level");
assert_eq!(inc[0].pattern, "error");
assert_eq!(exc[0].pattern, "debug");
}
#[test]
fn test_extract_field_filters_ordered_preserves_order() {
let defs = vec![
make_def(1, "@field:level:error", FilterType::Include, true),
make_def(2, "@field:level:debug", FilterType::Exclude, true),
make_def(3, "@field:target:api", FilterType::Include, true),
];
let ordered = extract_field_filters_ordered(&defs);
assert_eq!(ordered.len(), 3);
assert_eq!(ordered[0], ("level".to_string(), "error".to_string()));
assert_eq!(ordered[1], ("level".to_string(), "debug".to_string()));
assert_eq!(ordered[2], ("target".to_string(), "api".to_string()));
}
#[test]
fn test_extract_field_filters_ordered_skips_disabled() {
let defs = vec![
make_def(1, "@field:level:error", FilterType::Include, true),
make_def(2, "@field:level:debug", FilterType::Exclude, false),
];
let ordered = extract_field_filters_ordered(&defs);
assert_eq!(ordered.len(), 1);
assert_eq!(ordered[0].0, "level");
}
#[test]
fn test_count_field_filter_matches_increments_on_match() {
use std::sync::atomic::AtomicUsize;
let parts = make_parts(Some("error"), None, None, None, vec![]);
let filters = vec![("level".to_string(), "error".to_string())];
let counts = vec![AtomicUsize::new(0)];
count_field_filter_matches(&filters, Some(&parts), &counts);
assert_eq!(counts[0].load(Ordering::Relaxed), 1);
}
#[test]
fn test_count_field_filter_matches_no_increment_on_miss() {
use std::sync::atomic::AtomicUsize;
let parts = make_parts(Some("info"), None, None, None, vec![]);
let filters = vec![("level".to_string(), "error".to_string())];
let counts = vec![AtomicUsize::new(0)];
count_field_filter_matches(&filters, Some(&parts), &counts);
assert_eq!(counts[0].load(Ordering::Relaxed), 0);
}
#[test]
fn test_count_field_filter_matches_no_parts_skips() {
use std::sync::atomic::AtomicUsize;
let filters = vec![("level".to_string(), "error".to_string())];
let counts = vec![AtomicUsize::new(0)];
count_field_filter_matches(&filters, None, &counts);
assert_eq!(counts[0].load(Ordering::Relaxed), 0);
}
#[test]
fn test_count_field_filter_matches_multiple_filters() {
use std::sync::atomic::AtomicUsize;
let parts = make_parts(Some("error"), None, Some("crash"), None, vec![]);
let filters = vec![
("level".to_string(), "error".to_string()),
("message".to_string(), "crash".to_string()),
("level".to_string(), "debug".to_string()),
];
let counts = vec![
AtomicUsize::new(0),
AtomicUsize::new(0),
AtomicUsize::new(0),
];
count_field_filter_matches(&filters, Some(&parts), &counts);
assert_eq!(counts[0].load(Ordering::Relaxed), 1);
assert_eq!(counts[1].load(Ordering::Relaxed), 1);
assert_eq!(counts[2].load(Ordering::Relaxed), 0);
}
}