Skip to main content

relay_event_schema/protocol/ourlog/
mod.rs

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