Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bd-log-matcher/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ tempfile.workspace = true
assert_matches.workspace = true
bd-test-helpers.path = "../bd-test-helpers"
matches.workspace = true
ordered-float.workspace = true
pretty_assertions.workspace = true
time.workspace = true
tokio.workspace = true
100 changes: 84 additions & 16 deletions bd-log-matcher/src/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,22 +144,12 @@ impl Tree {
.try_into()
.is_ok_and(|log_level| log_level_matcher.evaluate(log_level, extracted_fields)),
Leaf::LogType(l_type) => *l_type == log_type as u32,
Leaf::IntValue(input, criteria) =>
{
#[allow(clippy::cast_possible_truncation)]
input.get(message, fields, state).is_some_and(|input| {
input
.parse::<f64>()
.is_ok_and(|v| criteria.evaluate(v as i32, extracted_fields))
})
},
Leaf::DoubleValue(input, criteria) => {
input.get(message, fields, state).is_some_and(|input| {
input
.parse()
.is_ok_and(|v| criteria.evaluate(v, extracted_fields))
})
},
Leaf::IntValue(input, criteria) => input
.get_as_i32(message, fields, state)
.is_some_and(|v| criteria.evaluate(v, extracted_fields)),
Leaf::DoubleValue(input, criteria) => input
.get_as_f64(message, fields, state)
.is_some_and(|v| criteria.evaluate(v, extracted_fields)),
Leaf::StringValue(input, criteria) => input
.get(message, fields, state)
.is_some_and(|input| criteria.evaluate(input.as_ref(), extracted_fields)),
Expand Down Expand Up @@ -245,6 +235,84 @@ impl InputType {
}),
}
}

/// Extracts a value as i32 for use with `IntMatch`. Handles numeric `DataValue` types directly,
/// falling back to string parsing for string types.
#[allow(clippy::cast_possible_truncation)]
fn get_as_i32(
&self,
message: &LogMessage,
fields: FieldsRef<'_>,
state: &dyn bd_state::StateReader,
) -> Option<i32> {
match self {
Self::Message => message.as_str().and_then(|s| s.parse().ok()),
Self::Field(field_key) => {
let field = fields.field(field_key)?;
match field {
DataValue::I64(v) => i32::try_from(*v).ok(),
DataValue::U64(v) => i32::try_from(*v).ok(),
DataValue::Double(v) => Some(**v as i32),
DataValue::String(_) | DataValue::SharedString(_) | DataValue::StaticString(_) => {
// Parse as f64 first then truncate to preserve backward compatibility with strings
// like "13.0" that were previously accepted.
Some(field.as_str()?.parse::<f64>().ok()? as i32)
},
DataValue::Bytes(_) | DataValue::Boolean(_) | DataValue::Map(_) | DataValue::Array(_) => {
None
},
}
},
Self::State(scope, flag_key) => {
use bd_state::Value_type;
let v = state.get(*scope, flag_key)?;
match v.value_type {
Some(Value_type::IntValue(i)) => i32::try_from(i).ok(),
Some(Value_type::DoubleValue(d)) => Some(d as i32),
Some(Value_type::StringValue(ref s)) => s.parse().ok(),
Some(Value_type::BoolValue(_)) | None => None,
}
},
}
}

/// Extracts a value as f64 for use with `DoubleMatch`. Handles numeric `DataValue` types
/// directly, falling back to string parsing for string types.
#[allow(clippy::cast_precision_loss)]
fn get_as_f64(
&self,
message: &LogMessage,
fields: FieldsRef<'_>,
state: &dyn bd_state::StateReader,
) -> Option<f64> {
match self {
Self::Message => message.as_str().and_then(|s| s.parse().ok()),
Self::Field(field_key) => {
let field = fields.field(field_key)?;
match field {
DataValue::Double(v) => Some(**v),
DataValue::I64(v) => Some(*v as f64),
DataValue::U64(v) => Some(*v as f64),
DataValue::String(_) | DataValue::SharedString(_) | DataValue::StaticString(_) => {
field.as_str()?.parse().ok()
},
DataValue::Bytes(_) | DataValue::Boolean(_) | DataValue::Map(_) | DataValue::Array(_) => {
None
},
}
},
Self::State(scope, flag_key) => {
use bd_state::Value_type;
let v = state.get(*scope, flag_key)?;
match v.value_type {
Some(Value_type::DoubleValue(d)) => Some(d),
Some(Value_type::IntValue(i)) => Some(i as f64),
Some(Value_type::StringValue(ref s)) => s.parse().ok(),
Some(Value_type::BoolValue(_)) | None => None,
}
},
}
}
}

