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::{Attribute, 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<Attribute>>,
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(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub enum OurLogLevel {
49 Trace,
50 Debug,
51 Info,
52 Warn,
53 Error,
54 Fatal,
55 Unknown(String),
57}
58
59impl OurLogLevel {
60 fn as_str(&self) -> &str {
61 match self {
62 OurLogLevel::Trace => "trace",
63 OurLogLevel::Debug => "debug",
64 OurLogLevel::Info => "info",
65 OurLogLevel::Warn => "warn",
66 OurLogLevel::Error => "error",
67 OurLogLevel::Fatal => "fatal",
68 OurLogLevel::Unknown(s) => s.as_str(),
69 }
70 }
71}
72
73impl Display for OurLogLevel {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(f, "{}", self.as_str())
76 }
77}
78
79impl From<String> for OurLogLevel {
80 fn from(value: String) -> Self {
81 match value.as_str() {
82 "trace" => OurLogLevel::Trace,
83 "debug" => OurLogLevel::Debug,
84 "info" => OurLogLevel::Info,
85 "warn" => OurLogLevel::Warn,
86 "error" => OurLogLevel::Error,
87 "fatal" => OurLogLevel::Fatal,
88 _ => OurLogLevel::Unknown(value),
89 }
90 }
91}
92
93impl FromValue for OurLogLevel {
94 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
95 match String::from_value(value) {
96 Annotated(Some(value), meta) => Annotated(Some(value.into()), meta),
97 Annotated(None, meta) => Annotated(None, meta),
98 }
99 }
100}
101
102impl IntoValue for OurLogLevel {
103 fn into_value(self) -> Value {
104 Value::String(self.to_string())
105 }
106
107 fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
108 where
109 Self: Sized,
110 S: Serializer,
111 {
112 Serialize::serialize(self.as_str(), s)
113 }
114}
115
116impl ProcessValue for OurLogLevel {}
117
118impl Empty for OurLogLevel {
119 #[inline]
120 fn is_empty(&self) -> bool {
121 false
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use relay_protocol::SerializableAnnotated;
129
130 #[test]
131 fn test_ourlog_serialization() {
132 let json = r#"{
133 "timestamp": 1544719860.0,
134 "trace_id": "5b8efff798038103d269b633813fc60c",
135 "span_id": "eee19b7ec3c1b174",
136 "level": "info",
137 "body": "Example log record",
138 "attributes": {
139 "boolean.attribute": {
140 "value": true,
141 "type": "boolean"
142 },
143 "double.attribute": {
144 "value": 1.23,
145 "type": "double"
146 },
147 "string.attribute": {
148 "value": "some string",
149 "type": "string"
150 },
151 "sentry.severity_text": {
152 "value": "info",
153 "type": "string"
154 },
155 "sentry.severity_number": {
156 "value": "10",
157 "type": "integer"
158 },
159 "sentry.observed_timestamp_nanos": {
160 "value": "1544712660300000000",
161 "type": "integer"
162 },
163 "sentry.trace_flags": {
164 "value": "10",
165 "type": "integer"
166 }
167 }
168 }"#;
169
170 let data = Annotated::<OurLog>::from_json(json).unwrap();
171 insta::assert_debug_snapshot!(data, @r###"
172 OurLog {
173 timestamp: Timestamp(
174 2018-12-13T16:51:00Z,
175 ),
176 trace_id: TraceId("5b8efff798038103d269b633813fc60c"),
177 span_id: SpanId("eee19b7ec3c1b174"),
178 level: Info,
179 body: "Example log record",
180 attributes: {
181 "boolean.attribute": Attribute {
182 value: Bool(
183 true,
184 ),
185 type: Boolean,
186 other: {},
187 },
188 "double.attribute": Attribute {
189 value: F64(
190 1.23,
191 ),
192 type: Double,
193 other: {},
194 },
195 "sentry.observed_timestamp_nanos": Attribute {
196 value: String(
197 "1544712660300000000",
198 ),
199 type: Integer,
200 other: {},
201 },
202 "sentry.severity_number": Attribute {
203 value: String(
204 "10",
205 ),
206 type: Integer,
207 other: {},
208 },
209 "sentry.severity_text": Attribute {
210 value: String(
211 "info",
212 ),
213 type: String,
214 other: {},
215 },
216 "sentry.trace_flags": Attribute {
217 value: String(
218 "10",
219 ),
220 type: Integer,
221 other: {},
222 },
223 "string.attribute": Attribute {
224 value: String(
225 "some string",
226 ),
227 type: String,
228 other: {},
229 },
230 },
231 other: {},
232 }
233 "###);
234
235 insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
236 {
237 "timestamp": 1544719860.0,
238 "trace_id": "5b8efff798038103d269b633813fc60c",
239 "span_id": "eee19b7ec3c1b174",
240 "level": "info",
241 "body": "Example log record",
242 "attributes": {
243 "boolean.attribute": {
244 "type": "boolean",
245 "value": true
246 },
247 "double.attribute": {
248 "type": "double",
249 "value": 1.23
250 },
251 "sentry.observed_timestamp_nanos": {
252 "type": "integer",
253 "value": "1544712660300000000"
254 },
255 "sentry.severity_number": {
256 "type": "integer",
257 "value": "10"
258 },
259 "sentry.severity_text": {
260 "type": "string",
261 "value": "info"
262 },
263 "sentry.trace_flags": {
264 "type": "integer",
265 "value": "10"
266 },
267 "string.attribute": {
268 "type": "string",
269 "value": "some string"
270 }
271 }
272 }
273 "###);
274 }
275
276 #[test]
277 fn test_invalid_int_attribute() {
278 let json = r#"{
279 "timestamp": 1544719860.0,
280 "trace_id": "5b8efff798038103d269b633813fc60c",
281 "span_id": "eee19b7ec3c1b174",
282 "level": "info",
283 "body": "Example log record",
284 "attributes": {
285 "sentry.severity_number": {
286 "value": 10,
287 "type": "integer"
288 }
289 }
290 }"#;
291
292 let data = Annotated::<OurLog>::from_json(json).unwrap();
293
294 insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
295 {
296 "timestamp": 1544719860.0,
297 "trace_id": "5b8efff798038103d269b633813fc60c",
298 "span_id": "eee19b7ec3c1b174",
299 "level": "info",
300 "body": "Example log record",
301 "attributes": {
302 "sentry.severity_number": {
303 "type": "integer",
304 "value": 10
305 }
306 }
307 }
308 "###);
309 }
310}