Skip to main content

relay_event_normalization/
logentry.rs

1#![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    // NB: This currently resembles the historic logic for formatting strings. It could be much more
35    // lenient however, and try multiple formats one after another without exiting early.
36    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    // An empty logentry should just be skipped during serialization. No need for an error.
56    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    // Conservative limit assuming all chars are 4 bytes.
66    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    // Move `message` to `formatted` if they are equal or only message is given. This also
77    // overwrites the meta data on formatted. However, do not move if both of them are None to
78    // retain potential meta data on `formatted`.
79    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(), // Ensure the logentry is not empty
196            ..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, {}!", &params, 13).as_deref(),
221            Some("hello, world!")
222        );
223        assert_eq!(format_message("hello, {}!", &params, 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}