/// Describes a compiled leaf node in the match tree. Each tree node evaluates to either
Expand Down
171 changes: 171 additions & 0 deletions bd-log-matcher/src/matcher_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use log_matcher::base_log_matcher::tag_match::Value_match::{
StringValueMatch,
};
use log_matcher::{BaseLogMatcher, Matcher, MatcherList, base_log_matcher};
use ordered_float::NotNan;
use pretty_assertions::assert_eq;
use protobuf::{Enum, MessageField};

Expand Down Expand Up @@ -92,6 +93,21 @@ fn map_log_tag(key: &'static str, value: DataValue) -> Input<'static> {
)
}

fn i64_log_tag(key: &'static str, value: i64) -> Input<'static> {
map_log_tag(key, DataValue::I64(value))
}

fn u64_log_tag(key: &'static str, value: u64) -> Input<'static> {
map_log_tag(key, DataValue::U64(value))
}

fn double_log_tag(key: &'static str, value: f64) -> Input<'static> {
map_log_tag(
key,
DataValue::Double(NotNan::new(value).expect("test value must not be NaN")),
)
}

fn log_type(log_type: LogType) -> Input<'static> {
(
log_type,
Expand Down Expand Up @@ -1571,3 +1587,158 @@ fn match_test_runner_with_extractions(
);
}
}

#[test]
fn int_matcher_with_i64_field() {
let config = simple_log_matcher(TagMatch(base_log_matcher::TagMatch {
tag_key: "key".to_string(),
value_match: Some(IntValueMatch(
bd_proto::protos::value_matcher::value_matcher::IntValueMatch {
operator: Operator::OPERATOR_EQUALS.into(),
int_value_match_type: Some(Int_value_match_type::MatchValue(42)),
..Default::default()
},
)),
..Default::default()
}));

match_test_runner(
config,
vec![
(i64_log_tag("key", 42), true),
(i64_log_tag("key", 41), false),
(i64_log_tag("key", 43), false),
(log_tag("key", "42"), true),
],
);
}

#[test]
fn int_matcher_with_u64_field() {
let config = simple_log_matcher(TagMatch(base_log_matcher::TagMatch {
tag_key: "key".to_string(),
value_match: Some(IntValueMatch(
bd_proto::protos::value_matcher::value_matcher::IntValueMatch {
operator: Operator::OPERATOR_LESS_THAN.into(),
int_value_match_type: Some(Int_value_match_type::MatchValue(100)),
..Default::default()
},
)),
..Default::default()
}));

match_test_runner(
config,
vec![
(u64_log_tag("key", 50), true),
(u64_log_tag("key", 100), false),
(u64_log_tag("key", 150), false),
],
);
}

#[test]
fn int_matcher_with_double_field() {
let config = simple_log_matcher(TagMatch(base_log_matcher::TagMatch {
tag_key: "key".to_string(),
value_match: Some(IntValueMatch(
bd_proto::protos::value_matcher::value_matcher::IntValueMatch {
operator: Operator::OPERATOR_EQUALS.into(),
int_value_match_type: Some(Int_value_match_type::MatchValue(42)),
..Default::default()
},
)),
..Default::default()
}));

match_test_runner(
config,
vec![
(double_log_tag("key", 42.0), true),
(double_log_tag("key", 42.9), true),
(double_log_tag("key", 41.1), false),
],
);
}

#[test]
fn double_matcher_with_double_field() {
fn make_config(match_value: f64, operator: Operator) -> LogMatcher {
simple_log_matcher(TagMatch(base_log_matcher::TagMatch {
tag_key: "key".to_string(),
value_match: Some(DoubleValueMatch(
bd_proto::protos::value_matcher::value_matcher::DoubleValueMatch {
operator: operator.into(),
double_value_match_type: Some(Double_value_match_type::MatchValue(match_value)),
..Default::default()
},
)),
..Default::default()
}))
}

match_test_runner(
make_config(12.0, Operator::OPERATOR_LESS_THAN_OR_EQUAL),
vec![
(double_log_tag("key", 13.0), false),
(double_log_tag("key", 12.0), true),
(double_log_tag("key", 11.0), true),
],
);

match_test_runner(
make_config(12.5, Operator::OPERATOR_EQUALS),
vec![
(double_log_tag("key", 12.5), true),
(double_log_tag("key", 12.0), false),
(log_tag("key", "12.5"), true),
],
);
}

#[test]
fn double_matcher_with_i64_field() {
let config = simple_log_matcher(TagMatch(base_log_matcher::TagMatch {
tag_key: "key".to_string(),
value_match: Some(DoubleValueMatch(
bd_proto::protos::value_matcher::value_matcher::DoubleValueMatch {
operator: Operator::OPERATOR_GREATER_THAN.into(),
double_value_match_type: Some(Double_value_match_type::MatchValue(10.5)),
..Default::default()
},
)),
..Default::default()
}));

match_test_runner(
config,
vec![
(i64_log_tag("key", 11), true),
(i64_log_tag("key", 10), false),
(i64_log_tag("key", 100), true),
],
);
}

#[test]
fn double_matcher_with_u64_field() {
let config = simple_log_matcher(TagMatch(base_log_matcher::TagMatch {
tag_key: "key".to_string(),
value_match: Some(DoubleValueMatch(
bd_proto::protos::value_matcher::value_matcher::DoubleValueMatch {
operator: Operator::OPERATOR_EQUALS.into(),
double_value_match_type: Some(Double_value_match_type::MatchValue(100.0)),
..Default::default()
},
)),
..Default::default()
}));

match_test_runner(
config,
vec![
(u64_log_tag("key", 100), true),
(u64_log_tag("key", 99), false),
],
);
}
27 changes: 23 additions & 4 deletions bd-log-primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,21 @@ impl DataValue {
}
}

/// Returns the value as a string, converting numeric types to their string representation.
/// Returns `None` for `Bytes`, `Boolean`, `Map`, and `Array` variants.
#[must_use]
pub fn to_string_value(&self) -> Option<Cow<'_, str>> {
match self {
Self::String(s) => Some(Cow::Borrowed(s.as_ref())),
Self::SharedString(s) => Some(Cow::Borrowed(s.as_ref())),
Self::StaticString(s) => Some(Cow::Borrowed(s)),
Self::I64(v) => Some(Cow::Owned(v.to_string())),
Self::U64(v) => Some(Cow::Owned(v.to_string())),
Self::Double(v) => Some(Cow::Owned(v.to_string())),
Self::Bytes(_) | Self::Boolean(_) | Self::Map(_) | Self::Array(_) => None,
}
}

/// Extracts the underlying bytes if the enum represents a Bytes, None otherwise.
#[must_use]
pub fn as_bytes(&self) -> Option<&[u8]> {
Expand Down Expand Up @@ -899,20 +914,24 @@ impl<'a> FieldsRef<'a> {
}

/// Looks up the field value corresponding to the provided key. If the field doesn't exist or
/// contains a binary value, None is returned.
/// contains a binary value, None is returned. Numeric types are converted to their string
/// representation.
#[must_use]
pub fn field_value(&self, field_key: &str) -> Option<Cow<'a, str>> {
// In cases where there are conflicts between the keys of captured and matching fields, captured
// fields take precedence, as they are potentially stored and uploaded to the remote server.
if let Some(value) = self
.captured_fields
.get(field_key)
.and_then(|value| value.as_str())
.and_then(DataValue::to_string_value)
{
return Some(Cow::Borrowed(value));
return Some(value);
}

self.matching_field_value(field_key).map(Cow::Borrowed)
self
.matching_fields
.get(field_key)
.and_then(DataValue::to_string_value)
}

#[must_use]
Expand Down
Loading
Loading