relay_event_normalization/transactions/
rules.rs1use std::borrow::Cow;
2
3use chrono::{DateTime, Utc};
4use relay_common::glob2::LazyGlob;
5use relay_event_schema::protocol::OperationType;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)]
12pub struct SpanDescriptionRuleScope {
13 #[serde(skip_serializing_if = "String::is_empty")]
15 pub op: OperationType,
16}
17
18fn default_substitution() -> String {
20 "*".to_string()
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
25#[serde(tag = "method", rename_all = "snake_case")]
26pub enum RedactionRule {
27 Replace {
29 #[serde(default = "default_substitution")]
33 substitution: String,
34 },
35
36 #[serde(other)]
38 Unknown,
39}
40
41impl Default for RedactionRule {
42 fn default() -> Self {
43 Self::Replace {
44 substitution: default_substitution(),
45 }
46 }
47}
48
49#[derive(Debug, Serialize, Deserialize, Clone)]
51pub struct SpanDescriptionRule {
52 pub pattern: LazyGlob,
54 pub expiry: DateTime<Utc>,
56 pub scope: SpanDescriptionRuleScope,
58 pub redaction: RedactionRule,
60}
61
62impl From<&TransactionNameRule> for SpanDescriptionRule {
63 fn from(value: &TransactionNameRule) -> Self {
64 SpanDescriptionRule {
65 pattern: LazyGlob::new(format!("**{}", value.pattern.as_str())),
66 expiry: value.expiry,
67 scope: SpanDescriptionRuleScope::default(),
68 redaction: value.redaction.clone(),
69 }
70 }
71}
72
73impl SpanDescriptionRule {
74 pub fn match_and_apply(&self, mut string: Cow<String>) -> Option<String> {
78 let slash_is_present = string.ends_with('/');
79 if !slash_is_present {
80 string.to_mut().push('/');
81 }
82 let is_matched = self.matches(&string);
83
84 if is_matched {
85 let mut result = self.apply(&string);
86 if !slash_is_present {
87 result.pop();
88 }
89 Some(result)
90 } else {
91 None
92 }
93 }
94
95 fn matches(&self, string: &str) -> bool {
97 let now = Utc::now();
98 self.expiry > now && self.pattern.compiled().is_match(string)
99 }
100
101 fn apply(&self, value: &str) -> String {
103 match &self.redaction {
104 RedactionRule::Replace { substitution } => self
105 .pattern
106 .compiled()
107 .replace_captures(value, substitution),
108 _ => {
109 relay_log::trace!("Replacement rule type is unsupported!");
110 value.to_owned()
111 }
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
118pub struct TransactionNameRule {
119 pub pattern: LazyGlob,
121 pub expiry: DateTime<Utc>,
123 pub redaction: RedactionRule,
125}
126
127impl TransactionNameRule {
128 pub fn match_and_apply(&self, mut transaction: Cow<String>) -> Option<String> {
130 let slash_is_present = transaction.ends_with('/');
131 if !slash_is_present {
132 transaction.to_mut().push('/');
133 }
134 let is_matched = self.matches(&transaction);
135
136 if is_matched {
137 let mut result = self.apply(&transaction);
138 if !slash_is_present {
139 result.pop();
140 }
141 Some(result)
142 } else {
143 None
144 }
145 }
146
147 fn apply(&self, value: &str) -> String {
151 match &self.redaction {
152 RedactionRule::Replace { substitution } => self
153 .pattern
154 .compiled()
155 .replace_captures(value, substitution),
156 _ => {
157 relay_log::trace!("Replacement rule type is unsupported!");
158 value.to_owned()
159 }
160 }
161 }
162
163 fn matches(&self, transaction: &str) -> bool {
166 self.expiry > Utc::now() && self.pattern.compiled().is_match(transaction)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_rule_format() {
176 let json = r#"
177 {
178 "pattern": "/auth/login/*/**",
179 "expiry": "2022-11-30T00:00:00.000000Z",
180 "scope": {
181 "source": "url"
182 },
183 "redaction": {
184 "method": "replace",
185 "substitution": ":id"
186 }
187 }
188 "#;
189
190 let rule: TransactionNameRule = serde_json::from_str(json).unwrap();
191
192 let parsed_time = DateTime::parse_from_rfc3339("2022-11-30T00:00:00Z").unwrap();
193 let result = TransactionNameRule {
194 pattern: LazyGlob::new("/auth/login/*/**".to_string()),
195 expiry: DateTime::from_naive_utc_and_offset(parsed_time.naive_utc(), Utc),
196 redaction: RedactionRule::Replace {
197 substitution: String::from(":id"),
198 },
199 };
200
201 assert_eq!(rule, result);
202 }
203
204 #[test]
205 fn test_rule_format_defaults() {
206 let json = r#"
207 {
208 "pattern": "/auth/login/*/**",
209 "expiry": "2022-11-30T00:00:00.000000Z",
210 "redaction": {
211 "method": "replace"
212 }
213 }
214 "#;
215
216 let rule: TransactionNameRule = serde_json::from_str(json).unwrap();
217
218 let parsed_time = DateTime::parse_from_rfc3339("2022-11-30T00:00:00Z").unwrap();
219 let result = TransactionNameRule {
220 pattern: LazyGlob::new("/auth/login/*/**".to_string()),
221 expiry: DateTime::from_naive_utc_and_offset(parsed_time.naive_utc(), Utc),
222 redaction: RedactionRule::Replace {
223 substitution: default_substitution(),
224 },
225 };
226
227 assert_eq!(rule, result);
228 }
229
230 #[test]
231 fn test_rule_format_unsupported_reduction() {
232 let json = r#"
233 {
234 "pattern": "/auth/login/*/**",
235 "expiry": "2022-11-30T00:00:00.000000Z",
236 "redaction": {
237 "method": "update"
238 }
239 }
240 "#;
241
242 let rule: TransactionNameRule = serde_json::from_str(json).unwrap();
243 let result = rule.apply("/auth/login/test/");
244
245 assert_eq!(result, "/auth/login/test/".to_string());
246 }
247
248 #[test]
249 fn test_rule_format_roundtrip() {
250 let json = r#"{
251 "pattern": "/auth/login/*/**",
252 "expiry": "2022-11-30T00:00:00Z",
253 "redaction": {
254 "method": "replace",
255 "substitution": ":id"
256 }
257}"#;
258
259 let rule: TransactionNameRule = serde_json::from_str(json).unwrap();
260 let rule_json = serde_json::to_string_pretty(&rule).unwrap();
261 assert_eq!(json, rule_json);
263 }
264}