relay_event_schema/protocol/
logentry.rs

1use relay_protocol::{Annotated, Empty, Error, FromValue, IntoValue, Meta, Object, Value};
2
3use crate::processor::ProcessValue;
4use crate::protocol::JsonLenientString;
5
6/// A log entry message.
7///
8/// A log message is similar to the `message` attribute on the event itself but
9/// can additionally hold optional parameters.
10///
11/// ```json
12/// {
13///   "logentry": {
14///     "message": "My raw message with interpreted strings like %s",
15///     "params": ["this"]
16///   }
17/// }
18/// ```
19///
20/// ```json
21/// {
22///   "logentry": {
23///     "message": "My raw message with interpreted strings like {foo}",
24///     "params": {"foo": "this"}
25///   }
26/// }
27/// ```
28#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
29#[metastructure(process_func = "process_logentry", value_type = "LogEntry")]
30pub struct LogEntry {
31    /// The log message with parameter placeholders.
32    ///
33    /// This attribute is primarily used for grouping related events together into issues.
34    /// Therefore this really should just be a string template, i.e. `Sending %d requests` instead
35    /// of `Sending 9999 requests`. The latter is much better at home in `formatted`.
36    ///
37    /// It must not exceed 8192 characters. Longer messages will be truncated.
38    #[metastructure(max_chars = 8192, max_chars_allowance = 200)]
39    pub message: Annotated<Message>,
40
41    /// The formatted message. If `message` and `params` are given, Sentry
42    /// will attempt to backfill `formatted` if empty.
43    ///
44    /// It must not exceed 8192 characters. Longer messages will be truncated.
45    #[metastructure(max_chars = 8192, max_chars_allowance = 200, pii = "true")]
46    pub formatted: Annotated<Message>,
47
48    /// Parameters to be interpolated into the log message. This can be an array of positional
49    /// parameters as well as a mapping of named arguments to their values.
50    #[metastructure(max_depth = 5, max_bytes = 2048, pii = "true")]
51    pub params: Annotated<Value>,
52
53    /// Additional arbitrary fields for forwards compatibility.
54    #[metastructure(additional_properties, pii = "true")]
55    pub other: Object<Value>,
56}
57
58impl From<String> for LogEntry {
59    fn from(formatted_msg: String) -> Self {
60        LogEntry {
61            formatted: Annotated::new(formatted_msg.into()),
62            ..Self::default()
63        }
64    }
65}
66
67#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
68#[metastructure(value_type = "Message", value_type = "String")]
69pub struct Message(String);
70
71impl From<String> for Message {
72    fn from(msg: String) -> Message {
73        Message(msg)
74    }
75}
76
77impl AsRef<str> for Message {
78    fn as_ref(&self) -> &str {
79        self.0.as_ref()
80    }
81}
82
83impl FromValue for LogEntry {
84    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
85        // raw 'message' is coerced to the Message interface, as its used for pure index of
86        // searchable strings. If both a raw 'message' and a Message interface exist, try and
87        // add the former as the 'formatted' attribute of the latter.
88        // See GH-3248
89        match value {
90            x @ Annotated(Some(Value::Object(_)), _) => {
91                #[derive(Debug, FromValue)]
92                struct Helper {
93                    message: Annotated<String>,
94                    formatted: Annotated<String>,
95                    params: Annotated<Value>,
96                    #[metastructure(additional_properties)]
97                    other: Object<Value>,
98                }
99
100                Helper::from_value(x).map_value(|helper| {
101                    let params = match helper.params {
102                        a @ Annotated(Some(Value::Object(_)), _) => a,
103                        a @ Annotated(Some(Value::Array(_)), _) => a,
104                        a @ Annotated(None, _) => a,
105                        Annotated(Some(value), _) => Annotated::from_error(
106                            Error::expected("message parameters"),
107                            Some(value),
108                        ),
109                    };
110
111                    LogEntry {
112                        message: helper.message.map_value(Message),
113                        formatted: helper.formatted.map_value(Message),
114                        params,
115                        other: helper.other,
116                    }
117                })
118            }
119            Annotated(None, meta) => Annotated(None, meta),
120            // The next two cases handle the legacy top-level `message` attribute, which was sent as
121            // literal string, false (which should be ignored) or even as deep JSON object. Sentry
122            // historically JSONified this field.
123            Annotated(Some(Value::Bool(false)), _) => Annotated(None, Meta::default()),
124            x => Annotated::new(LogEntry {
125                formatted: JsonLenientString::from_value(x)
126                    .map_value(JsonLenientString::into_inner)
127                    .map_value(Message),
128                ..Default::default()
129            }),
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use similar_asserts::assert_eq;
137
138    use super::*;
139
140    #[test]
141    fn test_logentry_roundtrip() {
142        let json = r#"{
143  "message": "Hello, %s %s!",
144  "params": [
145    "World",
146    1
147  ],
148  "other": "value"
149}"#;
150
151        let entry = Annotated::new(LogEntry {
152            message: Annotated::new("Hello, %s %s!".to_string().into()),
153            formatted: Annotated::empty(),
154            params: Annotated::new(Value::Array(vec![
155                Annotated::new(Value::String("World".to_string())),
156                Annotated::new(Value::I64(1)),
157            ])),
158            other: {
159                let mut map = Object::new();
160                map.insert(
161                    "other".to_string(),
162                    Annotated::new(Value::String("value".to_string())),
163                );
164                map
165            },
166        });
167
168        assert_eq!(entry, Annotated::from_json(json).unwrap());
169        assert_eq!(json, entry.to_json_pretty().unwrap());
170    }
171
172    #[test]
173    fn test_logentry_from_message() {
174        let input = r#""hi""#;
175        let output = r#"{
176  "formatted": "hi"
177}"#;
178
179        let entry = Annotated::new(LogEntry {
180            formatted: Annotated::new("hi".to_string().into()),
181            ..Default::default()
182        });
183
184        assert_eq!(entry, Annotated::from_json(input).unwrap());
185        assert_eq!(output, entry.to_json_pretty().unwrap());
186    }
187
188    #[test]
189    fn test_logentry_empty_params() {
190        let input = r#"{"params":[]}"#;
191        let entry = Annotated::new(LogEntry {
192            params: Annotated::new(Value::Array(vec![])),
193            ..Default::default()
194        });
195
196        assert_eq!(entry, Annotated::from_json(input).unwrap());
197        assert_eq!(input, entry.to_json().unwrap());
198    }
199
200    #[test]
201    fn test_logentry_named_params() {
202        let json = r#"{
203  "message": "Hello, %s!",
204  "params": {
205    "name": "World"
206  }
207}"#;
208
209        let entry = Annotated::new(LogEntry {
210            message: Annotated::new("Hello, %s!".to_string().into()),
211            params: Annotated::new(Value::Object({
212                let mut object = Object::new();
213                object.insert(
214                    "name".to_string(),
215                    Annotated::new(Value::String("World".to_string())),
216                );
217                object
218            })),
219            ..LogEntry::default()
220        });
221
222        assert_eq!(entry, Annotated::from_json(json).unwrap());
223        assert_eq!(json, entry.to_json_pretty().unwrap());
224    }
225
226    #[test]
227    fn test_logentry_invalid_params() {
228        let json = r#"{
229  "message": "Hello, %s!",
230  "params": 42
231}"#;
232
233        let entry = Annotated::new(LogEntry {
234            message: Annotated::new("Hello, %s!".to_string().into()),
235            params: Annotated::from_error(
236                Error::expected("message parameters"),
237                Some(Value::I64(42)),
238            ),
239            ..LogEntry::default()
240        });
241
242        assert_eq!(entry, Annotated::from_json(json).unwrap());
243    }
244}