relay_event_schema/protocol/
ourlog.rs

1use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, SkipSerialization, Value};
2use std::fmt::{self, Display};
3
4use serde::{Serialize, Serializer};
5
6use crate::processor::ProcessValue;
7use crate::protocol::{Attribute, SpanId, Timestamp, TraceId};
8
9#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
10#[metastructure(process_func = "process_ourlog", value_type = "OurLog")]
11pub struct OurLog {
12    /// Timestamp when the log was created.
13    #[metastructure(required = true)]
14    pub timestamp: Annotated<Timestamp>,
15
16    /// The ID of the trace the log belongs to.
17    #[metastructure(required = true, trim = false)]
18    pub trace_id: Annotated<TraceId>,
19
20    /// The Span this log entry belongs to.
21    #[metastructure(required = false, trim = false)]
22    pub span_id: Annotated<SpanId>,
23
24    /// The log level.
25    #[metastructure(required = true)]
26    pub level: Annotated<OurLogLevel>,
27
28    /// Log body.
29    #[metastructure(required = true, pii = "true", trim = false)]
30    pub body: Annotated<String>,
31
32    /// Arbitrary attributes on a log.
33    #[metastructure(pii = "true", trim = false)]
34    pub attributes: Annotated<Object<Attribute>>,
35
36    /// Additional arbitrary fields for forwards compatibility.
37    #[metastructure(additional_properties, retain = true, pii = "maybe")]
38    pub other: Object<Value>,
39}
40
41impl OurLog {
42    pub fn attribute(&self, key: &str) -> Option<&Annotated<Value>> {
43        Some(&self.attributes.value()?.get(key)?.value()?.value.value)
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub enum OurLogLevel {
49    Trace,
50    Debug,
51    Info,
52    Warn,
53    Error,
54    Fatal,
55    /// Unknown status, for forward compatibility.
56    Unknown(String),
57}
58
59impl OurLogLevel {
60    fn as_str(&self) -> &str {
61        match self {
62            OurLogLevel::Trace => "trace",
63            OurLogLevel::Debug => "debug",
64            OurLogLevel::Info => "info",
65            OurLogLevel::Warn => "warn",
66            OurLogLevel::Error => "error",
67            OurLogLevel::Fatal => "fatal",
68            OurLogLevel::Unknown(s) => s.as_str(),
69        }
70    }
71}
72
73impl Display for OurLogLevel {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(f, "{}", self.as_str())
76    }
77}
78
79impl From<String> for OurLogLevel {
80    fn from(value: String) -> Self {
81        match value.as_str() {
82            "trace" => OurLogLevel::Trace,
83            "debug" => OurLogLevel::Debug,
84            "info" => OurLogLevel::Info,
85            "warn" => OurLogLevel::Warn,
86            "error" => OurLogLevel::Error,
87            "fatal" => OurLogLevel::Fatal,
88            _ => OurLogLevel::Unknown(value),
89        }
90    }
91}
92
93impl FromValue for OurLogLevel {
94    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
95        match String::from_value(value) {
96            Annotated(Some(value), meta) => Annotated(Some(value.into()), meta),
97            Annotated(None, meta) => Annotated(None, meta),
98        }
99    }
100}
101
102impl IntoValue for OurLogLevel {
103    fn into_value(self) -> Value {
104        Value::String(self.to_string())
105    }
106
107    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
108    where
109        Self: Sized,
110        S: Serializer,
111    {
112        Serialize::serialize(self.as_str(), s)
113    }
114}
115
116impl ProcessValue for OurLogLevel {}
117
118impl Empty for OurLogLevel {
119    #[inline]
120    fn is_empty(&self) -> bool {
121        false
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use relay_protocol::SerializableAnnotated;
129
130    #[test]
131    fn test_ourlog_serialization() {
132        let json = r#"{
133            "timestamp": 1544719860.0,
134            "trace_id": "5b8efff798038103d269b633813fc60c",
135            "span_id": "eee19b7ec3c1b174",
136            "level": "info",
137            "body": "Example log record",
138            "attributes": {
139                "boolean.attribute": {
140                    "value": true,
141                    "type": "boolean"
142                },
143                "double.attribute": {
144                    "value": 1.23,
145                    "type": "double"
146                },
147                "string.attribute": {
148                    "value": "some string",
149                    "type": "string"
150                },
151                "sentry.severity_text": {
152                    "value": "info",
153                    "type": "string"
154                },
155                "sentry.severity_number": {
156                    "value": "10",
157                    "type": "integer"
158                },
159                "sentry.observed_timestamp_nanos": {
160                    "value": "1544712660300000000",
161                    "type": "integer"
162                },
163                "sentry.trace_flags": {
164                    "value": "10",
165                    "type": "integer"
166                }
167            }
168        }"#;
169
170        let data = Annotated::<OurLog>::from_json(json).unwrap();
171        insta::assert_debug_snapshot!(data, @r###"
172        OurLog {
173            timestamp: Timestamp(
174                2018-12-13T16:51:00Z,
175            ),
176            trace_id: TraceId("5b8efff798038103d269b633813fc60c"),
177            span_id: SpanId("eee19b7ec3c1b174"),
178            level: Info,
179            body: "Example log record",
180            attributes: {
181                "boolean.attribute": Attribute {
182                    value: Bool(
183                        true,
184                    ),
185                    type: Boolean,
186                    other: {},
187                },
188                "double.attribute": Attribute {
189                    value: F64(
190                        1.23,
191                    ),
192                    type: Double,
193                    other: {},
194                },
195                "sentry.observed_timestamp_nanos": Attribute {
196                    value: String(
197                        "1544712660300000000",
198                    ),
199                    type: Integer,
200                    other: {},
201                },
202                "sentry.severity_number": Attribute {
203                    value: String(
204                        "10",
205                    ),
206                    type: Integer,
207                    other: {},
208                },
209                "sentry.severity_text": Attribute {
210                    value: String(
211                        "info",
212                    ),
213                    type: String,
214                    other: {},
215                },
216                "sentry.trace_flags": Attribute {
217                    value: String(
218                        "10",
219                    ),
220                    type: Integer,
221                    other: {},
222                },
223                "string.attribute": Attribute {
224                    value: String(
225                        "some string",
226                    ),
227                    type: String,
228                    other: {},
229                },
230            },
231            other: {},
232        }
233        "###);
234
235        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
236        {
237          "timestamp": 1544719860.0,
238          "trace_id": "5b8efff798038103d269b633813fc60c",
239          "span_id": "eee19b7ec3c1b174",
240          "level": "info",
241          "body": "Example log record",
242          "attributes": {
243            "boolean.attribute": {
244              "type": "boolean",
245              "value": true
246            },
247            "double.attribute": {
248              "type": "double",
249              "value": 1.23
250            },
251            "sentry.observed_timestamp_nanos": {
252              "type": "integer",
253              "value": "1544712660300000000"
254            },
255            "sentry.severity_number": {
256              "type": "integer",
257              "value": "10"
258            },
259            "sentry.severity_text": {
260              "type": "string",
261              "value": "info"
262            },
263            "sentry.trace_flags": {
264              "type": "integer",
265              "value": "10"
266            },
267            "string.attribute": {
268              "type": "string",
269              "value": "some string"
270            }
271          }
272        }
273        "###);
274    }
275
276    #[test]
277    fn test_invalid_int_attribute() {
278        let json = r#"{
279            "timestamp": 1544719860.0,
280            "trace_id": "5b8efff798038103d269b633813fc60c",
281            "span_id": "eee19b7ec3c1b174",
282            "level": "info",
283            "body": "Example log record",
284            "attributes": {
285                "sentry.severity_number": {
286                    "value": 10,
287                    "type": "integer"
288                }
289            }
290        }"#;
291
292        let data = Annotated::<OurLog>::from_json(json).unwrap();
293
294        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
295        {
296          "timestamp": 1544719860.0,
297          "trace_id": "5b8efff798038103d269b633813fc60c",
298          "span_id": "eee19b7ec3c1b174",
299          "level": "info",
300          "body": "Example log record",
301          "attributes": {
302            "sentry.severity_number": {
303              "type": "integer",
304              "value": 10
305            }
306          }
307        }
308        "###);
309    }
310}