diff --git a/CHANGELOG.md b/CHANGELOG.md index aa586ef9..ba719c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,18 @@ # Changelog -### Unreleased +## Unreleased ### Breaking changes - refactor: remove `debug-logs` feature (#820) by @lcian - - The deprecated `debug-logs` feature of the `sentry` crate has been removed. + - The deprecated `debug-logs` feature of the `sentry` crate, used for the SDK's own internal logging, has been removed. + +### Behavioral changes + +- feat(core): implement Tracing without Performance (#811) by @lcian + - The SDK now implements Tracing without Performance, which makes it so that each `Scope` is associated with an object holding some tracing information. + - This information is used as a fallback when capturing an event with tracing disabled or otherwise no ongoing span, to still allow related events to be linked by a trace. + - A new API `Scope::iter_trace_propagation_headers` has been provided that will use the fallback tracing information if there is no current `Span` on the `Scope`. ## 0.38.1 diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index f36ed6b9..39ee2363 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -199,14 +199,12 @@ impl TransactionContext { sentry_trace: &SentryTrace, span_id: Option, ) -> Self { - let (trace_id, parent_span_id, sampled) = - (sentry_trace.0, Some(sentry_trace.1), sentry_trace.2); Self { name: name.into(), op: op.into(), - trace_id, - parent_span_id, - sampled, + trace_id: sentry_trace.trace_id, + parent_span_id: Some(sentry_trace.span_id), + sampled: sentry_trace.sampled, span_id: span_id.unwrap_or_default(), custom: None, } @@ -476,6 +474,8 @@ impl TransactionOrSpan { } /// Returns the headers needed for distributed tracing. + /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active + /// trace's distributed tracing headers. pub fn iter_headers(&self) -> TraceHeadersIter { match self { TransactionOrSpan::Transaction(transaction) => transaction.iter_headers(), @@ -774,9 +774,11 @@ impl Transaction { } /// Returns the headers needed for distributed tracing. + /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active + /// trace's distributed tracing headers. pub fn iter_headers(&self) -> TraceHeadersIter { let inner = self.inner.lock().unwrap(); - let trace = SentryTrace( + let trace = SentryTrace::new( inner.context.trace_id, inner.context.span_id, Some(inner.sampled), @@ -1026,9 +1028,11 @@ impl Span { } /// Returns the headers needed for distributed tracing. + /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active + /// trace's distributed tracing headers. pub fn iter_headers(&self) -> TraceHeadersIter { let span = self.span.lock().unwrap(); - let trace = SentryTrace(span.trace_id, span.span_id, Some(self.sampled)); + let trace = SentryTrace::new(span.trace_id, span.span_id, Some(self.sampled)); TraceHeadersIter { sentry_trace: Some(trace.to_string()), } @@ -1125,6 +1129,9 @@ impl Span { } } +/// Represents a key-value pair such as an HTTP header. +pub type TraceHeader = (&'static str, String); + /// An Iterator over HTTP header names and values needed for distributed tracing. /// /// This currently only yields the `sentry-trace` header, but other headers @@ -1133,6 +1140,15 @@ pub struct TraceHeadersIter { sentry_trace: Option, } +impl TraceHeadersIter { + #[cfg(feature = "client")] + pub(crate) fn new(sentry_trace: String) -> Self { + Self { + sentry_trace: Some(sentry_trace), + } + } +} + impl Iterator for TraceHeadersIter { type Item = (&'static str, String); @@ -1143,8 +1159,12 @@ impl Iterator for TraceHeadersIter { /// A container for distributed tracing metadata that can be extracted from e.g. the `sentry-trace` /// HTTP header. -#[derive(Debug, PartialEq)] -pub struct SentryTrace(protocol::TraceId, protocol::SpanId, Option); +#[derive(Debug, PartialEq, Clone, Copy, Default)] +pub struct SentryTrace { + pub(crate) trace_id: protocol::TraceId, + pub(crate) span_id: protocol::SpanId, + pub(crate) sampled: Option, +} impl SentryTrace { /// Creates a new [`SentryTrace`] from the provided parameters @@ -1153,7 +1173,11 @@ impl SentryTrace { span_id: protocol::SpanId, sampled: Option, ) -> Self { - SentryTrace(trace_id, span_id, sampled) + SentryTrace { + trace_id, + span_id, + sampled, + } } } @@ -1169,7 +1193,7 @@ fn parse_sentry_trace(header: &str) -> Option { _ => None, }); - Some(SentryTrace(trace_id, parent_span_id, parent_sampled)) + Some(SentryTrace::new(trace_id, parent_span_id, parent_sampled)) } /// Extracts distributed tracing metadata from headers (or, generally, key-value pairs), @@ -1189,8 +1213,8 @@ pub fn parse_headers<'a, I: IntoIterator>( impl std::fmt::Display for SentryTrace { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}-{}", self.0, self.1)?; - if let Some(sampled) = self.2 { + write!(f, "{}-{}", self.trace_id, self.span_id)?; + if let Some(sampled) = self.sampled { write!(f, "-{}", if sampled { '1' } else { '0' })?; } Ok(()) @@ -1211,10 +1235,10 @@ mod tests { let trace = parse_sentry_trace("09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0"); assert_eq!( trace, - Some(SentryTrace(trace_id, parent_trace_id, Some(false))) + Some(SentryTrace::new(trace_id, parent_trace_id, Some(false))) ); - let trace = SentryTrace(Default::default(), Default::default(), None); + let trace = SentryTrace::new(Default::default(), Default::default(), None); let parsed = parse_sentry_trace(&trace.to_string()); assert_eq!(parsed, Some(trace)); } @@ -1233,8 +1257,11 @@ mod tests { let header = span.iter_headers().next().unwrap().1; let parsed = parse_sentry_trace(&header).unwrap(); - assert_eq!(&parsed.0.to_string(), "09e04486820349518ac7b5d2adbf6ba5"); - assert_eq!(parsed.2, Some(true)); + assert_eq!( + &parsed.trace_id.to_string(), + "09e04486820349518ac7b5d2adbf6ba5" + ); + assert_eq!(parsed.sampled, Some(true)); } #[test] diff --git a/sentry-core/src/scope/real.rs b/sentry-core/src/scope/real.rs index 7c4eda47..47b1e9da 100644 --- a/sentry-core/src/scope/real.rs +++ b/sentry-core/src/scope/real.rs @@ -5,11 +5,13 @@ use std::fmt; use std::sync::Mutex; use std::sync::{Arc, PoisonError, RwLock}; +use sentry_types::protocol::v7::TraceContext; + use crate::performance::TransactionOrSpan; use crate::protocol::{Attachment, Breadcrumb, Context, Event, Level, Transaction, User, Value}; #[cfg(feature = "release-health")] use crate::session::Session; -use crate::Client; +use crate::{Client, SentryTrace, TraceHeader, TraceHeadersIter}; #[derive(Debug)] pub struct Stack { @@ -52,6 +54,7 @@ pub struct Scope { pub(crate) session: Arc>>, pub(crate) span: Arc>, pub(crate) attachments: Arc>, + pub(crate) propagation_context: SentryTrace, } impl fmt::Debug for Scope { @@ -74,6 +77,7 @@ impl fmt::Debug for Scope { debug_struct .field("span", &self.span) .field("attachments", &self.attachments.len()) + .field("propagation_context", &self.propagation_context) .finish() } } @@ -289,6 +293,8 @@ impl Scope { if let Some(span) = self.span.as_ref() { span.apply_to_event(&mut event); + } else { + self.apply_propagation_context(&mut event); } if event.transaction.is_none() { @@ -357,4 +363,31 @@ impl Scope { session.update_from_event(event); } } + + pub(crate) fn apply_propagation_context(&self, event: &mut Event<'_>) { + if event.contexts.contains_key("trace") { + return; + } + + let context = TraceContext { + trace_id: self.propagation_context.trace_id, + span_id: self.propagation_context.span_id, + ..Default::default() + }; + event.contexts.insert("trace".into(), context.into()); + } + + /// Returns the headers needed for distributed tracing. + pub fn iter_trace_propagation_headers(&self) -> impl Iterator { + if let Some(span) = self.get_span() { + span.iter_headers() + } else { + let data = SentryTrace::new( + self.propagation_context.trace_id, + self.propagation_context.span_id, + None, + ); + TraceHeadersIter::new(data.to_string()) + } + } } diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index bb2008f6..12de8639 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -1352,7 +1352,7 @@ pub struct OtelContext { } /// Holds the identifier for a Span -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)] #[serde(try_from = "String", into = "String")] pub struct SpanId([u8; 8]); @@ -1368,6 +1368,12 @@ impl fmt::Display for SpanId { } } +impl fmt::Debug for SpanId { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "SpanId({})", self) + } +} + impl From for String { fn from(span_id: SpanId) -> Self { span_id.to_string() @@ -1399,7 +1405,7 @@ impl From<[u8; 8]> for SpanId { } /// Holds the identifier for a Trace -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)] #[serde(try_from = "String", into = "String")] pub struct TraceId([u8; 16]); @@ -1415,6 +1421,12 @@ impl fmt::Display for TraceId { } } +impl fmt::Debug for TraceId { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "TraceId({})", self) + } +} + impl From for String { fn from(trace_id: TraceId) -> Self { trace_id.to_string() diff --git a/sentry/tests/test_basic.rs b/sentry/tests/test_basic.rs index 3813b14e..dac87e37 100644 --- a/sentry/tests/test_basic.rs +++ b/sentry/tests/test_basic.rs @@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use sentry::protocol::{Attachment, EnvelopeItem}; +use sentry::protocol::{Attachment, Context, EnvelopeItem}; use sentry::types::Uuid; #[test] @@ -28,6 +28,25 @@ fn test_basic_capture_message() { assert_eq!(Some(event.event_id), last_event_id); } +#[test] +fn test_event_trace_context_from_propagation_context() { + let mut last_event_id = None::; + let mut span = None; + let events = sentry::test::with_captured_events(|| { + sentry::configure_scope(|scope| { + span = scope.get_span(); + }); + sentry::capture_message("Hello World!", sentry::Level::Warning); + last_event_id = sentry::last_event_id(); + }); + assert_eq!(events.len(), 1); + let event = events.into_iter().next().unwrap(); + + let trace_context = event.contexts.get("trace"); + assert!(span.is_none()); + assert!(matches!(trace_context, Some(Context::Trace(_)))); +} + #[test] fn test_breadcrumbs() { let events = sentry::test::with_captured_events(|| {