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 #[metastructure(required = true)]
14 pub timestamp: Annotated<Timestamp>,
15
16 #[metastructure(required = true, trim = false)]
18 pub trace_id: Annotated<TraceId>,
19
20 #[metastructure(required = false, trim = false)]
22 pub span_id: Annotated<SpanId>,
23
24 #[metastructure(required = true)]
26 pub level: Annotated<OurLogLevel>,
27
28 #[metastructure(required = true, pii = "true", trim = false)]
30 pub body: Annotated<String>,
31
32 #[metastructure(pii = "true", trim = false)]
34 pub attributes: Annotated<Object<OurLogAttribute>>,
35
36 #[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 #[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(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}