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::{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<OurLogAttribute>>,
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(Clone, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
48pub struct OurLogAttribute {
49    #[metastructure(flatten)]
50    pub value: OurLogAttributeValue,
51
52    /// Additional arbitrary fields for forwards compatibility.
53    #[metastructure(additional_properties)]
54    pub other: Object<Value>,
55}
56
57impl OurLogAttribute {
58    pub fn new(attribute_type: OurLogAttributeType, value: Value) -> Self {
59        Self {
60            value: OurLogAttributeValue {
61                ty: Annotated::new(attribute_type),
62                value: Annotated::new(value),
63            },
64            other: Object::new(),
65        }
66    }
67}
68
69impl fmt::Debug for OurLogAttribute {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.debug_struct("OurLogAttribute")
72            .field("value", &self.value.value)
73            .field("type", &self.value.ty)
74            .field("other", &self.other)
75            .finish()
76    }
77}
78
79#[derive(Debug, Clone, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
80pub struct OurLogAttributeValue {
81    #[metastructure(field = "type", required = true, trim = false)]
82    pub ty: Annotated<OurLogAttributeType>,
83    #[metastructure(required = true, pii = "true")]
84    pub value: Annotated<Value>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum OurLogAttributeType {
89    Boolean,
90    Integer,
91    Double,
92    String,
93    Unknown(String),
94}
95
96impl ProcessValue for OurLogAttributeType {}
97
98impl OurLogAttributeType {
99    pub fn as_str(&self) -> &str {
100        match self {
101            Self::Boolean => "boolean",
102            Self::Integer => "integer",
103            Self::Double => "double",
104            Self::String => "string",
105            Self::Unknown(value) => value,
106        }
107    }
108
109    pub fn unknown_string() -> String {
110        "unknown".to_string()
111    }
112}
113
114impl fmt::Display for OurLogAttributeType {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "{}", self.as_str())
117    }
118}
119
120impl From<String> for OurLogAttributeType {
121    fn from(value: String) -> Self {
122        match value.as_str() {
123            "boolean" => Self::Boolean,
124            "integer" => Self::Integer,
125            "double" => Self::Double,
126            "string" => Self::String,
127            _ => Self::Unknown(value),
128        }
129    }
130}
131
132impl Empty for OurLogAttributeType {
133    #[inline]
134    fn is_empty(&self) -> bool {
135        false
136    }
137}
138
139impl FromValue for OurLogAttributeType {
140    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
141        match String::from_value(value) {
142            Annotated(Some(value), meta) => Annotated(Some(value.into()), meta),
143            Annotated(None, meta) => Annotated(None, meta),
144        }
145    }
146}
147
148impl IntoValue for OurLogAttributeType {
149    fn into_value(self) -> Value
150    where
151        Self: Sized,
152    {
153        Value::String(match self {
154            Self::Unknown(s) => s,
155            s => s.to_string(),
156        })
157    }
158
159    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
160    where
161        Self: Sized,
162        S: serde::Serializer,
163    {
164        serde::ser::Serialize::serialize(self.as_str(), s)
165    }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
169pub enum OurLogLevel {
170    Trace,
171    Debug,
172    Info,
173    Warn,
174    Error,
175    Fatal,
176    /// Unknown status, for forward compatibility.
177    Unknown(String),
178}
179
180impl OurLogLevel {
181    fn as_str(&self) -> &str {
182        match self {
183            OurLogLevel::Trace => "trace",
184            OurLogLevel::Debug => "debug",
185            OurLogLevel::Info => "info",
186            OurLogLevel::Warn => "warn",
187            OurLogLevel::Error => "error",
188            OurLogLevel::Fatal => "fatal",
189            OurLogLevel::Unknown(s) => s.as_str(),
190        }
191    }
192}
193
194impl Display for OurLogLevel {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        write!(f, "{}", self.as_str())
197    }
198}
199
200impl From<String> for OurLogLevel {
201    fn from(value: String) -> Self {
202        match value.as_str() {
203            "trace" => OurLogLevel::Trace,
204            "debug" => OurLogLevel::Debug,
205            "info" => OurLogLevel::Info,
206            "warn" => OurLogLevel::Warn,
207            "error" => OurLogLevel::Error,
208            "fatal" => OurLogLevel::Fatal,
209            _ => OurLogLevel::Unknown(value),
210        }
211    }
212}
213
214impl FromValue for OurLogLevel {
215    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
216        match String::from_value(value) {
217            Annotated(Some(value), meta) => Annotated(Some(value.into()), meta),
218            Annotated(None, meta) => Annotated(None, meta),
219        }
220    }
221}
222
223impl IntoValue for OurLogLevel {
224    fn into_value(self) -> Value {
225        Value::String(self.to_string())
226    }
227
228    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
229    where
230        Self: Sized,
231        S: Serializer,
232    {
233        Serialize::serialize(self.as_str(), s)
234    }
235}
236
237impl ProcessValue for OurLogLevel {}
238
239impl Empty for OurLogLevel {
240    #[inline]
241    fn is_empty(&self) -> bool {
242        false
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use relay_protocol::SerializableAnnotated;
250
251    #[test]
252    fn test_ourlog_serialization() {
253        let json = r#"{
254            "timestamp": 1544719860.0,
255            "trace_id": "5b8efff798038103d269b633813fc60c",
256            "span_id": "eee19b7ec3c1b174",
257            "level": "info",
258            "body": "Example log record",
259            "attributes": {
260                "boolean.attribute": {
261                    "value": true,
262                    "type": "boolean"
263                },
264                "double.attribute": {
265                    "value": 1.23,
266                    "type": "double"
267                },
268                "string.attribute": {
269                    "value": "some string",
270                    "type": "string"
271                },
272                "sentry.severity_text": {
273                    "value": "info",
274                    "type": "string"
275                },
276                "sentry.severity_number": {
277                    "value": "10",
278                    "type": "integer"
279                },
280                "sentry.observed_timestamp_nanos": {
281                    "value": "1544712660300000000",
282                    "type": "integer"
283                },
284                "sentry.trace_flags": {
285                    "value": "10",
286                    "type": "integer"
287                }
288            }
289        }"#;
290
291        let data = Annotated::<OurLog>::from_json(json).unwrap();
292        insta::assert_debug_snapshot!(data, @r#"
293        OurLog {
294            timestamp: Timestamp(
295                2018-12-13T16:51:00Z,
296            ),
297            trace_id: TraceId("5b8efff798038103d269b633813fc60c"),
298            span_id: SpanId(
299                "eee19b7ec3c1b174",
300            ),
301            level: Info,
302            body: "Example log record",
303            attributes: {
304                "boolean.attribute": OurLogAttribute {
305                    value: Bool(
306                        true,
307                    ),
308                    type: Boolean,
309                    other: {},
310                },
311                "double.attribute": OurLogAttribute {
312                    value: F64(
313                        1.23,
314                    ),
315                    type: Double,
316                    other: {},
317                },
318                "sentry.observed_timestamp_nanos": OurLogAttribute {
319                    value: String(
320                        "1544712660300000000",
321                    ),
322                    type: Integer,
323                    other: {},
324                },
325                "sentry.severity_number": OurLogAttribute {
326                    value: String(
327                        "10",
328                    ),
329                    type: Integer,
330                    other: {},
331                },
332                "sentry.severity_text": OurLogAttribute {
333                    value: String(
334                        "info",
335                    ),
336                    type: String,
337                    other: {},
338                },
339                "sentry.trace_flags": OurLogAttribute {
340                    value: String(
341                        "10",
342                    ),
343                    type: Integer,
344                    other: {},
345                },
346                "string.attribute": OurLogAttribute {
347                    value: String(
348                        "some string",
349                    ),
350                    type: String,
351                    other: {},
352                },
353            },
354            other: {},
355        }
356        "#);
357
358        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
359        {
360          "timestamp": 1544719860.0,
361          "trace_id": "5b8efff798038103d269b633813fc60c",
362          "span_id": "eee19b7ec3c1b174",
363          "level": "info",
364          "body": "Example log record",
365          "attributes": {
366            "boolean.attribute": {
367              "type": "boolean",
368              "value": true
369            },
370            "double.attribute": {
371              "type": "double",
372              "value": 1.23
373            },
374            "sentry.observed_timestamp_nanos": {
375              "type": "integer",
376              "value": "1544712660300000000"
377            },
378            "sentry.severity_number": {
379              "type": "integer",
380              "value": "10"
381            },
382            "sentry.severity_text": {
383              "type": "string",
384              "value": "info"
385            },
386            "sentry.trace_flags": {
387              "type": "integer",
388              "value": "10"
389            },
390            "string.attribute": {
391              "type": "string",
392              "value": "some string"
393            }
394          }
395        }
396        "###);
397    }
398
399    #[test]
400    fn test_invalid_int_attribute() {
401        let json = r#"{
402            "timestamp": 1544719860.0,
403            "trace_id": "5b8efff798038103d269b633813fc60c",
404            "span_id": "eee19b7ec3c1b174",
405            "level": "info",
406            "body": "Example log record",
407            "attributes": {
408                "sentry.severity_number": {
409                    "value": 10,
410                    "type": "integer"
411                }
412            }
413        }"#;
414
415        let data = Annotated::<OurLog>::from_json(json).unwrap();
416
417        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
418        {
419          "timestamp": 1544719860.0,
420          "trace_id": "5b8efff798038103d269b633813fc60c",
421          "span_id": "eee19b7ec3c1b174",
422          "level": "info",
423          "body": "Example log record",
424          "attributes": {
425            "sentry.severity_number": {
426              "type": "integer",
427              "value": 10
428            }
429          }
430        }
431        "###);
432    }
433}