relay_event_normalization/
logentry.rs1#![cfg_attr(test, allow(unused_must_use))]
2
3use bytes::BufMut;
4use dynfmt::{Argument, Format, FormatArgs, PythonFormat, SimpleCurlyFormat};
5use relay_event_schema::processor::{ProcessingAction, ProcessingResult};
6use relay_event_schema::protocol::LogEntry;
7use relay_protocol::{Annotated, Empty, Error, Meta, Value};
8
9struct ValueRef<'a>(&'a Value);
10
11impl FormatArgs for ValueRef<'_> {
12 fn get_index(&self, index: usize) -> Result<Option<Argument<'_>>, ()> {
13 match self.0 {
14 Value::Array(array) => Ok(array
15 .get(index)
16 .and_then(Annotated::value)
17 .map(|v| v as Argument<'_>)),
18 _ => Err(()),
19 }
20 }
21
22 fn get_key(&self, key: &str) -> Result<Option<Argument<'_>>, ()> {
23 match self.0 {
24 Value::Object(object) => Ok(object
25 .get(key)
26 .and_then(Annotated::value)
27 .map(|v| v as Argument<'_>)),
28 _ => Err(()),
29 }
30 }
31}
32
33fn format_message(format: &str, params: &Value, limit: usize) -> Option<String> {
34 let mut buf = Vec::new();
37 let writer = (&mut buf).limit(limit).writer();
38
39 if format.contains('%') {
40 PythonFormat
41 .format_into(writer, format, ValueRef(params))
42 .ok()?
43 } else if format.contains('{') {
44 SimpleCurlyFormat
45 .format_into(writer, format, ValueRef(params))
46 .ok()?
47 } else {
48 return None;
49 };
50
51 String::from_utf8(buf).ok()
52}
53
54pub fn normalize_logentry(logentry: &mut LogEntry, meta: &mut Meta) -> ProcessingResult {
55 if logentry.is_empty() {
57 return Ok(());
58 }
59
60 if logentry.formatted.value().is_none() && logentry.message.value().is_none() {
61 meta.add_error(Error::invalid("no message present"));
62 return Err(ProcessingAction::DeleteValueSoft);
63 }
64
65 let limit = LogEntry::MAX_MESSAGE_CHARS * 4;
67
68 if let Some(params) = logentry.params.value()
69 && logentry.formatted.value().is_none()
70 && let Some(message) = logentry.message.value()
71 && let Some(formatted) = format_message(message.as_ref(), params, limit)
72 {
73 logentry.formatted = Annotated::new(formatted.into());
74 }
75
76 if logentry.formatted.value().is_none()
80 || logentry.message.value() == logentry.formatted.value()
81 {
82 logentry.formatted = std::mem::take(&mut logentry.message);
83 }
84
85 Ok(())
86}
87
88#[cfg(test)]
89mod tests {
90 use relay_protocol::Object;
91 use similar_asserts::assert_eq;
92
93 use super::*;
94
95 #[test]
96 fn test_format_python() {
97 let mut logentry = LogEntry {
98 message: Annotated::new("hello, %s!".to_owned().into()),
99 params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
100 "world".to_owned(),
101 ))])),
102 ..LogEntry::default()
103 };
104
105 normalize_logentry(&mut logentry, &mut Meta::default());
106 assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
107 }
108
109 #[test]
110 fn test_format_python_named() {
111 let mut logentry = LogEntry {
112 message: Annotated::new("hello, %(name)s!".to_owned().into()),
113 params: Annotated::new(Value::Object({
114 let mut object = Object::new();
115 object.insert(
116 "name".to_owned(),
117 Annotated::new(Value::String("world".to_owned())),
118 );
119 object
120 })),
121 ..LogEntry::default()
122 };
123
124 normalize_logentry(&mut logentry, &mut Meta::default());
125 assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
126 }
127
128 #[test]
129 fn test_format_java() {
130 let mut logentry = LogEntry {
131 message: Annotated::new("hello, {}!".to_owned().into()),
132 params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
133 "world".to_owned(),
134 ))])),
135 ..LogEntry::default()
136 };
137
138 normalize_logentry(&mut logentry, &mut Meta::default());
139 assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
140 }
141
142 #[test]
143 fn test_format_dotnet() {
144 let mut logentry = LogEntry {
145 message: Annotated::new("hello, {0}!".to_owned().into()),
146 params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
147 "world".to_owned(),
148 ))])),
149 ..LogEntry::default()
150 };
151
152 normalize_logentry(&mut logentry, &mut Meta::default());
153 assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
154 }
155
156 #[test]
157 fn test_format_no_params() {
158 let mut logentry = LogEntry {
159 message: Annotated::new("hello, %s!".to_owned().into()),
160 ..LogEntry::default()
161 };
162
163 normalize_logentry(&mut logentry, &mut Meta::default());
164 assert_eq!(logentry.formatted.as_str(), Some("hello, %s!"));
165 }
166
167 #[test]
168 fn test_only_message() {
169 let mut logentry = LogEntry {
170 message: Annotated::new("hello, world!".to_owned().into()),
171 ..LogEntry::default()
172 };
173
174 normalize_logentry(&mut logentry, &mut Meta::default());
175 assert_eq!(logentry.message.value(), None);
176 assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
177 }
178
179 #[test]
180 fn test_message_formatted_equal() {
181 let mut logentry = LogEntry {
182 message: Annotated::new("hello, world!".to_owned().into()),
183 formatted: Annotated::new("hello, world!".to_owned().into()),
184 ..LogEntry::default()
185 };
186
187 normalize_logentry(&mut logentry, &mut Meta::default());
188 assert_eq!(logentry.message.value(), None);
189 assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
190 }
191
192 #[test]
193 fn test_empty_missing_message() {
194 let mut logentry = LogEntry {
195 params: Value::U64(0).into(), ..LogEntry::default()
197 };
198 let mut meta = Meta::default();
199
200 assert_eq!(
201 normalize_logentry(&mut logentry, &mut meta),
202 Err(ProcessingAction::DeleteValueSoft)
203 );
204 assert!(meta.has_errors());
205 }
206
207 #[test]
208 fn test_empty_logentry() {
209 let mut logentry = LogEntry::default();
210 let mut meta = Meta::default();
211
212 assert_eq!(normalize_logentry(&mut logentry, &mut meta), Ok(()));
213 assert!(!meta.has_errors());
214 }
215
216 #[test]
217 fn test_format_message_limit() {
218 let params = Value::Array(vec![Annotated::new(Value::String("world".to_owned()))]);
219 assert_eq!(
220 format_message("hello, {}!", ¶ms, 13).as_deref(),
221 Some("hello, world!")
222 );
223 assert_eq!(format_message("hello, {}!", ¶ms, 12), None);
224 }
225
226 #[test]
227 fn test_normalize_logentry_limit() {
228 let mut logentry = LogEntry {
229 message: Annotated::new("{0}{0}{0}{0}".to_owned().into()),
230 params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
231 "#".repeat(9000),
232 ))])),
233 ..LogEntry::default()
234 };
235
236 normalize_logentry(&mut logentry, &mut Meta::default());
237 assert_eq!(logentry.formatted.as_str(), Some("{0}{0}{0}{0}"));
238 }
239}