relay_event_normalization/
logentry.rs

1#![cfg_attr(test, allow(unused_must_use))]
2
3use std::borrow::Cow;
4
5use dynfmt::{Argument, Format, FormatArgs, PythonFormat, SimpleCurlyFormat};
6use relay_event_schema::processor::{ProcessingAction, ProcessingResult};
7use relay_event_schema::protocol::LogEntry;
8use relay_protocol::{Annotated, Empty, Error, Meta, Value};
9
10struct ValueRef<'a>(&'a Value);
11
12impl FormatArgs for ValueRef<'_> {
13    fn get_index(&self, index: usize) -> Result<Option<Argument<'_>>, ()> {
14        match self.0 {
15            Value::Array(array) => Ok(array
16                .get(index)
17                .and_then(Annotated::value)
18                .map(|v| v as Argument<'_>)),
19            _ => Err(()),
20        }
21    }
22
23    fn get_key(&self, key: &str) -> Result<Option<Argument<'_>>, ()> {
24        match self.0 {
25            Value::Object(object) => Ok(object
26                .get(key)
27                .and_then(Annotated::value)
28                .map(|v| v as Argument<'_>)),
29            _ => Err(()),
30        }
31    }
32}
33
34fn format_message(format: &str, params: &Value) -> Option<String> {
35    // NB: This currently resembles the historic logic for formatting strings. It could be much more
36    // lenient however, and try multiple formats one after another without exiting early.
37    if format.contains('%') {
38        PythonFormat
39            .format(format, ValueRef(params))
40            .ok()
41            .map(Cow::into_owned)
42    } else if format.contains('{') {
43        SimpleCurlyFormat
44            .format(format, ValueRef(params))
45            .ok()
46            .map(Cow::into_owned)
47    } else {
48        None
49    }
50}
51
52pub fn normalize_logentry(logentry: &mut LogEntry, meta: &mut Meta) -> ProcessingResult {
53    // An empty logentry should just be skipped during serialization. No need for an error.
54    if logentry.is_empty() {
55        return Ok(());
56    }
57
58    if logentry.formatted.value().is_none() && logentry.message.value().is_none() {
59        meta.add_error(Error::invalid("no message present"));
60        return Err(ProcessingAction::DeleteValueSoft);
61    }
62
63    if let Some(params) = logentry.params.value() {
64        if logentry.formatted.value().is_none() {
65            if let Some(message) = logentry.message.value() {
66                if let Some(formatted) = format_message(message.as_ref(), params) {
67                    logentry.formatted = Annotated::new(formatted.into());
68                }
69            }
70        }
71    }
72
73    // Move `message` to `formatted` if they are equal or only message is given. This also
74    // overwrites the meta data on formatted. However, do not move if both of them are None to
75    // retain potential meta data on `formatted`.
76    if logentry.formatted.value().is_none()
77        || logentry.message.value() == logentry.formatted.value()
78    {
79        logentry.formatted = std::mem::take(&mut logentry.message);
80    }
81
82    Ok(())
83}
84
85#[cfg(test)]
86mod tests {
87    use relay_protocol::Object;
88    use similar_asserts::assert_eq;
89
90    use super::*;
91
92    #[test]
93    fn test_format_python() {
94        let mut logentry = LogEntry {
95            message: Annotated::new("hello, %s!".to_owned().into()),
96            params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
97                "world".to_owned(),
98            ))])),
99            ..LogEntry::default()
100        };
101
102        normalize_logentry(&mut logentry, &mut Meta::default());
103        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
104    }
105
106    #[test]
107    fn test_format_python_named() {
108        let mut logentry = LogEntry {
109            message: Annotated::new("hello, %(name)s!".to_owned().into()),
110            params: Annotated::new(Value::Object({
111                let mut object = Object::new();
112                object.insert(
113                    "name".to_owned(),
114                    Annotated::new(Value::String("world".to_owned())),
115                );
116                object
117            })),
118            ..LogEntry::default()
119        };
120
121        normalize_logentry(&mut logentry, &mut Meta::default());
122        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
123    }
124
125    #[test]
126    fn test_format_java() {
127        let mut logentry = LogEntry {
128            message: Annotated::new("hello, {}!".to_owned().into()),
129            params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
130                "world".to_owned(),
131            ))])),
132            ..LogEntry::default()
133        };
134
135        normalize_logentry(&mut logentry, &mut Meta::default());
136        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
137    }
138
139    #[test]
140    fn test_format_dotnet() {
141        let mut logentry = LogEntry {
142            message: Annotated::new("hello, {0}!".to_owned().into()),
143            params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
144                "world".to_owned(),
145            ))])),
146            ..LogEntry::default()
147        };
148
149        normalize_logentry(&mut logentry, &mut Meta::default());
150        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
151    }
152
153    #[test]
154    fn test_format_no_params() {
155        let mut logentry = LogEntry {
156            message: Annotated::new("hello, %s!".to_owned().into()),
157            ..LogEntry::default()
158        };
159
160        normalize_logentry(&mut logentry, &mut Meta::default());
161        assert_eq!(logentry.formatted.as_str(), Some("hello, %s!"));
162    }
163
164    #[test]
165    fn test_only_message() {
166        let mut logentry = LogEntry {
167            message: Annotated::new("hello, world!".to_owned().into()),
168            ..LogEntry::default()
169        };
170
171        normalize_logentry(&mut logentry, &mut Meta::default());
172        assert_eq!(logentry.message.value(), None);
173        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
174    }
175
176    #[test]
177    fn test_message_formatted_equal() {
178        let mut logentry = LogEntry {
179            message: Annotated::new("hello, world!".to_owned().into()),
180            formatted: Annotated::new("hello, world!".to_owned().into()),
181            ..LogEntry::default()
182        };
183
184        normalize_logentry(&mut logentry, &mut Meta::default());
185        assert_eq!(logentry.message.value(), None);
186        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
187    }
188
189    #[test]
190    fn test_empty_missing_message() {
191        let mut logentry = LogEntry {
192            params: Value::U64(0).into(), // Ensure the logentry is not empty
193            ..LogEntry::default()
194        };
195        let mut meta = Meta::default();
196
197        assert_eq!(
198            normalize_logentry(&mut logentry, &mut meta),
199            Err(ProcessingAction::DeleteValueSoft)
200        );
201        assert!(meta.has_errors());
202    }
203
204    #[test]
205    fn test_empty_logentry() {
206        let mut logentry = LogEntry::default();
207        let mut meta = Meta::default();
208
209        assert_eq!(normalize_logentry(&mut logentry, &mut meta), Ok(()));
210        assert!(!meta.has_errors());
211    }
212}