relay_event_schema/protocol/
ourlog.rs

1use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, SkipSerialization, Value};
2use std::collections::BTreeMap;
3use std::fmt::{self, Display};
4
5use serde::{Deserialize, Serialize, Serializer};
6
7use crate::processor::ProcessValue;
8use crate::protocol::{Attributes, SpanId, Timestamp, TraceId};
9
10#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
11#[metastructure(process_func = "process_ourlog", value_type = "OurLog")]
12pub struct OurLog {
13    /// Timestamp when the log was created.
14    #[metastructure(required = true)]
15    pub timestamp: Annotated<Timestamp>,
16
17    /// The ID of the trace the log belongs to.
18    #[metastructure(required = true, trim = false)]
19    pub trace_id: Annotated<TraceId>,
20
21    /// The Span this log entry belongs to.
22    #[metastructure(required = false, trim = false)]
23    pub span_id: Annotated<SpanId>,
24
25    /// The log level.
26    #[metastructure(required = true)]
27    pub level: Annotated<OurLogLevel>,
28
29    /// Log body.
30    #[metastructure(required = true, pii = "true", trim = false)]
31    pub body: Annotated<String>,
32
33    /// Arbitrary attributes on a log.
34    #[metastructure(pii = "true", trim = false)]
35    pub attributes: Annotated<Attributes>,
36
37    /// Additional arbitrary fields for forwards compatibility.
38    #[metastructure(additional_properties, retain = true, pii = "maybe")]
39    pub other: Object<Value>,
40}
41
42/// Relay specific metadata embedded into the log item.
43///
44/// This metadata is purely an internal protocol extension used by Relay,
45/// no one except Relay should be sending this data, nor should anyone except Relay rely on it.
46#[derive(Clone, Debug, Default, Serialize, Deserialize)]
47pub struct OurLogHeader {
48    /// Original (calculated) size of the log item when it was first received by a Relay.
49    ///
50    /// If this value exists, Relay uses it as quantity for all outcomes emitted to the
51    /// log byte data category.
52    pub byte_size: Option<u64>,
53
54    /// Forward compatibility for additional headers.
55    #[serde(flatten)]
56    pub other: BTreeMap<String, Value>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub enum OurLogLevel {
61    Trace,
62    Debug,
63    Info,
64    Warn,
65    Error,
66    Fatal,
67    /// Unknown status, for forward compatibility.
68    Unknown(String),
69}
70
71impl OurLogLevel {
72    fn as_str(&self) -> &str {
73        match self {
74            OurLogLevel::Trace => "trace",
75            OurLogLevel::Debug => "debug",
76            OurLogLevel::Info => "info",
77            OurLogLevel::Warn => "warn",
78            OurLogLevel::Error => "error",
79            OurLogLevel::Fatal => "fatal",
80            OurLogLevel::Unknown(s) => s.as_str(),
81        }
82    }
83}
84
85impl Display for OurLogLevel {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.as_str())
88    }
89}
90
91impl From<String> for OurLogLevel {
92    fn from(value: String) -> Self {
93        match value.as_str() {
94            "trace" => OurLogLevel::Trace,
95            "debug" => OurLogLevel::Debug,
96            "info" => OurLogLevel::Info,
97            "warn" => OurLogLevel::Warn,
98            "error" => OurLogLevel::Error,
99            "fatal" => OurLogLevel::Fatal,
100            _ => OurLogLevel::Unknown(value),
101        }
102    }
103}
104
105impl FromValue for OurLogLevel {
106    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
107        match String::from_value(value) {
108            Annotated(Some(value), meta) => Annotated(Some(value.into()), meta),
109            Annotated(None, meta) => Annotated(None, meta),
110        }
111    }
112}
113
114impl IntoValue for OurLogLevel {
115    fn into_value(self) -> Value {
116        Value::String(self.to_string())
117    }
118
119    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
120    where
121        Self: Sized,
122        S: Serializer,
123    {
124        Serialize::serialize(self.as_str(), s)
125    }
126}
127
128impl ProcessValue for OurLogLevel {}
129
130impl Empty for OurLogLevel {
131    #[inline]
132    fn is_empty(&self) -> bool {
133        false
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use relay_protocol::SerializableAnnotated;
141
142    #[test]
143    fn test_ourlog_serialization() {
144        let json = r#"{
145            "timestamp": 1544719860.0,
146            "trace_id": "5b8efff798038103d269b633813fc60c",
147            "span_id": "eee19b7ec3c1b174",
148            "level": "info",
149            "body": "Example log record",
150            "attributes": {
151                "boolean.attribute": {
152                    "value": true,
153                    "type": "boolean"
154                },
155                "double.attribute": {
156                    "value": 1.23,
157                    "type": "double"
158                },
159                "string.attribute": {
160                    "value": "some string",
161                    "type": "string"
162                },
163                "sentry.severity_text": {
164                    "value": "info",
165                    "type": "string"
166                },
167                "sentry.severity_number": {
168                    "value": "10",
169                    "type": "integer"
170                },
171                "sentry.observed_timestamp_nanos": {
172                    "value": "1544712660300000000",
173                    "type": "integer"
174                },
175                "sentry.trace_flags": {
176                    "value": "10",
177                    "type": "integer"
178                }
179            }
180        }"#;
181
182        let data = Annotated::<OurLog>::from_json(json).unwrap();
183
184        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
185        {
186          "timestamp": 1544719860.0,
187          "trace_id": "5b8efff798038103d269b633813fc60c",
188          "span_id": "eee19b7ec3c1b174",
189          "level": "info",
190          "body": "Example log record",
191          "attributes": {
192            "boolean.attribute": {
193              "type": "boolean",
194              "value": true
195            },
196            "double.attribute": {
197              "type": "double",
198              "value": 1.23
199            },
200            "sentry.observed_timestamp_nanos": {
201              "type": "integer",
202              "value": "1544712660300000000"
203            },
204            "sentry.severity_number": {
205              "type": "integer",
206              "value": "10"
207            },
208            "sentry.severity_text": {
209              "type": "string",
210              "value": "info"
211            },
212            "sentry.trace_flags": {
213              "type": "integer",
214              "value": "10"
215            },
216            "string.attribute": {
217              "type": "string",
218              "value": "some string"
219            }
220          }
221        }
222        "###);
223    }
224
225    #[test]
226    fn test_invalid_int_attribute() {
227        let json = r#"{
228            "timestamp": 1544719860.0,
229            "trace_id": "5b8efff798038103d269b633813fc60c",
230            "span_id": "eee19b7ec3c1b174",
231            "level": "info",
232            "body": "Example log record",
233            "attributes": {
234                "sentry.severity_number": {
235                    "value": 10,
236                    "type": "integer"
237                }
238            }
239        }"#;
240
241        let data = Annotated::<OurLog>::from_json(json).unwrap();
242
243        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
244        {
245          "timestamp": 1544719860.0,
246          "trace_id": "5b8efff798038103d269b633813fc60c",
247          "span_id": "eee19b7ec3c1b174",
248          "level": "info",
249          "body": "Example log record",
250          "attributes": {
251            "sentry.severity_number": {
252              "type": "integer",
253              "value": 10
254            }
255          }
256        }
257        "###);
258    }
259}