relay_event_normalization/
logentry.rs1#![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 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 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 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(), ..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}