From 33f85f6c7e079ee5de1348ead3ced5a66fa3de35 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 26 May 2025 17:58:01 +0200 Subject: [PATCH 01/26] feat(logs): add log protocol types --- sentry-core/src/client.rs | 3 + sentry-types/src/protocol/envelope.rs | 52 +++++- sentry-types/src/protocol/mod.rs | 1 + sentry-types/src/protocol/v7.rs | 228 +++++++++++++++++++++++++ sentry-types/tests/test_protocol_v7.rs | 61 +++++++ 5 files changed, 336 insertions(+), 9 deletions(-) diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index 684f7f2a8..23768157d 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -298,6 +298,8 @@ impl Client { } } + dbg!(&envelope); + transport.send_envelope(envelope); return event_id; } @@ -307,6 +309,7 @@ impl Client { /// Sends the specified [`Envelope`] to sentry. pub fn send_envelope(&self, envelope: Envelope) { + dbg!(&envelope); if let Some(ref transport) = *self.transport.read().unwrap() { transport.send_envelope(envelope); } diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 1d7452f6d..f19727ba6 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use thiserror::Error; use uuid::Uuid; -use super::v7 as protocol; +use super::v7::{self as protocol, ItemsContainer}; use protocol::{ Attachment, AttachmentType, Event, MonitorCheckIn, SessionAggregates, SessionUpdate, @@ -61,9 +61,12 @@ enum EnvelopeItemType { /// An Attachment Item type. #[serde(rename = "attachment")] Attachment, - /// A Monitor Check In Item Type + /// A Monitor Check In Item Type. #[serde(rename = "check_in")] MonitorCheckIn, + /// A Log Items Type. + #[serde(rename = "log")] + Log, } /// An Envelope Item Header. @@ -71,10 +74,15 @@ enum EnvelopeItemType { struct EnvelopeItemHeader { r#type: EnvelopeItemType, length: Option, + + content_type: Option, // Applies (only) to both Attachment and ItemsContainer Item type + // Fields below apply only to Attachment Item type filename: Option, attachment_type: Option, - content_type: Option, + + // Field below applies only to ItemsContainer Item type + item_count: Option, } /// An Envelope Item. @@ -112,6 +120,11 @@ pub enum EnvelopeItem { Attachment(Attachment), /// A MonitorCheckIn item. MonitorCheckIn(MonitorCheckIn), + /// A log item. + /// + /// See the [Log Item documentation](https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item) + /// for more details. + ItemsContainer(ItemsContainer), /// This is a sentinel item used to `filter` raw envelopes. Raw, // TODO: @@ -154,6 +167,12 @@ impl From for EnvelopeItem { } } +impl From for EnvelopeItem { + fn from(container: ItemsContainer) -> Self { + EnvelopeItem::ItemsContainer(container) + } +} + /// An Iterator over the items of an Envelope. #[derive(Clone)] pub struct EnvelopeItemIter<'s> { @@ -352,6 +371,7 @@ impl Envelope { EnvelopeItem::MonitorCheckIn(check_in) => { serde_json::to_writer(&mut item_buf, check_in)? } + EnvelopeItem::ItemsContainer(items) => serde_json::to_writer(&mut item_buf, items)?, EnvelopeItem::Raw => { continue; } @@ -362,14 +382,26 @@ impl Envelope { EnvelopeItem::SessionAggregates(_) => "sessions", EnvelopeItem::Transaction(_) => "transaction", EnvelopeItem::MonitorCheckIn(_) => "check_in", + EnvelopeItem::ItemsContainer(container) => container.items_type(), EnvelopeItem::Attachment(_) | EnvelopeItem::Raw => unreachable!(), }; - writeln!( - writer, - r#"{{"type":"{}","length":{}}}"#, - item_type, - item_buf.len() - )?; + + if let EnvelopeItem::ItemsContainer(container) = item { + writeln!( + writer, + r#"{{"type":"{}","item_count":{},"content_type":"{}"}}"#, + item_type, + container.len(), + container.content_type() + )?; + } else { + writeln!( + writer, + r#"{{"type":"{}","length":{}}}"#, + item_type, + item_buf.len() + )?; + } writer.write_all(&item_buf)?; writeln!(writer)?; item_buf.clear(); @@ -506,6 +538,8 @@ impl Envelope { EnvelopeItemType::MonitorCheckIn => { serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn) } + EnvelopeItemType::Log => serde_json::from_slice(payload) + .map(|logs| EnvelopeItem::ItemsContainer(ItemsContainer::Logs(logs))), } .map_err(EnvelopeError::InvalidItemPayload)?; diff --git a/sentry-types/src/protocol/mod.rs b/sentry-types/src/protocol/mod.rs index bf0d8977d..c365a906f 100644 --- a/sentry-types/src/protocol/mod.rs +++ b/sentry-types/src/protocol/mod.rs @@ -14,6 +14,7 @@ pub const LATEST: u16 = 7; pub use v7 as latest; mod attachment; + mod envelope; mod monitor; mod session; diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index bb2008f60..c2bf3d928 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -16,6 +16,7 @@ use std::str; use std::time::SystemTime; use self::debugid::{CodeId, DebugId}; +use serde::de::Unexpected; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; @@ -2106,3 +2107,230 @@ impl fmt::Display for Transaction<'_> { ) } } + +/// An homogeneous container for multiple items. +/// This is considered a single envelope item. +/// The serialized value for the `type` of this envelope item will match the type of the contained +/// items. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum ItemsContainer { + /// A list of logs. + #[serde(rename = "items")] + Logs(Vec), +} + +impl ItemsContainer { + /// Returns the number of items in this container. + pub fn len(&self) -> usize { + match self { + ItemsContainer::Logs(logs) => logs.len(), + } + } + + /// Returns the type of the items in this container as a string. + pub fn items_type(&self) -> &'static str { + match self { + ItemsContainer::Logs(_) => "log", + } + } + + /// Returns the content-type expected by Relay for this container. + pub fn content_type(&self) -> &'static str { + match self { + ItemsContainer::Logs(_) => "application/vnd.sentry.items.log+json", + } + } +} + +/// A single log. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct LogItem { + /// The severity of the log (required). + pub level: LogLevel, + /// The log body/message (required). + pub body: String, + /// The ID of the Trace in which this log happened (required). + pub trace_id: TraceId, + /// The timestamp of the log (required). + #[serde(with = "ts_seconds_float")] + pub timestamp: SystemTime, + /// The severity number of the log (required). + pub severity_number: LogSeverityNumber, + /// Additional arbitrary attributes attached to the log. + #[serde(default, skip_serializing_if = "Map::is_empty")] + pub attributes: Map, +} + +impl From> for ItemsContainer { + fn from(logs: Vec) -> Self { + Self::Logs(logs) + } +} + +/// A string indicating the severity of a log, according to the +/// OpenTelemetry [`SeverityText`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitytext) spec. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + /// A fine-grained debugging event. + Trace, + /// A debugging event. + Debug, + /// An informational event. Indicates that an event happened. + Info, + /// A warning event. Not an error but is likely more important than an informational event. + Warn, + /// An error event. Something went wrong. + Error, + /// A fatal error such as application or system crash. + Fatal, +} + +/// A number indicating the severity of a log, according to the OpenTelemetry +/// [`SeverityNumber`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber) spec. +/// This should be a number between 1 and 24 (inclusive). +pub type LogSeverityNumber = u8; + +/// An attribute that can be attached to a log. +#[derive(Clone, Debug, PartialEq)] +pub struct LogAttribute(pub Value); + +impl From for LogAttribute { + fn from(value: Value) -> Self { + Self(value) + } +} + +impl Serialize for LogAttribute { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("LogAttribute", 2)?; + + match &self.0 { + Value::String(s) => { + state.serialize_field("value", s.as_str())?; + state.serialize_field("type", "string")?; + } + Value::Number(n) => { + if let Some(i) = n.as_i64() { + state.serialize_field("value", &i)?; + state.serialize_field("type", "integer")?; + } else if let Some(f) = n.as_f64() { + state.serialize_field("value", &f)?; + state.serialize_field("type", "double")?; + } else { + // This should be unreachable, as a `Value::Number` can only be built from an i64, u64 or f64 + state.serialize_field("value", &n.to_string())?; + state.serialize_field("type", "string")?; + } + } + Value::Bool(b) => { + state.serialize_field("value", &b)?; + state.serialize_field("type", "boolean")?; + } + // For any other type (Null, Array, Object), convert to string + _ => { + state.serialize_field("value", &self.0.to_string())?; + state.serialize_field("type", "string")?; + } + } + + state.end() + } +} + +impl<'de> Deserialize<'de> for LogAttribute { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{self, MapAccess, Visitor}; + use std::fmt; + + struct LogAttributeVisitor; + + impl<'de> Visitor<'de> for LogAttributeVisitor { + type Value = LogAttribute; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a LogAttribute with value and type fields") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut value: Option = None; + let mut type_str: Option = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "value" => { + if value.is_some() { + return Err(de::Error::duplicate_field("value")); + } + value = Some(map.next_value()?); + } + "type" => { + if type_str.is_some() { + return Err(de::Error::duplicate_field("type")); + } + type_str = Some(map.next_value()?); + } + _ => { + // Ignore unknown fields + let _: serde_json::Value = map.next_value()?; + } + } + } + + let value = value.ok_or_else(|| de::Error::missing_field("value"))?; + let type_str = type_str.ok_or_else(|| de::Error::missing_field("type"))?; + + match type_str.as_str() { + "string" => { + if !value.is_string() { + return Err(de::Error::custom( + "type is 'string' but value is not a string", + )); + } + } + "integer" => { + if !value.is_i64() { + return Err(de::Error::custom( + "type is 'integer' but value is not an integer", + )); + } + } + "double" => { + if !value.is_f64() { + return Err(de::Error::custom( + "type is 'double' but value is not a double", + )); + } + } + "boolean" => { + if !value.is_boolean() { + return Err(de::Error::custom( + "type is 'boolean' but value is not a boolean", + )); + } + } + _ => { + return Err(de::Error::custom(format!( + "expected type to be 'string' | 'integer' | 'double' | 'boolean', found {}", + type_str + ))) + } + } + + Ok(LogAttribute(value)) + } + } + + deserializer.deserialize_map(LogAttributeVisitor) + } +} diff --git a/sentry-types/tests/test_protocol_v7.rs b/sentry-types/tests/test_protocol_v7.rs index ea5b92756..b79c61640 100644 --- a/sentry-types/tests/test_protocol_v7.rs +++ b/sentry-types/tests/test_protocol_v7.rs @@ -1523,3 +1523,64 @@ fn test_orientation() { "\"portrait\"" ); } + +mod test_logs { + use sentry_types::protocol::v7::LogAttribute; + use serde_json::{json, Value}; + + #[test] + fn test_log_attribute_serialization() { + let attributes = vec![ + // Supported types + ( + LogAttribute(Value::from(42)), + r#"{"value":42,"type":"integer"}"#, + ), + ( + LogAttribute(Value::from(3.14)), + r#"{"value":3.14,"type":"double"}"#, + ), + ( + LogAttribute(Value::from("lol")), + r#"{"value":"lol","type":"string"}"#, + ), + ( + LogAttribute(Value::from(false)), + r#"{"value":false,"type":"boolean"}"#, + ), + // Special case + ( + LogAttribute(Value::Null), + r#"{"value":"null","type":"string"}"#, + ), + // Unsupported types (for now) + ( + LogAttribute(json!(r#"[1,2,3,4]"#)), + r#"{"value":"[1,2,3,4]","type":"string"}"#, + ), + ( + LogAttribute(json!(r#"["a","b","c"]"#)), + r#"{"value":"[\"a\",\"b\",\"c\"]","type":"string"}"#, + ), + ]; + for (attribute, expected) in attributes { + let actual = serde_json::to_string(&attribute).unwrap(); + assert_eq!(expected, actual); + } + } + + #[test] + fn test_log_attribute_roundtrip() { + let attributes = vec![ + LogAttribute(Value::from(42)), + LogAttribute(Value::from(3.14)), + LogAttribute(Value::from("lol")), + LogAttribute(Value::from(false)), + ]; + for expected in attributes { + let serialized = serde_json::to_string(&expected).unwrap(); + let actual: LogAttribute = serde_json::from_str(&serialized).unwrap(); + assert_eq!(expected, actual); + } + } +} From fc0dbedff76401781c35771da43adeadc6345625 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 26 May 2025 18:00:16 +0200 Subject: [PATCH 02/26] remove unnecessary type --- sentry-types/src/protocol/envelope.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index f19727ba6..fa1bb2930 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -74,15 +74,10 @@ enum EnvelopeItemType { struct EnvelopeItemHeader { r#type: EnvelopeItemType, length: Option, - - content_type: Option, // Applies (only) to both Attachment and ItemsContainer Item type - // Fields below apply only to Attachment Item type filename: Option, attachment_type: Option, - - // Field below applies only to ItemsContainer Item type - item_count: Option, + content_type: Option, } /// An Envelope Item. From a1c5800c4b5d78264475db77c9bd6b4daa323000 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 26 May 2025 18:03:29 +0200 Subject: [PATCH 03/26] lints --- sentry-types/src/protocol/v7.rs | 2 +- sentry-types/tests/test_protocol_v7.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index c2bf3d928..4250b1730 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -16,7 +16,6 @@ use std::str; use std::time::SystemTime; use self::debugid::{CodeId, DebugId}; -use serde::de::Unexpected; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; @@ -2119,6 +2118,7 @@ pub enum ItemsContainer { Logs(Vec), } +#[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")] impl ItemsContainer { /// Returns the number of items in this container. pub fn len(&self) -> usize { diff --git a/sentry-types/tests/test_protocol_v7.rs b/sentry-types/tests/test_protocol_v7.rs index b79c61640..0e9da3d15 100644 --- a/sentry-types/tests/test_protocol_v7.rs +++ b/sentry-types/tests/test_protocol_v7.rs @@ -1537,8 +1537,8 @@ mod test_logs { r#"{"value":42,"type":"integer"}"#, ), ( - LogAttribute(Value::from(3.14)), - r#"{"value":3.14,"type":"double"}"#, + LogAttribute(Value::from(3.1)), + r#"{"value":3.1,"type":"double"}"#, ), ( LogAttribute(Value::from("lol")), @@ -1573,7 +1573,7 @@ mod test_logs { fn test_log_attribute_roundtrip() { let attributes = vec![ LogAttribute(Value::from(42)), - LogAttribute(Value::from(3.14)), + LogAttribute(Value::from(3.1)), LogAttribute(Value::from("lol")), LogAttribute(Value::from(false)), ]; From e57ccabafcc26eefccfc57e5ea3b80ae080970d4 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 26 May 2025 18:07:02 +0200 Subject: [PATCH 04/26] remove debug prints --- sentry-core/src/client.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index 23768157d..684f7f2a8 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -298,8 +298,6 @@ impl Client { } } - dbg!(&envelope); - transport.send_envelope(envelope); return event_id; } @@ -309,7 +307,6 @@ impl Client { /// Sends the specified [`Envelope`] to sentry. pub fn send_envelope(&self, envelope: Envelope) { - dbg!(&envelope); if let Some(ref transport) = *self.transport.read().unwrap() { transport.send_envelope(envelope); } From 3c903a0117c612754a1ba2baeb40804e80162de5 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 26 May 2025 18:07:22 +0200 Subject: [PATCH 05/26] details --- sentry-types/src/protocol/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry-types/src/protocol/mod.rs b/sentry-types/src/protocol/mod.rs index c365a906f..bf0d8977d 100644 --- a/sentry-types/src/protocol/mod.rs +++ b/sentry-types/src/protocol/mod.rs @@ -14,7 +14,6 @@ pub const LATEST: u16 = 7; pub use v7 as latest; mod attachment; - mod envelope; mod monitor; mod session; From f44b00195946f0fa1a0a760d463e03fd47bc8c80 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 26 May 2025 18:13:54 +0200 Subject: [PATCH 06/26] docstring --- sentry-types/src/protocol/envelope.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index fa1bb2930..6f23e17fb 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -115,9 +115,9 @@ pub enum EnvelopeItem { Attachment(Attachment), /// A MonitorCheckIn item. MonitorCheckIn(MonitorCheckIn), - /// A log item. + /// A container for multiple batched items. /// - /// See the [Log Item documentation](https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item) + /// Currently, this is only used for logs. See the [Log Item documentation](https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item) /// for more details. ItemsContainer(ItemsContainer), /// This is a sentinel item used to `filter` raw envelopes. From ad09fab6c5aebc4978147932b326bc622e625419 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 27 May 2025 16:03:15 +0200 Subject: [PATCH 07/26] add full envelope test --- sentry-types/src/protocol/envelope.rs | 33 +++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 6f23e17fb..bb33aa16e 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -565,13 +565,15 @@ mod test { use std::str::FromStr; use std::time::{Duration, SystemTime}; + use protocol::Map; + use serde_json::Value; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; use super::*; use crate::protocol::v7::{ - Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SessionAttributes, - SessionStatus, Span, + Level, LogAttribute, LogItem, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, + SessionAttributes, SessionStatus, Span, }; fn to_str(envelope: Envelope) -> String { @@ -998,12 +1000,39 @@ some content ..Default::default() }; + let mut attributes = Map::new(); + attributes.insert("key".into(), Value::from("value").into()); + attributes.insert("num".into(), Value::from(10).into()); + attributes.insert("val".into(), Value::from(10.2).into()); + attributes.insert("bool".into(), Value::from(false).into()); + let mut attributes_2 = attributes.clone(); + attributes_2.insert("more".into(), Value::from(true).into()); + let logs = ItemsContainer::Logs(vec![ + LogItem { + level: protocol::LogLevel::Warn, + body: "test".to_owned(), + trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(), + timestamp: timestamp("2022-07-25T14:51:14.296Z"), + severity_number: 10, + attributes, + }, + LogItem { + level: protocol::LogLevel::Error, + body: "a body".to_owned(), + trace_id: "332253d614472a2c9f89e232b7762c28".parse().unwrap(), + timestamp: timestamp("2021-07-21T14:51:14.296Z"), + severity_number: 10, + attributes: attributes_2, + }, + ]); + let mut envelope: Envelope = Envelope::new(); envelope.add_item(event); envelope.add_item(transaction); envelope.add_item(session); envelope.add_item(attachment); + envelope.add_item(logs); let serialized = to_str(envelope); let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap(); From 89b0e166b76f549214e575c05ce2da7442804aef Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 27 May 2025 16:03:43 +0200 Subject: [PATCH 08/26] cargo fmt --- sentry-actix/examples/basic.rs | 2 +- sentry-actix/src/lib.rs | 4 ++-- sentry-types/src/protocol/envelope.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry-actix/examples/basic.rs b/sentry-actix/examples/basic.rs index 6e4544fbc..acde8561a 100644 --- a/sentry-actix/examples/basic.rs +++ b/sentry-actix/examples/basic.rs @@ -11,7 +11,7 @@ async fn healthy(_req: HttpRequest) -> Result { #[get("/err")] async fn errors(_req: HttpRequest) -> Result { - Err(io::Error::new(io::ErrorKind::Other, "An error happens here").into()) + Err(io::Error::other("An error happens here").into()) } #[get("/msg")] diff --git a/sentry-actix/src/lib.rs b/sentry-actix/src/lib.rs index 78938d548..4a2de94fe 100644 --- a/sentry-actix/src/lib.rs +++ b/sentry-actix/src/lib.rs @@ -589,7 +589,7 @@ mod tests { // Current hub should have no events _assert_hub_no_events(); - Err(io::Error::new(io::ErrorKind::Other, "Test Error").into()) + Err(io::Error::other("Test Error").into()) } let app = init_service( @@ -652,7 +652,7 @@ mod tests { async fn original_transaction(_req: HttpRequest) -> Result { // Override transaction name sentry::configure_scope(|scope| scope.set_transaction(Some("new_transaction"))); - Err(io::Error::new(io::ErrorKind::Other, "Test Error").into()) + Err(io::Error::other("Test Error").into()) } let app = init_service( diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index bb33aa16e..6cfb3f027 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -572,7 +572,7 @@ mod test { use super::*; use crate::protocol::v7::{ - Level, LogAttribute, LogItem, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, + Level, LogItem, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SessionAttributes, SessionStatus, Span, }; From 046f5e4b0e3dbcfd10db92913b5f49ded154e07f Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 27 May 2025 16:04:39 +0200 Subject: [PATCH 09/26] cargo fmt --- sentry-types/src/protocol/envelope.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 6cfb3f027..74a3b793e 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -572,8 +572,8 @@ mod test { use super::*; use crate::protocol::v7::{ - Level, LogItem, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, - SessionAttributes, SessionStatus, Span, + Level, LogItem, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SessionAttributes, + SessionStatus, Span, }; fn to_str(envelope: Envelope) -> String { From 7a33e3672055ad0de044ea39434cad4205450206 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 27 May 2025 16:12:13 +0200 Subject: [PATCH 10/26] changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 687a9227c..1efe4883a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +- feat(logs): add log protocol types (#821) by @lcian + - Basic types for [Sentry structured logs](https://docs.sentry.io/product/explore/logs/) have been added. + - It's possible to use them to send logs to Sentry by directly constructing an `Envelope` containing an `ItemsContainer::Logs` item and sending it through `Client::send_envelope`. + - A high-level API and integrations will come in the next release. + ## 0.38.1 ### Fixes From f67fa673b0cc7a236b92d11bdf3864242893b25a Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 11:08:53 +0200 Subject: [PATCH 11/26] wip --- sentry-types/src/protocol/envelope.rs | 19 ++++--- sentry-types/src/protocol/v7.rs | 73 ++++++++++++++++++--------- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 74a3b793e..c3668edc2 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use thiserror::Error; use uuid::Uuid; -use super::v7::{self as protocol, ItemsContainer}; +use super::v7::{self as protocol, ConcreteItemsContainer, ItemsContainer}; use protocol::{ Attachment, AttachmentType, Event, MonitorCheckIn, SessionAggregates, SessionUpdate, @@ -64,7 +64,7 @@ enum EnvelopeItemType { /// A Monitor Check In Item Type. #[serde(rename = "check_in")] MonitorCheckIn, - /// A Log Items Type. + /// A Log (Container) Items Type. #[serde(rename = "log")] Log, } @@ -119,7 +119,7 @@ pub enum EnvelopeItem { /// /// Currently, this is only used for logs. See the [Log Item documentation](https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item) /// for more details. - ItemsContainer(ItemsContainer), + ItemsContainer(ConcreteItemsContainer), /// This is a sentinel item used to `filter` raw envelopes. Raw, // TODO: @@ -467,6 +467,8 @@ impl Envelope { let bytes = slice .get(offset..) .ok_or(EnvelopeError::MissingItemHeader)?; + println!("item"); + println!("{}", String::from_utf8_lossy(bytes)); let (item, item_size) = Self::parse_item(bytes)?; offset += item_size; items.push(item); @@ -512,6 +514,8 @@ impl Envelope { }; let payload = slice.get(payload_start..payload_end).unwrap(); + println!("payload: {}", String::from_utf8_lossy(payload)); + println!("type: {:?}", header.r#type); let item = match header.r#type { EnvelopeItemType::Event => serde_json::from_slice(payload).map(EnvelopeItem::Event), @@ -534,7 +538,7 @@ impl Envelope { serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn) } EnvelopeItemType::Log => serde_json::from_slice(payload) - .map(|logs| EnvelopeItem::ItemsContainer(ItemsContainer::Logs(logs))), + .map(EnvelopeItem::ItemsContainer) } .map_err(EnvelopeError::InvalidItemPayload)?; @@ -1007,7 +1011,7 @@ some content attributes.insert("bool".into(), Value::from(false).into()); let mut attributes_2 = attributes.clone(); attributes_2.insert("more".into(), Value::from(true).into()); - let logs = ItemsContainer::Logs(vec![ + let logs: ItemsContainer = vec![ LogItem { level: protocol::LogLevel::Warn, body: "test".to_owned(), @@ -1022,9 +1026,9 @@ some content trace_id: "332253d614472a2c9f89e232b7762c28".parse().unwrap(), timestamp: timestamp("2021-07-21T14:51:14.296Z"), severity_number: 10, - attributes: attributes_2, + attributes: attributes_2, }, - ]); + ].into(); let mut envelope: Envelope = Envelope::new(); @@ -1035,6 +1039,7 @@ some content envelope.add_item(logs); let serialized = to_str(envelope); + print!("serialized: {}", serialized); let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap(); assert_eq!(serialized, to_str(deserialized)) } diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 4250b1730..fa4510b57 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2112,34 +2112,52 @@ impl fmt::Display for Transaction<'_> { /// The serialized value for the `type` of this envelope item will match the type of the contained /// items. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub enum ItemsContainer { - /// A list of logs. - #[serde(rename = "items")] - Logs(Vec), +pub struct ItemsContainer { + pub items: Vec, } -#[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")] -impl ItemsContainer { - /// Returns the number of items in this container. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum ConcreteItemsContainer { + Logs(ItemsContainer) +} + +impl ItemsContainer { pub fn len(&self) -> usize { - match self { - ItemsContainer::Logs(logs) => logs.len(), - } + self.items.len() } +} - /// Returns the type of the items in this container as a string. - pub fn items_type(&self) -> &'static str { - match self { - ItemsContainer::Logs(_) => "log", - } - } +pub trait ContainerItem: FromValue + IntoValue { + /// The expected content type of the container for this type. + const CONTENT_TYPE: ContentType; +} - /// Returns the content-type expected by Relay for this container. - pub fn content_type(&self) -> &'static str { - match self { - ItemsContainer::Logs(_) => "application/vnd.sentry.items.log+json", - } - } +/// A container for multiple homogeneous envelope items. +/// +/// Item containers are used to minimize the amount of single envelope items contained in an +/// envelope. They massively improve parsing speed of envelopes in Relay but are also used +/// to minimize metadata duplication on item headers. +/// +/// Especially for small envelope items with high quantities (e.g. logs), this drastically +/// improves fast path parsing speeds and minimizes serialization overheads, by minimizing +/// the amount of items in an envelope. +/// +/// An item container does not have a special [`super::ItemType`], but is identified by the +/// content type of the item. +#[derive(Debug)] +pub struct ItemContainer { + items: ContainerItems, +} + +/// A list of items in an item container. +pub type ContainerItems = Vec<[Annotated; 3]>; + +/// Any item contained in an [`ItemContainer`] needs to implement this trait. +pub trait ContainerItem { + /// The serialized type of the iteem. + const TYPE: &'static str; + /// The content type expected by Relay for the container of this type. + const CONTENT_TYPE: &'static str; } /// A single log. @@ -2161,9 +2179,14 @@ pub struct LogItem { pub attributes: Map, } -impl From> for ItemsContainer { - fn from(logs: Vec) -> Self { - Self::Logs(logs) +impl Containable for LogItem { + const TYPE: &'static str = "log"; + const CONTENT_TYPE: &'static str = "whatever"; +} + +impl From> for ItemsContainer { + fn from(items: Vec) -> Self { + ItemsContainer { items } } } From 58098aeae889827241b8f1812a38dc09994ccb72 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 12:53:37 +0200 Subject: [PATCH 12/26] refactor --- sentry-types/src/protocol/envelope.rs | 95 +++++++++++++++++++-------- sentry-types/src/protocol/v7.rs | 67 +------------------ 2 files changed, 69 insertions(+), 93 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index c3668edc2..0dfd170cb 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -1,14 +1,14 @@ use std::{io::Write, path::Path}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; -use super::v7::{self as protocol, ConcreteItemsContainer, ItemsContainer}; +use super::v7::{self as protocol}; use protocol::{ Attachment, AttachmentType, Event, MonitorCheckIn, SessionAggregates, SessionUpdate, - Transaction, + Transaction, Log }; /// Raised if a envelope cannot be parsed from a given input. @@ -66,7 +66,7 @@ enum EnvelopeItemType { MonitorCheckIn, /// A Log (Container) Items Type. #[serde(rename = "log")] - Log, + LogsContainer, } /// An Envelope Item Header. @@ -74,10 +74,11 @@ enum EnvelopeItemType { struct EnvelopeItemHeader { r#type: EnvelopeItemType, length: Option, - // Fields below apply only to Attachment Item type + // Applies both to Attachment and ItemContainer Item type + content_type: Option, + // Fields below apply only to Attachment Item types filename: Option, attachment_type: Option, - content_type: Option, } /// An Envelope Item. @@ -116,16 +117,48 @@ pub enum EnvelopeItem { /// A MonitorCheckIn item. MonitorCheckIn(MonitorCheckIn), /// A container for multiple batched items. - /// - /// Currently, this is only used for logs. See the [Log Item documentation](https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item) - /// for more details. - ItemsContainer(ConcreteItemsContainer), + ItemContainer(ItemContainer), /// This is a sentinel item used to `filter` raw envelopes. Raw, // TODO: // etc… } +#[derive(Deserialize, Clone, Debug, PartialEq)] +pub enum ItemContainer { + Logs(Vec), +} + +impl ItemContainer { + fn len(&self) -> usize { + match self { + Self::Logs(logs) => logs.len(), + } + } + + fn ty(&self) -> &'static str { + match self { + Self::Logs(_) => "log", + } + } + + fn content_type(&self) -> &'static str { + match self { + Self::Logs(_) => "application/vnd.sentry.items.log+json", + } + } +} + +#[derive(Serialize)] +struct LogsSerializationWrapper<'a> { + items: &'a [Log], +} + +#[derive(Deserialize)] +struct LogsDeserializationWrapper { + items: Vec, +} + impl From> for EnvelopeItem { fn from(event: Event<'static>) -> Self { EnvelopeItem::Event(event) @@ -162,9 +195,9 @@ impl From for EnvelopeItem { } } -impl From for EnvelopeItem { - fn from(container: ItemsContainer) -> Self { - EnvelopeItem::ItemsContainer(container) +impl From for EnvelopeItem { + fn from(container: ItemContainer) -> Self { + EnvelopeItem::ItemContainer(container) } } @@ -366,7 +399,14 @@ impl Envelope { EnvelopeItem::MonitorCheckIn(check_in) => { serde_json::to_writer(&mut item_buf, check_in)? } - EnvelopeItem::ItemsContainer(items) => serde_json::to_writer(&mut item_buf, items)?, + EnvelopeItem::ItemContainer(container) => { + match container { + ItemContainer::Logs(logs) => { + let wrapper = LogsSerializationWrapper { items: logs }; + serde_json::to_writer(&mut item_buf, &wrapper)? + } + } + } EnvelopeItem::Raw => { continue; } @@ -377,11 +417,15 @@ impl Envelope { EnvelopeItem::SessionAggregates(_) => "sessions", EnvelopeItem::Transaction(_) => "transaction", EnvelopeItem::MonitorCheckIn(_) => "check_in", - EnvelopeItem::ItemsContainer(container) => container.items_type(), + EnvelopeItem::ItemContainer(container) => { + match container { + ItemContainer::Logs(_) => "log", + } + } EnvelopeItem::Attachment(_) | EnvelopeItem::Raw => unreachable!(), }; - if let EnvelopeItem::ItemsContainer(container) = item { + if let EnvelopeItem::ItemContainer(container) = item { writeln!( writer, r#"{{"type":"{}","item_count":{},"content_type":"{}"}}"#, @@ -467,8 +511,6 @@ impl Envelope { let bytes = slice .get(offset..) .ok_or(EnvelopeError::MissingItemHeader)?; - println!("item"); - println!("{}", String::from_utf8_lossy(bytes)); let (item, item_size) = Self::parse_item(bytes)?; offset += item_size; items.push(item); @@ -514,8 +556,6 @@ impl Envelope { }; let payload = slice.get(payload_start..payload_end).unwrap(); - println!("payload: {}", String::from_utf8_lossy(payload)); - println!("type: {:?}", header.r#type); let item = match header.r#type { EnvelopeItemType::Event => serde_json::from_slice(payload).map(EnvelopeItem::Event), @@ -537,8 +577,8 @@ impl Envelope { EnvelopeItemType::MonitorCheckIn => { serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn) } - EnvelopeItemType::Log => serde_json::from_slice(payload) - .map(EnvelopeItem::ItemsContainer) + EnvelopeItemType::LogsContainer => serde_json::from_slice::(payload) + .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items))), } .map_err(EnvelopeError::InvalidItemPayload)?; @@ -576,7 +616,7 @@ mod test { use super::*; use crate::protocol::v7::{ - Level, LogItem, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SessionAttributes, + Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SessionAttributes, SessionStatus, Span, }; @@ -1011,8 +1051,8 @@ some content attributes.insert("bool".into(), Value::from(false).into()); let mut attributes_2 = attributes.clone(); attributes_2.insert("more".into(), Value::from(true).into()); - let logs: ItemsContainer = vec![ - LogItem { + let logs = ItemContainer::Logs(vec![ + Log { level: protocol::LogLevel::Warn, body: "test".to_owned(), trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(), @@ -1020,7 +1060,7 @@ some content severity_number: 10, attributes, }, - LogItem { + Log { level: protocol::LogLevel::Error, body: "a body".to_owned(), trace_id: "332253d614472a2c9f89e232b7762c28".parse().unwrap(), @@ -1028,7 +1068,7 @@ some content severity_number: 10, attributes: attributes_2, }, - ].into(); + ]); let mut envelope: Envelope = Envelope::new(); @@ -1040,6 +1080,7 @@ some content let serialized = to_str(envelope); print!("serialized: {}", serialized); + std::fs::write("output.json", &serialized).expect("Unable to write file"); let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap(); assert_eq!(serialized, to_str(deserialized)) } diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index fa4510b57..4ba7d3ad1 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -25,7 +25,6 @@ pub use uuid::Uuid; use crate::utils::{ts_rfc3339_opt, ts_seconds_float}; pub use super::attachment::*; -pub use super::envelope::*; pub use super::monitor::*; pub use super::session::*; @@ -2107,62 +2106,9 @@ impl fmt::Display for Transaction<'_> { } } -/// An homogeneous container for multiple items. -/// This is considered a single envelope item. -/// The serialized value for the `type` of this envelope item will match the type of the contained -/// items. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct ItemsContainer { - pub items: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub enum ConcreteItemsContainer { - Logs(ItemsContainer) -} - -impl ItemsContainer { - pub fn len(&self) -> usize { - self.items.len() - } -} - -pub trait ContainerItem: FromValue + IntoValue { - /// The expected content type of the container for this type. - const CONTENT_TYPE: ContentType; -} - -/// A container for multiple homogeneous envelope items. -/// -/// Item containers are used to minimize the amount of single envelope items contained in an -/// envelope. They massively improve parsing speed of envelopes in Relay but are also used -/// to minimize metadata duplication on item headers. -/// -/// Especially for small envelope items with high quantities (e.g. logs), this drastically -/// improves fast path parsing speeds and minimizes serialization overheads, by minimizing -/// the amount of items in an envelope. -/// -/// An item container does not have a special [`super::ItemType`], but is identified by the -/// content type of the item. -#[derive(Debug)] -pub struct ItemContainer { - items: ContainerItems, -} - -/// A list of items in an item container. -pub type ContainerItems = Vec<[Annotated; 3]>; - -/// Any item contained in an [`ItemContainer`] needs to implement this trait. -pub trait ContainerItem { - /// The serialized type of the iteem. - const TYPE: &'static str; - /// The content type expected by Relay for the container of this type. - const CONTENT_TYPE: &'static str; -} - /// A single log. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct LogItem { +pub struct Log { /// The severity of the log (required). pub level: LogLevel, /// The log body/message (required). @@ -2179,17 +2125,6 @@ pub struct LogItem { pub attributes: Map, } -impl Containable for LogItem { - const TYPE: &'static str = "log"; - const CONTENT_TYPE: &'static str = "whatever"; -} - -impl From> for ItemsContainer { - fn from(items: Vec) -> Self { - ItemsContainer { items } - } -} - /// A string indicating the severity of a log, according to the /// OpenTelemetry [`SeverityText`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitytext) spec. #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] From 8ed9e157d516207056cc660e55968e9275b0cbbf Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 13:06:05 +0200 Subject: [PATCH 13/26] refactor --- sentry-types/output.json | 11 ++++++ sentry-types/src/protocol/envelope.rs | 50 +++++++++++++++------------ sentry-types/src/protocol/v7.rs | 1 + 3 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 sentry-types/output.json diff --git a/sentry-types/output.json b/sentry-types/output.json new file mode 100644 index 000000000..46232d669 --- /dev/null +++ b/sentry-types/output.json @@ -0,0 +1,11 @@ +{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"} +{"type":"event","length":74} +{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296} +{"type":"transaction","length":200} +{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9d","start_timestamp":1595256674.296,"spans":[{"span_id":"d42cee9fc3e74f5c","trace_id":"335e53d614474acc9f89e632b776cc28","start_timestamp":1595256674.296}]} +{"type":"session","length":222} +{"sid":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c","did":"foo@bar.baz","started":"2020-07-20T14:51:14.296Z","init":true,"duration":1.234,"status":"ok","errors":123,"attrs":{"release":"foo-bar@1.2.3","environment":"production"}} +{"type":"attachment","length":12,"filename":"file.txt","attachment_type":"event.attachment","content_type":"application/octet-stream"} +some content +{"type":"log","item_count":2,"content_type":"application/vnd.sentry.items.log+json"} +{"items":[{"level":"warn","body":"test","trace_id":"335e53d614474acc9f89e632b776cc28","timestamp":1658760674.296,"severity_number":10,"attributes":{"bool":{"value":false,"type":"boolean"},"key":{"value":"value","type":"string"},"num":{"value":10,"type":"integer"},"val":{"value":10.2,"type":"double"}}},{"level":"error","body":"a body","trace_id":"332253d614472a2c9f89e232b7762c28","timestamp":1626879074.296,"severity_number":10,"attributes":{"bool":{"value":false,"type":"boolean"},"key":{"value":"value","type":"string"},"more":{"value":true,"type":"boolean"},"num":{"value":10,"type":"integer"},"val":{"value":10.2,"type":"double"}}}]} diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 0dfd170cb..0608d4c65 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -7,8 +7,8 @@ use uuid::Uuid; use super::v7::{self as protocol}; use protocol::{ - Attachment, AttachmentType, Event, MonitorCheckIn, SessionAggregates, SessionUpdate, - Transaction, Log + Attachment, AttachmentType, Event, Log, MonitorCheckIn, SessionAggregates, SessionUpdate, + Transaction, }; /// Raised if a envelope cannot be parsed from a given input. @@ -116,7 +116,7 @@ pub enum EnvelopeItem { Attachment(Attachment), /// A MonitorCheckIn item. MonitorCheckIn(MonitorCheckIn), - /// A container for multiple batched items. + /// A container for a list of multiple items. ItemContainer(ItemContainer), /// This is a sentinel item used to `filter` raw envelopes. Raw, @@ -124,25 +124,33 @@ pub enum EnvelopeItem { // etc… } -#[derive(Deserialize, Clone, Debug, PartialEq)] +/// A container for a list of multiple items. +/// It's considered a single envelope item, with its `type` corresponding to the contained items' +/// `type`. +#[derive(Clone, Debug, PartialEq)] pub enum ItemContainer { + /// A list of logs. Logs(Vec), } - + +#[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")] impl ItemContainer { - fn len(&self) -> usize { + /// The number of items in this item container. + pub fn len(&self) -> usize { match self { Self::Logs(logs) => logs.len(), } } - fn ty(&self) -> &'static str { + /// The `type` of this item container, which corresponds to the `type` of the contained items. + pub fn ty(&self) -> &'static str { match self { Self::Logs(_) => "log", } } - fn content_type(&self) -> &'static str { + /// The `content-type` expected by Relay for this item container. + pub fn content_type(&self) -> &'static str { match self { Self::Logs(_) => "application/vnd.sentry.items.log+json", } @@ -399,14 +407,12 @@ impl Envelope { EnvelopeItem::MonitorCheckIn(check_in) => { serde_json::to_writer(&mut item_buf, check_in)? } - EnvelopeItem::ItemContainer(container) => { - match container { - ItemContainer::Logs(logs) => { - let wrapper = LogsSerializationWrapper { items: logs }; - serde_json::to_writer(&mut item_buf, &wrapper)? - } + EnvelopeItem::ItemContainer(container) => match container { + ItemContainer::Logs(logs) => { + let wrapper = LogsSerializationWrapper { items: logs }; + serde_json::to_writer(&mut item_buf, &wrapper)? } - } + }, EnvelopeItem::Raw => { continue; } @@ -417,11 +423,7 @@ impl Envelope { EnvelopeItem::SessionAggregates(_) => "sessions", EnvelopeItem::Transaction(_) => "transaction", EnvelopeItem::MonitorCheckIn(_) => "check_in", - EnvelopeItem::ItemContainer(container) => { - match container { - ItemContainer::Logs(_) => "log", - } - } + EnvelopeItem::ItemContainer(container) => container.ty(), EnvelopeItem::Attachment(_) | EnvelopeItem::Raw => unreachable!(), }; @@ -577,8 +579,10 @@ impl Envelope { EnvelopeItemType::MonitorCheckIn => { serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn) } - EnvelopeItemType::LogsContainer => serde_json::from_slice::(payload) - .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items))), + EnvelopeItemType::LogsContainer => { + serde_json::from_slice::(payload) + .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items))) + } } .map_err(EnvelopeError::InvalidItemPayload)?; @@ -1066,7 +1070,7 @@ some content trace_id: "332253d614472a2c9f89e232b7762c28".parse().unwrap(), timestamp: timestamp("2021-07-21T14:51:14.296Z"), severity_number: 10, - attributes: attributes_2, + attributes: attributes_2, }, ]); diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 4ba7d3ad1..616bf425b 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -25,6 +25,7 @@ pub use uuid::Uuid; use crate::utils::{ts_rfc3339_opt, ts_seconds_float}; pub use super::attachment::*; +pub use super::envelope::*; pub use super::monitor::*; pub use super::session::*; From 4d8e557e5fb1556903b4555624e1c378e7f9d05c Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 13:06:35 +0200 Subject: [PATCH 14/26] remove file --- sentry-types/output.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 sentry-types/output.json diff --git a/sentry-types/output.json b/sentry-types/output.json deleted file mode 100644 index 46232d669..000000000 --- a/sentry-types/output.json +++ /dev/null @@ -1,11 +0,0 @@ -{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"} -{"type":"event","length":74} -{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296} -{"type":"transaction","length":200} -{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9d","start_timestamp":1595256674.296,"spans":[{"span_id":"d42cee9fc3e74f5c","trace_id":"335e53d614474acc9f89e632b776cc28","start_timestamp":1595256674.296}]} -{"type":"session","length":222} -{"sid":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c","did":"foo@bar.baz","started":"2020-07-20T14:51:14.296Z","init":true,"duration":1.234,"status":"ok","errors":123,"attrs":{"release":"foo-bar@1.2.3","environment":"production"}} -{"type":"attachment","length":12,"filename":"file.txt","attachment_type":"event.attachment","content_type":"application/octet-stream"} -some content -{"type":"log","item_count":2,"content_type":"application/vnd.sentry.items.log+json"} -{"items":[{"level":"warn","body":"test","trace_id":"335e53d614474acc9f89e632b776cc28","timestamp":1658760674.296,"severity_number":10,"attributes":{"bool":{"value":false,"type":"boolean"},"key":{"value":"value","type":"string"},"num":{"value":10,"type":"integer"},"val":{"value":10.2,"type":"double"}}},{"level":"error","body":"a body","trace_id":"332253d614472a2c9f89e232b7762c28","timestamp":1626879074.296,"severity_number":10,"attributes":{"bool":{"value":false,"type":"boolean"},"key":{"value":"value","type":"string"},"more":{"value":true,"type":"boolean"},"num":{"value":10,"type":"integer"},"val":{"value":10.2,"type":"double"}}}]} From a6ba441ab0753d9621ca92881488e1faa611c9f8 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 13:13:32 +0200 Subject: [PATCH 15/26] add from --- sentry-types/src/protocol/envelope.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 0608d4c65..485b30d70 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; -use super::v7::{self as protocol}; +use super::v7 as protocol; use protocol::{ Attachment, AttachmentType, Event, Log, MonitorCheckIn, SessionAggregates, SessionUpdate, @@ -133,6 +133,12 @@ pub enum ItemContainer { Logs(Vec), } +impl From> for ItemContainer { + fn from(logs: Vec) -> Self { + Self::Logs(logs) + } +} + #[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")] impl ItemContainer { /// The number of items in this item container. @@ -209,6 +215,12 @@ impl From for EnvelopeItem { } } +impl From> for EnvelopeItem { + fn from(logs: Vec) -> Self { + EnvelopeItem::ItemContainer(logs.into()) + } +} + /// An Iterator over the items of an Envelope. #[derive(Clone)] pub struct EnvelopeItemIter<'s> { @@ -1055,7 +1067,7 @@ some content attributes.insert("bool".into(), Value::from(false).into()); let mut attributes_2 = attributes.clone(); attributes_2.insert("more".into(), Value::from(true).into()); - let logs = ItemContainer::Logs(vec![ + let logs: EnvelopeItem = vec![ Log { level: protocol::LogLevel::Warn, body: "test".to_owned(), @@ -1072,7 +1084,7 @@ some content severity_number: 10, attributes: attributes_2, }, - ]); + ].into(); let mut envelope: Envelope = Envelope::new(); From 51b24707593a21bb841418d17c097fe0d04ca363 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 13:26:43 +0200 Subject: [PATCH 16/26] severity number changes --- sentry-types/src/protocol/envelope.rs | 11 +++++------ sentry-types/src/protocol/v7.rs | 25 ++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 485b30d70..cef060c78 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -135,7 +135,7 @@ pub enum ItemContainer { impl From> for ItemContainer { fn from(logs: Vec) -> Self { - Self::Logs(logs) + Self::Logs(logs) } } @@ -1073,7 +1073,7 @@ some content body: "test".to_owned(), trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(), timestamp: timestamp("2022-07-25T14:51:14.296Z"), - severity_number: 10, + severity_number: 1.try_into().unwrap(), attributes, }, Log { @@ -1081,10 +1081,11 @@ some content body: "a body".to_owned(), trace_id: "332253d614472a2c9f89e232b7762c28".parse().unwrap(), timestamp: timestamp("2021-07-21T14:51:14.296Z"), - severity_number: 10, + severity_number: 1.try_into().unwrap(), attributes: attributes_2, }, - ].into(); + ] + .into(); let mut envelope: Envelope = Envelope::new(); @@ -1095,8 +1096,6 @@ some content envelope.add_item(logs); let serialized = to_str(envelope); - print!("serialized: {}", serialized); - std::fs::write("output.json", &serialized).expect("Unable to write file"); let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap(); assert_eq!(serialized, to_str(deserialized)) } diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 5e959650d..1ed570c1d 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2138,7 +2138,7 @@ pub struct Log { pub attributes: Map, } -/// A string indicating the severity of a log, according to the +/// Indicates the severity of a log, according to the /// OpenTelemetry [`SeverityText`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitytext) spec. #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] #[serde(rename_all = "lowercase")] @@ -2159,8 +2159,27 @@ pub enum LogLevel { /// A number indicating the severity of a log, according to the OpenTelemetry /// [`SeverityNumber`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber) spec. -/// This should be a number between 1 and 24 (inclusive). -pub type LogSeverityNumber = u8; +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] +pub struct LogSeverityNumber(u8); + +impl LogSeverityNumber { + /// The minimum severity number. + pub const MIN: u8 = 1; + /// The maximum severity number. + pub const MAX: u8 = 24; +} + +impl TryFrom for LogSeverityNumber { + type Error = &'static str; + + fn try_from(value: u8) -> Result { + if (LogSeverityNumber::MIN..=LogSeverityNumber::MAX).contains(&value) { + Ok(Self(value)) + } else { + Err("Log severity number must be between 1 and 24 (inclusive)") + } + } +} /// An attribute that can be attached to a log. #[derive(Clone, Debug, PartialEq)] From 7ef7157a66b87a1e221e64281fc48cad89b9e839 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 13:41:23 +0200 Subject: [PATCH 17/26] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f67a3bfd..74b3b97bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ - feat(logs): add log protocol types (#821) by @lcian - Basic types for [Sentry structured logs](https://docs.sentry.io/product/explore/logs/) have been added. - - It's possible to use them to send logs to Sentry by directly constructing an `Envelope` containing an `ItemContainer::Logs` envelope item and sending it through `Client::send_envelope`. + - It's possible (but not recommended) to use them to send logs to Sentry by directly constructing an `Envelope` containing an `ItemContainer::Logs` envelope item and sending it through `Client::send_envelope`. - A high-level API and integrations will come soon. ## 0.38.1 From bd6e2937b03645416c5796919bd995040c5ccfb6 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 13:49:11 +0200 Subject: [PATCH 18/26] improve --- sentry-types/src/protocol/v7.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 1ed570c1d..2d93c9764 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2170,13 +2170,17 @@ impl LogSeverityNumber { } impl TryFrom for LogSeverityNumber { - type Error = &'static str; + type Error = String; fn try_from(value: u8) -> Result { if (LogSeverityNumber::MIN..=LogSeverityNumber::MAX).contains(&value) { Ok(Self(value)) } else { - Err("Log severity number must be between 1 and 24 (inclusive)") + Err(format!( + "Log severity number must be between {} and {}", + LogSeverityNumber::MIN, + LogSeverityNumber::MAX + )) } } } From 2cff09d59da661ad93044d6a62781894ac4659d6 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 13:51:14 +0200 Subject: [PATCH 19/26] improve --- sentry-types/src/protocol/envelope.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index cef060c78..be8f67ba0 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -64,7 +64,7 @@ enum EnvelopeItemType { /// A Monitor Check In Item Type. #[serde(rename = "check_in")] MonitorCheckIn, - /// A Log (Container) Items Type. + /// A container of Log items. #[serde(rename = "log")] LogsContainer, } From 1a3a0889c4339ad15e59b52f4230604db643f279 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 13:52:54 +0200 Subject: [PATCH 20/26] improve --- sentry-types/src/protocol/envelope.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index be8f67ba0..e3311af5e 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -133,12 +133,6 @@ pub enum ItemContainer { Logs(Vec), } -impl From> for ItemContainer { - fn from(logs: Vec) -> Self { - Self::Logs(logs) - } -} - #[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")] impl ItemContainer { /// The number of items in this item container. @@ -163,6 +157,12 @@ impl ItemContainer { } } +impl From> for ItemContainer { + fn from(logs: Vec) -> Self { + Self::Logs(logs) + } +} + #[derive(Serialize)] struct LogsSerializationWrapper<'a> { items: &'a [Log], From e915747a06f95af007ad0c06a566d2d0dff19186 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 28 May 2025 13:56:13 +0200 Subject: [PATCH 21/26] improve --- sentry-types/src/protocol/v7.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 2d93c9764..75292ddf5 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2225,7 +2225,7 @@ impl Serialize for LogAttribute { state.serialize_field("value", &b)?; state.serialize_field("type", "boolean")?; } - // For any other type (Null, Array, Object), convert to string + // For any other type (Null, Array, Object), convert to string with JSON representation _ => { state.serialize_field("value", &self.0.to_string())?; state.serialize_field("type", "string")?; From de8a0dfee3407194ca7d2e660d84f8f9249f131a Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 3 Jun 2025 16:17:32 +0200 Subject: [PATCH 22/26] non-exhaustive --- sentry-types/src/protocol/envelope.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index e3311af5e..33837d676 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -128,6 +128,7 @@ pub enum EnvelopeItem { /// It's considered a single envelope item, with its `type` corresponding to the contained items' /// `type`. #[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] pub enum ItemContainer { /// A list of logs. Logs(Vec), From d851cb87be8540f89d4b23bddbe651a15cca12b3 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 3 Jun 2025 16:31:37 +0200 Subject: [PATCH 23/26] generic from value --- sentry-types/src/protocol/envelope.rs | 11 +++++------ sentry-types/src/protocol/v7.rs | 9 ++++++--- sentry-types/tests/test_protocol_v7.rs | 22 ++++++++-------------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 33837d676..03db0e72d 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -627,7 +627,6 @@ mod test { use std::time::{Duration, SystemTime}; use protocol::Map; - use serde_json::Value; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; @@ -1062,12 +1061,12 @@ some content }; let mut attributes = Map::new(); - attributes.insert("key".into(), Value::from("value").into()); - attributes.insert("num".into(), Value::from(10).into()); - attributes.insert("val".into(), Value::from(10.2).into()); - attributes.insert("bool".into(), Value::from(false).into()); + attributes.insert("key".into(), "value".into()); + attributes.insert("num".into(), 10.into()); + attributes.insert("val".into(), 10.2.into()); + attributes.insert("bool".into(), false.into()); let mut attributes_2 = attributes.clone(); - attributes_2.insert("more".into(), Value::from(true).into()); + attributes_2.insert("more".into(), true.into()); let logs: EnvelopeItem = vec![ Log { level: protocol::LogLevel::Warn, diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 75292ddf5..a5562fd5e 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2189,9 +2189,12 @@ impl TryFrom for LogSeverityNumber { #[derive(Clone, Debug, PartialEq)] pub struct LogAttribute(pub Value); -impl From for LogAttribute { - fn from(value: Value) -> Self { - Self(value) +impl From for LogAttribute +where + Value: From, +{ + fn from(value: T) -> Self { + Self(Value::from(value)) } } diff --git a/sentry-types/tests/test_protocol_v7.rs b/sentry-types/tests/test_protocol_v7.rs index 0e9da3d15..9fa2a1707 100644 --- a/sentry-types/tests/test_protocol_v7.rs +++ b/sentry-types/tests/test_protocol_v7.rs @@ -1532,20 +1532,14 @@ mod test_logs { fn test_log_attribute_serialization() { let attributes = vec![ // Supported types + (LogAttribute(42.into()), r#"{"value":42,"type":"integer"}"#), + (LogAttribute(3.1.into()), r#"{"value":3.1,"type":"double"}"#), ( - LogAttribute(Value::from(42)), - r#"{"value":42,"type":"integer"}"#, - ), - ( - LogAttribute(Value::from(3.1)), - r#"{"value":3.1,"type":"double"}"#, - ), - ( - LogAttribute(Value::from("lol")), + LogAttribute("lol".into()), r#"{"value":"lol","type":"string"}"#, ), ( - LogAttribute(Value::from(false)), + LogAttribute(false.into()), r#"{"value":false,"type":"boolean"}"#, ), // Special case @@ -1572,10 +1566,10 @@ mod test_logs { #[test] fn test_log_attribute_roundtrip() { let attributes = vec![ - LogAttribute(Value::from(42)), - LogAttribute(Value::from(3.1)), - LogAttribute(Value::from("lol")), - LogAttribute(Value::from(false)), + LogAttribute(42.into()), + LogAttribute(3.1.into()), + LogAttribute("lol".into()), + LogAttribute(false.into()), ]; for expected in attributes { let serialized = serde_json::to_string(&expected).unwrap(); From a36c7696b76b056f0e37dff77e54b9cdeb7edbdc Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 3 Jun 2025 16:36:07 +0200 Subject: [PATCH 24/26] severity_number optional --- sentry-types/src/protocol/envelope.rs | 4 ++-- sentry-types/src/protocol/v7.rs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 03db0e72d..006c59caf 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -1073,7 +1073,7 @@ some content body: "test".to_owned(), trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(), timestamp: timestamp("2022-07-25T14:51:14.296Z"), - severity_number: 1.try_into().unwrap(), + severity_number: Some(1.try_into().unwrap()), attributes, }, Log { @@ -1081,7 +1081,7 @@ some content body: "a body".to_owned(), trace_id: "332253d614472a2c9f89e232b7762c28".parse().unwrap(), timestamp: timestamp("2021-07-21T14:51:14.296Z"), - severity_number: 1.try_into().unwrap(), + severity_number: Some(1.try_into().unwrap()), attributes: attributes_2, }, ] diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index a5562fd5e..4fc8ec48c 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2131,8 +2131,9 @@ pub struct Log { /// The timestamp of the log (required). #[serde(with = "ts_seconds_float")] pub timestamp: SystemTime, - /// The severity number of the log (required). - pub severity_number: LogSeverityNumber, + /// The severity number of the log. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub severity_number: Option, /// Additional arbitrary attributes attached to the log. #[serde(default, skip_serializing_if = "Map::is_empty")] pub attributes: Map, From b4070c1b25c42c1af8c250e7e2bb5b8b3a7de75e Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 4 Jun 2025 10:32:41 +0200 Subject: [PATCH 25/26] simplify test --- sentry-types/tests/test_protocol_v7.rs | 32 ++++++++------------------ 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/sentry-types/tests/test_protocol_v7.rs b/sentry-types/tests/test_protocol_v7.rs index 9fa2a1707..88a567cc2 100644 --- a/sentry-types/tests/test_protocol_v7.rs +++ b/sentry-types/tests/test_protocol_v7.rs @@ -1530,30 +1530,21 @@ mod test_logs { #[test] fn test_log_attribute_serialization() { - let attributes = vec![ + let attributes: Vec<(LogAttribute, &str)> = vec![ // Supported types - (LogAttribute(42.into()), r#"{"value":42,"type":"integer"}"#), - (LogAttribute(3.1.into()), r#"{"value":3.1,"type":"double"}"#), - ( - LogAttribute("lol".into()), - r#"{"value":"lol","type":"string"}"#, - ), - ( - LogAttribute(false.into()), - r#"{"value":false,"type":"boolean"}"#, - ), + (42.into(), r#"{"value":42,"type":"integer"}"#), + (3.1.into(), r#"{"value":3.1,"type":"double"}"#), + ("lol".into(), r#"{"value":"lol","type":"string"}"#), + (false.into(), r#"{"value":false,"type":"boolean"}"#), // Special case - ( - LogAttribute(Value::Null), - r#"{"value":"null","type":"string"}"#, - ), + (Value::Null.into(), r#"{"value":"null","type":"string"}"#), // Unsupported types (for now) ( - LogAttribute(json!(r#"[1,2,3,4]"#)), + json!(r#"[1,2,3,4]"#).into(), r#"{"value":"[1,2,3,4]","type":"string"}"#, ), ( - LogAttribute(json!(r#"["a","b","c"]"#)), + json!(r#"["a","b","c"]"#).into(), r#"{"value":"[\"a\",\"b\",\"c\"]","type":"string"}"#, ), ]; @@ -1565,12 +1556,7 @@ mod test_logs { #[test] fn test_log_attribute_roundtrip() { - let attributes = vec![ - LogAttribute(42.into()), - LogAttribute(3.1.into()), - LogAttribute("lol".into()), - LogAttribute(false.into()), - ]; + let attributes: Vec = vec![42.into(), 3.1.into(), "lol".into(), false.into()]; for expected in attributes { let serialized = serde_json::to_string(&expected).unwrap(); let actual: LogAttribute = serde_json::from_str(&serialized).unwrap(); From 85704c001bd8e8ae085b66784188961743cea5bc Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 4 Jun 2025 10:35:50 +0200 Subject: [PATCH 26/26] link to docs in code --- sentry-types/src/protocol/v7.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 4fc8ec48c..d984035a1 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2119,7 +2119,7 @@ impl fmt::Display for Transaction<'_> { } } -/// A single log. +/// A single [structured log](https://docs.sentry.io/product/explore/logs/). #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct Log { /// The severity of the log (required).