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