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