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 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 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(), ..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}