Skip to main content

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 [`LogEntry::MAX_MESSAGE_CHARS`] characters. Longer messages will be truncated.
38    #[metastructure(max_chars = LogEntry::MAX_MESSAGE_CHARS, 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 [`LogEntry::MAX_MESSAGE_CHARS`] characters. Longer messages will be truncated.
45    #[metastructure(max_chars = LogEntry::MAX_MESSAGE_CHARS, 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 LogEntry {
59    /// Maximum number of characters in a log entry message.
60    pub const MAX_MESSAGE_CHARS: usize = 8192;
61}
62
63impl From<String> for LogEntry {
64    fn from(formatted_msg: String) -> Self {
65        LogEntry {
66            formatted: Annotated::new(formatted_msg.into()),
67            ..Self::default()
68        }
69    }
70}
71
72#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
73#[metastructure(value_type = "Message", value_type = "String")]
74pub struct Message(String);
75
76impl From<String> for Message {
77    fn from(msg: String) -> Message {
78        Message(msg)
79    }
80}
81
82impl AsRef<str> for Message {
83    fn as_ref(&self) -> &str {
84        self.0.as_ref()
85    }
86}
87
88impl FromValue for LogEntry {
89    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
90        // raw 'message' is coerced to the Message interface, as its used for pure index of
91        // searchable strings. If both a raw 'message' and a Message interface exist, try and
92        // add the former as the 'formatted' attribute of the latter.
93        // See GH-3248
94        match value {
95            x @ Annotated(Some(Value::Object(_)), _) => {
96                #[derive(Debug, FromValue)]
97                struct Helper {
98                    message: Annotated<String>,
99                    formatted: Annotated<String>,
100                    params: Annotated<Value>,
101                    #[metastructure(additional_properties)]
102                    other: Object<Value>,
103                }
104
105                Helper::from_value(x).map_value(|helper| {
106                    let params = match helper.params {
107                        a @ Annotated(Some(Value::Object(_)), _) => a,
108                        a @ Annotated(Some(Value::Array(_)), _) => a,
109                        a @ Annotated(None, _) => a,
110                        Annotated(Some(value), _) => Annotated::from_error(
111                            Error::expected("message parameters"),
112                            Some(value),
113                        ),
114                    };
115
116                    LogEntry {
117                        message: helper.message.map_value(Message),
118                        formatted: helper.formatted.map_value(Message),
119                        params,
120                        other: helper.other,
121                    }
122                })
123            }
124            Annotated(None, meta) => Annotated(None, meta),
125            // The next two cases handle the legacy top-level `message` attribute, which was sent as
126            // literal string, false (which should be ignored) or even as deep JSON object. Sentry
127            // historically JSONified this field.
128            Annotated(Some(Value::Bool(false)), _) => Annotated(None, Meta::default()),
129            x => Annotated::new(LogEntry {
130                formatted: JsonLenientString::from_value(x)
131                    .map_value(JsonLenientString::into_inner)
132                    .map_value(Message),
133                ..Default::default()
134            }),
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use similar_asserts::assert_eq;
142
143    use super::*;
144
145    #[test]
146    fn test_logentry_roundtrip() {
147        let json = r#"{
148  "message": "Hello, %s %s!",
149  "params": [
150    "World",
151    1
152  ],
153  "other": "value"
154}"#;
155
156        let entry = Annotated::new(LogEntry {
157            message: Annotated::new("Hello, %s %s!".to_owned().into()),
158            formatted: Annotated::empty(),
159            params: Annotated::new(Value::Array(vec![
160                Annotated::new(Value::String("World".to_owned())),
161                Annotated::new(Value::I64(1)),
162            ])),
163            other: {
164                let mut map = Object::new();
165                map.insert(
166                    "other".to_owned(),
167                    Annotated::new(Value::String("value".to_owned())),
168                );
169                map
170            },
171        });
172
173        assert_eq!(entry, Annotated::from_json(json).unwrap());
174        assert_eq!(json, entry.to_json_pretty().unwrap());
175    }
176
177    #[test]
178    fn test_logentry_from_message() {
179        let input = r#""hi""#;
180        let output = r#"{
181  "formatted": "hi"
182}"#;
183
184        let entry = Annotated::new(LogEntry {
185            formatted: Annotated::new("hi".to_owned().into()),
186            ..Default::default()
187        });
188
189        assert_eq!(entry, Annotated::from_json(input).unwrap());
190        assert_eq!(output, entry.to_json_pretty().unwrap());
191    }
192
193    #[test]
194    fn test_logentry_empty_params() {
195        let input = r#"{"params":[]}"#;
196        let entry = Annotated::new(LogEntry {
197            params: Annotated::new(Value::Array(vec![])),
198            ..Default::default()
199        });
200
201        assert_eq!(entry, Annotated::from_json(input).unwrap());
202        assert_eq!(input, entry.to_json().unwrap());
203    }
204
205    #[test]
206    fn test_logentry_named_params() {
207        let json = r#"{
208  "message": "Hello, %s!",
209  "params": {
210    "name": "World"
211  }
212}"#;
213
214        let entry = Annotated::new(LogEntry {
215            message: Annotated::new("Hello, %s!".to_owned().into()),
216            params: Annotated::new(Value::Object({
217                let mut object = Object::new();
218                object.insert(
219                    "name".to_owned(),
220                    Annotated::new(Value::String("World".to_owned())),
221                );
222                object
223            })),
224            ..LogEntry::default()
225        });
226
227        assert_eq!(entry, Annotated::from_json(json).unwrap());
228        assert_eq!(json, entry.to_json_pretty().unwrap());
229    }
230
231    #[test]
232    fn test_logentry_invalid_params() {
233        let json = r#"{
234  "message": "Hello, %s!",
235  "params": 42
236}"#;
237
238        let entry = Annotated::new(LogEntry {
239            message: Annotated::new("Hello, %s!".to_owned().into()),
240            params: Annotated::from_error(
241                Error::expected("message parameters"),
242                Some(Value::I64(42)),
243            ),
244            ..LogEntry::default()
245        });
246
247        assert_eq!(entry, Annotated::from_json(json).unwrap());
248    }
249}