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        && logentry.formatted.value().is_none()
65        && let Some(message) = logentry.message.value()
66        && let Some(formatted) = format_message(message.as_ref(), params)
67    {
68        logentry.formatted = Annotated::new(formatted.into());
69    }
70
71    // Move `message` to `formatted` if they are equal or only message is given. This also
72    // overwrites the meta data on formatted. However, do not move if both of them are None to
73    // retain potential meta data on `formatted`.
74    if logentry.formatted.value().is_none()
75        || logentry.message.value() == logentry.formatted.value()
76    {
77        logentry.formatted = std::mem::take(&mut logentry.message);
78    }
79
80    Ok(())
81}
82
83#[cfg(test)]
84mod tests {
85    use relay_protocol::Object;
86    use similar_asserts::assert_eq;
87
88    use super::*;
89
90    #[test]
91    fn test_format_python() {
92        let mut logentry = LogEntry {
93            message: Annotated::new("hello, %s!".to_owned().into()),
94            params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
95                "world".to_owned(),
96            ))])),
97            ..LogEntry::default()
98        };
99
100        normalize_logentry(&mut logentry, &mut Meta::default());
101        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
102    }
103
104    #[test]
105    fn test_format_python_named() {
106        let mut logentry = LogEntry {
107            message: Annotated::new("hello, %(name)s!".to_owned().into()),
108            params: Annotated::new(Value::Object({
109                let mut object = Object::new();
110                object.insert(
111                    "name".to_owned(),
112                    Annotated::new(Value::String("world".to_owned())),
113                );
114                object
115            })),
116            ..LogEntry::default()
117        };
118
119        normalize_logentry(&mut logentry, &mut Meta::default());
120        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
121    }
122
123    #[test]
124    fn test_format_java() {
125        let mut logentry = LogEntry {
126            message: Annotated::new("hello, {}!".to_owned().into()),
127            params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
128                "world".to_owned(),
129            ))])),
130            ..LogEntry::default()
131        };
132
133        normalize_logentry(&mut logentry, &mut Meta::default());
134        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
135    }
136
137    #[test]
138    fn test_format_dotnet() {
139        let mut logentry = LogEntry {
140            message: Annotated::new("hello, {0}!".to_owned().into()),
141            params: Annotated::new(Value::Array(vec![Annotated::new(Value::String(
142                "world".to_owned(),
143            ))])),
144            ..LogEntry::default()
145        };
146
147        normalize_logentry(&mut logentry, &mut Meta::default());
148        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
149    }
150
151    #[test]
152    fn test_format_no_params() {
153        let mut logentry = LogEntry {
154            message: Annotated::new("hello, %s!".to_owned().into()),
155            ..LogEntry::default()
156        };
157
158        normalize_logentry(&mut logentry, &mut Meta::default());
159        assert_eq!(logentry.formatted.as_str(), Some("hello, %s!"));
160    }
161
162    #[test]
163    fn test_only_message() {
164        let mut logentry = LogEntry {
165            message: Annotated::new("hello, world!".to_owned().into()),
166            ..LogEntry::default()
167        };
168
169        normalize_logentry(&mut logentry, &mut Meta::default());
170        assert_eq!(logentry.message.value(), None);
171        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
172    }
173
174    #[test]
175    fn test_message_formatted_equal() {
176        let mut logentry = LogEntry {
177            message: Annotated::new("hello, world!".to_owned().into()),
178            formatted: Annotated::new("hello, world!".to_owned().into()),
179            ..LogEntry::default()
180        };
181
182        normalize_logentry(&mut logentry, &mut Meta::default());
183        assert_eq!(logentry.message.value(), None);
184        assert_eq!(logentry.formatted.as_str(), Some("hello, world!"));
185    }
186
187    #[test]
188    fn test_empty_missing_message() {
189        let mut logentry = LogEntry {
190            params: Value::U64(0).into(), // Ensure the logentry is not empty
191            ..LogEntry::default()
192        };
193        let mut meta = Meta::default();
194
195        assert_eq!(
196            normalize_logentry(&mut logentry, &mut meta),
197            Err(ProcessingAction::DeleteValueSoft)
198        );
199        assert!(meta.has_errors());
200    }
201
202    #[test]
203    fn test_empty_logentry() {
204        let mut logentry = LogEntry::default();
205        let mut meta = Meta::default();
206
207        assert_eq!(normalize_logentry(&mut logentry, &mut meta), Ok(()));
208        assert!(!meta.has_errors());
209    }
210}