1use std::collections::BTreeSet;
6use std::net::IpAddr;
7
8use ipnetwork::IpNetwork;
9use relay_pattern::{CaseInsensitive, TypedPatterns};
10use serde::{Deserialize, Deserializer, Serialize};
11use serde_json::Value;
12
13use crate::{Getter, Val};
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
17#[serde(rename_all = "camelCase")]
18pub struct EqCondOptions {
19 #[serde(default)]
23 pub ignore_case: bool,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct EqCondition {
35 pub name: String,
37
38 pub value: Value,
43
44 #[serde(default, skip_serializing_if = "is_default")]
46 pub options: EqCondOptions,
47}
48
49impl EqCondition {
50 pub fn new(field: impl Into<String>, value: impl Into<Value>) -> Self {
57 Self {
58 name: field.into(),
59 value: value.into(),
60 options: EqCondOptions { ignore_case: false },
61 }
62 }
63
64 pub fn ignore_case(mut self) -> Self {
68 self.options.ignore_case = true;
69 self
70 }
71
72 fn cmp(&self, left: &str, right: &str) -> bool {
73 if self.options.ignore_case {
74 unicase::eq(left, right)
75 } else {
76 left == right
77 }
78 }
79
80 fn matches<T>(&self, instance: &T) -> bool
81 where
82 T: Getter + ?Sized,
83 {
84 match (instance.get_value(self.name.as_str()), &self.value) {
85 (None, Value::Null) => true,
86 (Some(Val::String(f)), Value::String(val)) => self.cmp(f, val),
87 (Some(Val::String(f)), Value::Array(arr)) => arr
88 .iter()
89 .filter_map(|v| v.as_str())
90 .any(|v| self.cmp(v, f)),
91 (Some(Val::HexId(f)), Value::String(val)) => f.match_str(val),
92 (Some(Val::IpAddr(f)), Value::String(val)) => val.parse::<IpAddr>() == Ok(f),
93 (Some(Val::Bool(f)), Value::Bool(v)) => f == *v,
94 _ => false,
95 }
96 }
97}
98
99fn is_default<T: Default + PartialEq>(t: &T) -> bool {
101 *t == T::default()
102}
103
104macro_rules! impl_cmp_condition {
105 ($struct_name:ident, $operator:tt, $doc:literal) => {
106 #[doc = $doc]
107 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110 pub struct $struct_name {
111 pub name: String,
113 pub value: Value,
115 }
116
117 impl $struct_name {
118 pub fn new(field: impl Into<String>, value: impl Into<Value>) -> Self {
120 Self {
121 name: field.into(),
122 value: value.into(),
123 }
124 }
125
126 fn matches<T>(&self, instance: &T) -> bool
127 where
128 T: Getter + ?Sized,
129 {
130 let Some(value) = instance.get_value(self.name.as_str()) else {
131 return false;
132 };
133
134 if let (Some(a), Some(b)) = (value.as_i64(), self.value.as_i64()) {
139 a $operator b
140 } else if let (Some(a), Some(b)) = (value.as_u64(), self.value.as_u64()) {
141 a $operator b
142 } else if let (Some(a), Some(b)) = (value.as_f64(), self.value.as_f64()) {
143 a $operator b
144 } else if let (Some(a), Some(b)) = (value.as_str(), self.value.as_str()) {
145 a $operator b
146 } else {
147 false
148 }
149 }
150 }
151 }
152}
153
154impl_cmp_condition!(GteCondition, >=, "A condition that applies `>=`.");
155impl_cmp_condition!(LteCondition, <=, "A condition that applies `<=`.");
156impl_cmp_condition!(GtCondition, >, "A condition that applies `>`.");
157impl_cmp_condition!(LtCondition, <, "A condition that applies `<`.");
158
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct GlobCondition {
166 pub name: String,
168 pub value: TypedPatterns<CaseInsensitive>,
172}
173
174impl GlobCondition {
175 pub fn new(field: impl Into<String>, value: impl IntoStrings) -> Self {
177 Self {
178 name: field.into(),
179 value: TypedPatterns::from(value.into_strings()),
180 }
181 }
182
183 fn matches<T>(&self, instance: &T) -> bool
184 where
185 T: Getter + ?Sized,
186 {
187 match instance.get_value(self.name.as_str()) {
188 Some(Val::String(s)) => self.value.is_match(s),
189 _ => false,
190 }
191 }
192}
193
194pub trait IntoStrings {
196 fn into_strings(self) -> Vec<String>;
198}
199
200impl IntoStrings for &'_ str {
201 fn into_strings(self) -> Vec<String> {
202 vec![self.to_owned()]
203 }
204}
205
206impl IntoStrings for String {
207 fn into_strings(self) -> Vec<String> {
208 vec![self]
209 }
210}
211
212impl IntoStrings for std::borrow::Cow<'_, str> {
213 fn into_strings(self) -> Vec<String> {
214 vec![self.into_owned()]
215 }
216}
217
218impl IntoStrings for &'_ [&'_ str] {
219 fn into_strings(self) -> Vec<String> {
220 self.iter().copied().map(str::to_owned).collect()
221 }
222}
223
224impl IntoStrings for &'_ [String] {
225 fn into_strings(self) -> Vec<String> {
226 self.to_vec()
227 }
228}
229
230impl IntoStrings for Vec<&'_ str> {
231 fn into_strings(self) -> Vec<String> {
232 self.into_iter().map(str::to_owned).collect()
233 }
234}
235
236impl IntoStrings for Vec<String> {
237 fn into_strings(self) -> Vec<String> {
238 self
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Default, Serialize)]
247#[serde(transparent)]
248pub struct IpNetworks(BTreeSet<IpNetwork>);
249
250impl IpNetworks {
251 pub fn contains(&self, ip: IpAddr) -> bool {
253 self.0.iter().any(|network| network.contains(ip))
254 }
255}
256
257impl<S: AsRef<str>> FromIterator<S> for IpNetworks {
258 fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
259 Self(
260 iter.into_iter()
261 .filter_map(|entry| entry.as_ref().parse().ok())
262 .collect(),
263 )
264 }
265}
266
267impl<'de> Deserialize<'de> for IpNetworks {
268 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
269 Ok(Vec::<String>::deserialize(deserializer)?
270 .into_iter()
271 .collect())
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
281pub struct CidrCondition {
282 pub name: String,
284 pub value: IpNetworks,
288}
289
290impl CidrCondition {
291 pub fn new(field: impl Into<String>, value: impl IntoStrings) -> Self {
295 Self {
296 name: field.into(),
297 value: value.into_strings().into_iter().collect(),
298 }
299 }
300
301 fn matches<T>(&self, instance: &T) -> bool
302 where
303 T: Getter + ?Sized,
304 {
305 let ip = match instance.get_value(self.name.as_str()) {
306 Some(Val::IpAddr(ip)) => ip,
307 Some(Val::String(s)) => match s.parse() {
308 Ok(ip) => ip,
309 Err(_) => return false,
310 },
311 _ => return false,
312 };
313
314 self.value.contains(ip)
315 }
316}
317
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
325pub struct OrCondition {
326 pub inner: Vec<RuleCondition>,
328}
329
330impl OrCondition {
331 fn supported(&self) -> bool {
332 self.inner.iter().all(RuleCondition::supported)
333 }
334
335 fn matches<T>(&self, value: &T) -> bool
336 where
337 T: Getter + ?Sized,
338 {
339 self.inner.iter().any(|cond| cond.matches(value))
340 }
341}
342
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350pub struct AndCondition {
351 pub inner: Vec<RuleCondition>,
353}
354
355impl AndCondition {
356 fn supported(&self) -> bool {
357 self.inner.iter().all(RuleCondition::supported)
358 }
359 fn matches<T>(&self, value: &T) -> bool
360 where
361 T: Getter + ?Sized,
362 {
363 self.inner.iter().all(|cond| cond.matches(value))
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
373pub struct NotCondition {
374 pub inner: Box<RuleCondition>,
376}
377
378impl NotCondition {
379 fn supported(&self) -> bool {
380 self.inner.supported()
381 }
382
383 fn matches<T>(&self, value: &T) -> bool
384 where
385 T: Getter + ?Sized,
386 {
387 !self.inner.matches(value)
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
396pub struct AnyCondition {
397 pub name: String,
399 pub inner: Box<RuleCondition>,
401}
402
403impl AnyCondition {
404 pub fn new(field: impl Into<String>, inner: RuleCondition) -> Self {
406 Self {
407 name: field.into(),
408 inner: Box::new(inner),
409 }
410 }
411
412 fn supported(&self) -> bool {
413 self.inner.supported()
414 }
415 fn matches<T>(&self, instance: &T) -> bool
416 where
417 T: Getter + ?Sized,
418 {
419 let Some(mut getter_iter) = instance.get_iter(self.name.as_str()) else {
420 return false;
421 };
422
423 getter_iter.any(|g| self.inner.matches(g))
424 }
425}
426
427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
431pub struct AllCondition {
432 pub name: String,
434 pub inner: Box<RuleCondition>,
436}
437
438impl AllCondition {
439 pub fn new(field: impl Into<String>, inner: RuleCondition) -> Self {
442 Self {
443 name: field.into(),
444 inner: Box::new(inner),
445 }
446 }
447
448 fn supported(&self) -> bool {
449 self.inner.supported()
450 }
451 fn matches<T>(&self, instance: &T) -> bool
452 where
453 T: Getter + ?Sized,
454 {
455 let Some(mut getter_iter) = instance.get_iter(self.name.as_str()) else {
456 return false;
457 };
458
459 getter_iter.all(|g| self.inner.matches(g))
460 }
461}
462
463#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493#[serde(rename_all = "camelCase", tag = "op")]
494pub enum RuleCondition {
495 Eq(EqCondition),
510
511 Gte(GteCondition),
521
522 Lte(LteCondition),
532
533 Gt(GtCondition),
543
544 Lt(LtCondition),
554
555 Glob(GlobCondition),
565
566 Cidr(CidrCondition),
576
577 Or(OrCondition),
588
589 And(AndCondition),
600
601 Not(NotCondition),
611
612 Any(AnyCondition),
625
626 All(AllCondition),
638
639 #[serde(other)]
641 Unsupported,
642}
643
644impl RuleCondition {
645 pub fn all() -> Self {
647 Self::And(AndCondition { inner: Vec::new() })
648 }
649
650 pub fn never() -> Self {
652 Self::Or(OrCondition { inner: Vec::new() })
653 }
654
655 pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Self {
677 Self::Eq(EqCondition::new(field, value))
678 }
679
680 pub fn eq_ignore_case(field: impl Into<String>, value: impl Into<Value>) -> Self {
694 Self::Eq(EqCondition::new(field, value).ignore_case())
695 }
696
697 pub fn glob(field: impl Into<String>, value: impl IntoStrings) -> Self {
711 Self::Glob(GlobCondition::new(field, value))
712 }
713
714 pub fn cidr(field: impl Into<String>, value: impl IntoStrings) -> Self {
726 Self::Cidr(CidrCondition::new(field, value))
727 }
728
729 pub fn gt(field: impl Into<String>, value: impl Into<Value>) -> Self {
739 Self::Gt(GtCondition::new(field, value))
740 }
741
742 pub fn gte(field: impl Into<String>, value: impl Into<Value>) -> Self {
752 Self::Gte(GteCondition::new(field, value))
753 }
754
755 pub fn lt(field: impl Into<String>, value: impl Into<Value>) -> Self {
765 Self::Lt(LtCondition::new(field, value))
766 }
767
768 pub fn lte(field: impl Into<String>, value: impl Into<Value>) -> Self {
778 Self::Lte(LteCondition::new(field, value))
779 }
780
781 pub fn and(mut self, other: RuleCondition) -> Self {
794 if let Self::And(ref mut condition) = self {
795 condition.inner.push(other);
796 self
797 } else {
798 Self::And(AndCondition {
799 inner: vec![self, other],
800 })
801 }
802 }
803
804 pub fn or(mut self, other: RuleCondition) -> Self {
817 if let Self::Or(ref mut condition) = self {
818 condition.inner.push(other);
819 self
820 } else {
821 Self::Or(OrCondition {
822 inner: vec![self, other],
823 })
824 }
825 }
826
827 pub fn negate(self) -> Self {
839 match self {
840 Self::Not(condition) => *condition.inner,
841 other => Self::Not(NotCondition {
842 inner: Box::new(other),
843 }),
844 }
845 }
846
847 pub fn for_any(field: impl Into<String>, inner: RuleCondition) -> Self {
859 Self::Any(AnyCondition::new(field, inner))
860 }
861
862 pub fn for_all(field: impl Into<String>, inner: RuleCondition) -> Self {
874 Self::All(AllCondition::new(field, inner))
875 }
876
877 pub fn supported(&self) -> bool {
881 match self {
882 RuleCondition::Unsupported => false,
883 RuleCondition::Gte(_)
885 | RuleCondition::Lte(_)
886 | RuleCondition::Gt(_)
887 | RuleCondition::Lt(_)
888 | RuleCondition::Eq(_)
889 | RuleCondition::Glob(_)
890 | RuleCondition::Cidr(_) => true,
891 RuleCondition::And(rules) => rules.supported(),
893 RuleCondition::Or(rules) => rules.supported(),
894 RuleCondition::Not(rule) => rule.supported(),
895 RuleCondition::Any(rule) => rule.supported(),
896 RuleCondition::All(rule) => rule.supported(),
897 }
898 }
899
900 pub fn matches<T>(&self, value: &T) -> bool
902 where
903 T: Getter + ?Sized,
904 {
905 match self {
906 RuleCondition::Eq(condition) => condition.matches(value),
907 RuleCondition::Lte(condition) => condition.matches(value),
908 RuleCondition::Gte(condition) => condition.matches(value),
909 RuleCondition::Gt(condition) => condition.matches(value),
910 RuleCondition::Lt(condition) => condition.matches(value),
911 RuleCondition::Glob(condition) => condition.matches(value),
912 RuleCondition::Cidr(condition) => condition.matches(value),
913 RuleCondition::And(conditions) => conditions.matches(value),
914 RuleCondition::Or(conditions) => conditions.matches(value),
915 RuleCondition::Not(condition) => condition.matches(value),
916 RuleCondition::Any(condition) => condition.matches(value),
917 RuleCondition::All(condition) => condition.matches(value),
918 RuleCondition::Unsupported => false,
919 }
920 }
921}
922
923impl std::ops::BitAnd for RuleCondition {
924 type Output = Self;
925
926 fn bitand(self, rhs: Self) -> Self::Output {
927 self.and(rhs)
928 }
929}
930
931impl std::ops::BitOr for RuleCondition {
932 type Output = Self;
933
934 fn bitor(self, rhs: Self) -> Self::Output {
935 self.or(rhs)
936 }
937}
938
939impl std::ops::Not for RuleCondition {
940 type Output = Self;
941
942 fn not(self) -> Self::Output {
943 self.negate()
944 }
945}
946
947#[cfg(test)]
948mod tests {
949 use uuid::Uuid;
950
951 use super::*;
952 use crate::{GetterIter, HexId};
953
954 #[derive(Debug)]
955 struct Exception {
956 name: String,
957 }
958
959 impl Getter for Exception {
960 fn get_value(&self, path: &str) -> Option<Val<'_>> {
961 Some(match path {
962 "name" => self.name.as_str().into(),
963 _ => return None,
964 })
965 }
966 }
967
968 struct Trace {
969 trace_id: Uuid,
970 span_id: [u8; 4],
971 transaction: String,
972 release: String,
973 environment: String,
974 user_segment: String,
975 user_ip: String,
976 client_ip: IpAddr,
977 exceptions: Vec<Exception>,
978 }
979
980 impl Getter for Trace {
981 fn get_value(&self, path: &str) -> Option<Val<'_>> {
982 Some(match path.strip_prefix("trace.")? {
983 "trace_id" => (&self.trace_id).into(),
984 "span_id" => Val::HexId(HexId(&self.span_id[..])),
985 "transaction" => self.transaction.as_str().into(),
986 "release" => self.release.as_str().into(),
987 "environment" => self.environment.as_str().into(),
988 "user.segment" => self.user_segment.as_str().into(),
989 "user.ip" => self.user_ip.as_str().into(),
990 "client_ip" => self.client_ip.into(),
991 _ => {
992 return None;
993 }
994 })
995 }
996
997 fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
998 Some(match path.strip_prefix("trace.")? {
999 "exceptions" => GetterIter::new(self.exceptions.iter()),
1000 _ => return None,
1001 })
1002 }
1003 }
1004
1005 fn mock_trace() -> Trace {
1006 Trace {
1007 trace_id: "6b7d15b8-cee2-4354-9fee-dae7ef43e434".parse().unwrap(),
1008 span_id: [0xde, 0xad, 0xbe, 0xef],
1009 transaction: "transaction1".to_owned(),
1010 release: "1.1.1".to_owned(),
1011 environment: "debug".to_owned(),
1012 user_segment: "vip".to_owned(),
1013 user_ip: "10.1.2.3".to_owned(),
1014 client_ip: "2001:db8::1".parse().unwrap(),
1015 exceptions: vec![
1016 Exception {
1017 name: "NullPointerException".to_owned(),
1018 },
1019 Exception {
1020 name: "NullUser".to_owned(),
1021 },
1022 ],
1023 }
1024 }
1025
1026 #[test]
1027 fn deserialize() {
1028 let serialized_rules = r#"[
1029 {
1030 "op":"eq",
1031 "name": "field_1",
1032 "value": ["UPPER","lower"],
1033 "options":{
1034 "ignoreCase": true
1035 }
1036 },
1037 {
1038 "op":"eq",
1039 "name": "field_2",
1040 "value": ["UPPER","lower"]
1041 },
1042 {
1043 "op":"glob",
1044 "name": "field_3",
1045 "value": ["1.2.*","2.*"]
1046 },
1047 {
1048 "op":"cidr",
1049 "name": "field_ip",
1050 "value": ["192.168.1.1","10.0.0.0/8","192.168.1.1/32","not-an-ip"]
1051 },
1052 {
1053 "op":"not",
1054 "inner": {
1055 "op":"glob",
1056 "name": "field_4",
1057 "value": ["1.*"]
1058 }
1059 },
1060 {
1061 "op":"and",
1062 "inner": [{
1063 "op":"glob",
1064 "name": "field_5",
1065 "value": ["2.*"]
1066 }]
1067 },
1068 {
1069 "op":"or",
1070 "inner": [{
1071 "op":"glob",
1072 "name": "field_6",
1073 "value": ["3.*"]
1074 }]
1075 },
1076 {
1077 "op": "any",
1078 "name": "obj.exceptions",
1079 "inner": {
1080 "op": "glob",
1081 "name": "value",
1082 "value": ["*Exception"]
1083 }
1084 },
1085 {
1086 "op": "all",
1087 "name": "obj.exceptions",
1088 "inner": {
1089 "op": "glob",
1090 "name": "value",
1091 "value": ["*Exception"]
1092 }
1093 }
1094 ]"#;
1095
1096 let rules: Result<Vec<RuleCondition>, _> = serde_json::from_str(serialized_rules);
1097 assert!(rules.is_ok());
1098 let rules = rules.unwrap();
1099 insta::assert_ron_snapshot!(rules, @r###"
1100 [
1101 EqCondition(
1102 op: "eq",
1103 name: "field_1",
1104 value: [
1105 "UPPER",
1106 "lower",
1107 ],
1108 options: EqCondOptions(
1109 ignoreCase: true,
1110 ),
1111 ),
1112 EqCondition(
1113 op: "eq",
1114 name: "field_2",
1115 value: [
1116 "UPPER",
1117 "lower",
1118 ],
1119 ),
1120 GlobCondition(
1121 op: "glob",
1122 name: "field_3",
1123 value: [
1124 "1.2.*",
1125 "2.*",
1126 ],
1127 ),
1128 CidrCondition(
1129 op: "cidr",
1130 name: "field_ip",
1131 value: [
1132 "10.0.0.0/8",
1133 "192.168.1.1/32",
1134 ],
1135 ),
1136 NotCondition(
1137 op: "not",
1138 inner: GlobCondition(
1139 op: "glob",
1140 name: "field_4",
1141 value: [
1142 "1.*",
1143 ],
1144 ),
1145 ),
1146 AndCondition(
1147 op: "and",
1148 inner: [
1149 GlobCondition(
1150 op: "glob",
1151 name: "field_5",
1152 value: [
1153 "2.*",
1154 ],
1155 ),
1156 ],
1157 ),
1158 OrCondition(
1159 op: "or",
1160 inner: [
1161 GlobCondition(
1162 op: "glob",
1163 name: "field_6",
1164 value: [
1165 "3.*",
1166 ],
1167 ),
1168 ],
1169 ),
1170 AnyCondition(
1171 op: "any",
1172 name: "obj.exceptions",
1173 inner: GlobCondition(
1174 op: "glob",
1175 name: "value",
1176 value: [
1177 "*Exception",
1178 ],
1179 ),
1180 ),
1181 AllCondition(
1182 op: "all",
1183 name: "obj.exceptions",
1184 inner: GlobCondition(
1185 op: "glob",
1186 name: "value",
1187 value: [
1188 "*Exception",
1189 ],
1190 ),
1191 ),
1192 ]
1193 "###);
1194 }
1195
1196 #[test]
1197 fn unsupported_rule_deserialize() {
1198 let bad_json = r#"{
1199 "op": "BadOperator",
1200 "name": "foo",
1201 "value": "bar"
1202 }"#;
1203
1204 let rule: RuleCondition = serde_json::from_str(bad_json).unwrap();
1205 assert!(matches!(rule, RuleCondition::Unsupported));
1206 }
1207
1208 #[test]
1209 fn test_matches() {
1211 let conditions = [
1212 (
1213 "simple",
1214 RuleCondition::glob("trace.release", "1.1.1")
1215 & RuleCondition::eq_ignore_case("trace.environment", "debug")
1216 & RuleCondition::eq_ignore_case("trace.user.segment", "vip")
1217 & RuleCondition::eq_ignore_case("trace.transaction", "transaction1"),
1218 ),
1219 (
1220 "glob releases",
1221 RuleCondition::glob("trace.release", "1.*")
1222 & RuleCondition::eq_ignore_case("trace.environment", "debug")
1223 & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1224 ),
1225 (
1226 "glob transaction",
1227 RuleCondition::glob("trace.transaction", "trans*"),
1228 ),
1229 (
1230 "multiple releases",
1231 RuleCondition::glob("trace.release", vec!["2.1.1", "1.1.*"])
1232 & RuleCondition::eq_ignore_case("trace.environment", "debug")
1233 & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1234 ),
1235 (
1236 "multiple user segments",
1237 RuleCondition::glob("trace.release", "1.1.1")
1238 & RuleCondition::eq_ignore_case("trace.environment", "debug")
1239 & RuleCondition::eq_ignore_case(
1240 "trace.user.segment",
1241 vec!["paid", "vip", "free"],
1242 ),
1243 ),
1244 (
1245 "multiple transactions",
1246 RuleCondition::glob("trace.transaction", &["t22", "trans*", "t33"][..]),
1247 ),
1248 (
1249 "case insensitive user segments",
1250 RuleCondition::glob("trace.release", "1.1.1")
1251 & RuleCondition::eq_ignore_case("trace.environment", "debug")
1252 & RuleCondition::eq_ignore_case("trace.user.segment", &["ViP", "FrEe"][..]),
1253 ),
1254 (
1255 "multiple user environments",
1256 RuleCondition::glob("trace.release", "1.1.1")
1257 & RuleCondition::eq_ignore_case(
1258 "trace.environment",
1259 &["integration", "debug", "production"][..],
1260 )
1261 & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1262 ),
1263 (
1264 "case insensitive environments",
1265 RuleCondition::glob("trace.release", "1.1.1")
1266 & RuleCondition::eq_ignore_case("trace.environment", &["DeBuG", "PrOd"][..])
1267 & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1268 ),
1269 (
1270 "all environments",
1271 RuleCondition::glob("trace.release", "1.1.1")
1272 & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1273 ),
1274 (
1275 "undefined environments",
1276 RuleCondition::glob("trace.release", "1.1.1")
1277 & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1278 ),
1279 (
1280 "trace/span ID bytes",
1281 RuleCondition::eq("trace.trace_id", "6b7d15b8cee243549feedae7ef43e434")
1282 & RuleCondition::eq("trace.span_id", "DEADBEEF"),
1283 ),
1284 ("match no conditions", RuleCondition::all()),
1285 ("string cmp", RuleCondition::gt("trace.transaction", "t")),
1286 ];
1287
1288 let trace = mock_trace();
1289
1290 for (rule_test_name, condition) in conditions.iter() {
1291 let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1292 assert!(condition.matches(&trace), "{failure_name}");
1293 }
1294 }
1295
1296 #[test]
1297 fn test_cidr_condition() {
1298 let trace = mock_trace();
1299
1300 assert!(RuleCondition::cidr("trace.user.ip", "10.0.0.0/8").matches(&trace));
1301 assert!(RuleCondition::cidr("trace.user.ip", "10.1.2.3").matches(&trace));
1302 assert!(
1303 RuleCondition::cidr("trace.user.ip", &["192.168.0.0/16", "10.1.0.0/16"][..])
1304 .matches(&trace)
1305 );
1306 assert!(!RuleCondition::cidr("trace.user.ip", "192.168.0.0/16").matches(&trace));
1307 assert!(!RuleCondition::cidr("trace.user.ip", "2001:db8::/32").matches(&trace));
1308 assert!(!RuleCondition::cidr("trace.user.ip", Vec::<String>::new()).matches(&trace));
1309 assert!(!RuleCondition::cidr("trace.missing", "10.0.0.0/8").matches(&trace));
1310 assert!(!RuleCondition::cidr("trace.release", "10.0.0.0/8").matches(&trace));
1311
1312 assert!(RuleCondition::cidr("trace.client_ip", "2001:db8::/32").matches(&trace));
1313 assert!(RuleCondition::cidr("trace.client_ip", "2001:db8::1").matches(&trace));
1314 assert!(!RuleCondition::cidr("trace.client_ip", "10.0.0.0/8").matches(&trace));
1315 }
1316
1317 #[test]
1318 fn test_eq_condition_ip_addr() {
1319 let trace = mock_trace();
1320
1321 assert!(RuleCondition::eq("trace.client_ip", "2001:db8::1").matches(&trace));
1322 assert!(RuleCondition::eq("trace.client_ip", "2001:db8:0::1").matches(&trace));
1323 assert!(!RuleCondition::eq("trace.client_ip", "2001:db8::2").matches(&trace));
1324 assert!(!RuleCondition::eq("trace.client_ip", "not-an-ip").matches(&trace));
1325 }
1326
1327 #[test]
1328 fn test_cidr_condition_skips_invalid_entries() {
1329 let condition: RuleCondition = serde_json::from_str(
1330 r#"{"op": "cidr", "name": "trace.user.ip", "value": ["garbage", "10.0.0.0/8"]}"#,
1331 )
1332 .unwrap();
1333
1334 assert!(condition.matches(&mock_trace()));
1335 assert_eq!(
1336 serde_json::to_string(&condition).unwrap(),
1337 r#"{"op":"cidr","name":"trace.user.ip","value":["10.0.0.0/8"]}"#
1338 );
1339 }
1340
1341 #[test]
1342 fn test_or_combinator() {
1343 let conditions = [
1344 (
1345 "both",
1346 true,
1347 RuleCondition::eq_ignore_case("trace.environment", "debug")
1348 | RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1349 ),
1350 (
1351 "first",
1352 true,
1353 RuleCondition::eq_ignore_case("trace.environment", "debug")
1354 | RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1355 ),
1356 (
1357 "second",
1358 true,
1359 RuleCondition::eq_ignore_case("trace.environment", "prod")
1360 | RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1361 ),
1362 (
1363 "none",
1364 false,
1365 RuleCondition::eq_ignore_case("trace.environment", "prod")
1366 | RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1367 ),
1368 (
1369 "empty",
1370 false,
1371 RuleCondition::Or(OrCondition { inner: vec![] }),
1372 ),
1373 ("never", false, RuleCondition::never()),
1374 ];
1375
1376 let trace = mock_trace();
1377
1378 for (rule_test_name, expected, condition) in conditions.iter() {
1379 let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1380 assert!(condition.matches(&trace) == *expected, "{failure_name}");
1381 }
1382 }
1383
1384 #[test]
1385 fn test_and_combinator() {
1386 let conditions = [
1387 (
1388 "both",
1389 true,
1390 RuleCondition::eq_ignore_case("trace.environment", "debug")
1391 & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1392 ),
1393 (
1394 "first",
1395 false,
1396 RuleCondition::eq_ignore_case("trace.environment", "debug")
1397 & RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1398 ),
1399 (
1400 "second",
1401 false,
1402 RuleCondition::eq_ignore_case("trace.environment", "prod")
1403 & RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
1404 ),
1405 (
1406 "none",
1407 false,
1408 RuleCondition::eq_ignore_case("trace.environment", "prod")
1409 & RuleCondition::eq_ignore_case("trace.user.segment", "all"),
1410 ),
1411 (
1412 "empty",
1413 true,
1414 RuleCondition::And(AndCondition { inner: vec![] }),
1415 ),
1416 ("all", true, RuleCondition::all()),
1417 ];
1418
1419 let trace = mock_trace();
1420
1421 for (rule_test_name, expected, condition) in conditions.iter() {
1422 let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1423 assert!(condition.matches(&trace) == *expected, "{failure_name}");
1424 }
1425 }
1426
1427 #[test]
1428 fn test_not_combinator() {
1429 let conditions = [
1430 (
1431 "not true",
1432 false,
1433 !RuleCondition::eq_ignore_case("trace.environment", "debug"),
1434 ),
1435 (
1436 "not false",
1437 true,
1438 !RuleCondition::eq_ignore_case("trace.environment", "prod"),
1439 ),
1440 ];
1441
1442 let trace = mock_trace();
1443
1444 for (rule_test_name, expected, condition) in conditions.iter() {
1445 let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1446 assert!(condition.matches(&trace) == *expected, "{failure_name}");
1447 }
1448 }
1449
1450 #[test]
1451 fn test_does_not_match() {
1453 let conditions = [
1454 (
1455 "release",
1456 RuleCondition::glob("trace.release", "1.1.2")
1457 & RuleCondition::eq_ignore_case("trace.environment", "debug")
1458 & RuleCondition::eq_ignore_case("trace.user", "vip"),
1459 ),
1460 (
1461 "user segment",
1462 RuleCondition::glob("trace.release", "1.1.1")
1463 & RuleCondition::eq_ignore_case("trace.environment", "debug")
1464 & RuleCondition::eq_ignore_case("trace.user", "all"),
1465 ),
1466 (
1467 "environment",
1468 RuleCondition::glob("trace.release", "1.1.1")
1469 & RuleCondition::eq_ignore_case("trace.environment", "prod")
1470 & RuleCondition::eq_ignore_case("trace.user", "vip"),
1471 ),
1472 (
1473 "transaction",
1474 RuleCondition::glob("trace.release", "1.1.1")
1475 & RuleCondition::glob("trace.transaction", "t22")
1476 & RuleCondition::eq_ignore_case("trace.user", "vip"),
1477 ),
1478 ("span ID", RuleCondition::eq("trace.span_id", "deadbeer")),
1479 ];
1480
1481 let trace = mock_trace();
1482
1483 for (rule_test_name, condition) in conditions.iter() {
1484 let failure_name = format!("Failed on test: '{rule_test_name}'!!!");
1485 assert!(!condition.matches(&trace), "{failure_name}");
1486 }
1487 }
1488
1489 #[test]
1490 fn test_any_condition_with_match() {
1491 let condition = RuleCondition::for_any(
1492 "trace.exceptions",
1493 RuleCondition::glob("name", "*Exception"),
1494 );
1495
1496 let trace = mock_trace();
1497
1498 assert!(condition.matches(&trace));
1499 }
1500
1501 #[test]
1502 fn test_any_condition_with_no_match() {
1503 let condition =
1504 RuleCondition::for_any("trace.exceptions", RuleCondition::glob("name", "Error"));
1505
1506 let trace = mock_trace();
1507
1508 assert!(!condition.matches(&trace));
1509 }
1510
1511 #[test]
1512 fn test_all_condition() {
1513 let condition =
1514 RuleCondition::for_all("trace.exceptions", RuleCondition::glob("name", "Null*"));
1515
1516 let trace = mock_trace();
1517
1518 assert!(condition.matches(&trace));
1519 }
1520
1521 #[test]
1522 fn test_all_condition_with_no_match() {
1523 let condition = RuleCondition::for_all(
1524 "trace.exceptions",
1525 RuleCondition::glob("name", "*Exception"),
1526 );
1527
1528 let trace = mock_trace();
1529
1530 assert!(!condition.matches(&trace));
1531 }
1532}