diff --git a/CHANGELOG.md b/CHANGELOG.md index 64d492cf3..317c2331b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ ### New Features +- Added [`TracePropagationContext`](https://docs.rs/sentry-core/latest/sentry_core/struct.TracePropagationContext.html) as the preferred type for Sentry trace propagation metadata. The existing [`SentryTrace`](https://docs.rs/sentry-core/latest/sentry_core/struct.SentryTrace.html) type remains available for backwards compatibility ([#1212](https://github.com/getsentry/sentry-rust/pull/1212)). - Added [`Dsn::org_id`](https://docs.rs/sentry-types/latest/sentry_types/struct.Dsn.html#method.org_id), which parses the Sentry SaaS organization ID from DSN hosts such as `o123.ingest.sentry.io` ([#1202](https://github.com/getsentry/sentry-rust/pull/1202)). - Added [`ClientOptions::org_id`](https://docs.rs/sentry-core/latest/sentry_core/struct.ClientOptions.html#method.org_id) and [`ClientOptions::strict_trace_continuation`](https://docs.rs/sentry-core/latest/sentry_core/struct.ClientOptions.html#method.strict_trace_continuation) ([#1203](https://github.com/getsentry/sentry-rust/pull/1203)). These options control how traces are continued and can help prevent traces from third-party services, which happen to be instrumented with Sentry, from being continued. diff --git a/sentry-core/src/performance/headers.rs b/sentry-core/src/performance/headers.rs index 84666556b..ffcc42158 100644 --- a/sentry-core/src/performance/headers.rs +++ b/sentry-core/src/performance/headers.rs @@ -1,46 +1,185 @@ //! Module containing utilities for interacting with Sentry tracing headers. -use crate::protocol; +use std::error::Error; +use std::fmt::{Display, Formatter, Result as FmtResult}; -/// A container for distributed tracing metadata that can be extracted from e.g. the `sentry-trace` -/// HTTP header. +use crate::protocol::{SpanId, TraceId}; + +/// A key-value header pair. +type Header<'h> = (&'h str, &'h str); + +/// The [trace propagation] context. +/// +/// Contains the information necessary for propagating Sentry traces and continuing traces from +/// incoming requests. +/// +/// The data stored in this struct can be parsed from and transmitted as `sentry-trace` headers. +/// +/// Note that the Rust SDK only partially supports trace propagation, certain features such as +/// [dynamic sampling] may be missing or incomplete. +/// +/// [trace propagation]: https://develop.sentry.dev/sdk/foundations/trace-propagation/ +/// [dynamic sampling]: https://develop.sentry.dev/sdk/foundations/trace-propagation/dynamic-sampling-context/ +#[derive(Debug, PartialEq, Clone, Default)] +pub struct TracePropagationContext { + pub(crate) trace_id: TraceId, + pub(crate) span_id: SpanId, + pub(super) sampled: Option, +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +/// Error type returned by [`TracePropagationContext::try_from_headers`]. +pub enum HeaderParseError { + /// The `sentry-trace` header was missing. + Missing, + /// There was a `sentry-trace` header, but it was invalid. + Invalid, +} + +/// A container for `sentry-trace` data. +#[deprecated = "Please use `TracePropagationContext` instead"] #[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, + trace_id: TraceId, + span_id: SpanId, + sampled: Option, } -impl SentryTrace { - /// Creates a new [`SentryTrace`] from the provided parameters - pub fn new( - trace_id: protocol::TraceId, - span_id: protocol::SpanId, - sampled: Option, - ) -> Self { - SentryTrace { +impl TracePropagationContext { + /// Creates a new [`TracePropagationContext`] from the provided parameters + pub fn new(trace_id: TraceId, span_id: SpanId) -> Self { + TracePropagationContext { + trace_id, + span_id, + sampled: None, + } + } + + /// Set the sampling decision on `self`. + pub fn with_sampled(self, sampled: bool) -> Self { + let sampled = Some(sampled); + Self { sampled, ..self } + } + + /// Computes the `sentry-trace` header for this [`TracePropagationContext`]. + pub fn sentry_trace_header(&self) -> String { + let Self { trace_id, span_id, sampled, + } = self; + + let sampled_suffix = sampled + .map(|sampled| format!("-{}", if sampled { "1" } else { "0" })) + .unwrap_or_default(); + + format!("{trace_id}-{span_id}{sampled_suffix}") + } + + /// Attempt to parse a list of Sentry headers into [`TracePropagationContext`]. + /// + /// The parsing will fail if there is no valid `sentry-trace` header. + pub fn try_from_headers<'a, I>(headers: I) -> Result + where + I: IntoIterator>, + { + let mut context_result = Err(HeaderParseError::Missing); + + for (header, value) in headers { + if header.eq_ignore_ascii_case("sentry-trace") { + // Parse the header, falling back to the previous header value if Ok (headers not + // guaranteed unique), only falling back to invalid error if there's no prev value. + context_result = TracePropagationContext::from_sentry_trace(value) + .map_or(context_result, Ok) + .map_err(|_| HeaderParseError::Invalid); + } } + + context_result + } + + /// Attempts to construct a [`TracePropagationContext`] from the given Sentry trace header. + /// + /// Returns [`None`] if the header cannot be parsed. + fn from_sentry_trace(header: &str) -> Option { + let header = header.trim(); + let mut parts = header.splitn(3, '-'); + + let trace_id = parts.next()?.parse().ok()?; + let span_id = parts.next()?.parse().ok()?; + let sampled = parts.next().and_then(|sampled| match sampled { + "1" => Some(true), + "0" => Some(false), + _ => None, + }); + + Some(Self { + trace_id, + span_id, + sampled, + }) } } -fn parse_sentry_trace(header: &str) -> Option { - let header = header.trim(); - let mut parts = header.splitn(3, '-'); +/// Extracts distributed tracing metadata from headers (or, generally, key-value pairs), +/// considering the values for `sentry-trace`. +#[deprecated = "use TracePropagationContext::try_from_headers instead"] +#[expect(deprecated, reason = "backwards-compatible function")] +pub fn parse_sentry_trace_header<'a, I>(headers: I) -> Option +where + I: IntoIterator>, +{ + let TracePropagationContext { + trace_id, + span_id, + sampled, + } = TracePropagationContext::try_from_headers(headers).ok()?; - let trace_id = parts.next()?.parse().ok()?; - let parent_span_id = parts.next()?.parse().ok()?; - let parent_sampled = parts.next().and_then(|sampled| match sampled { - "1" => Some(true), - "0" => Some(false), - _ => None, - }); + Some(SentryTrace { + trace_id, + span_id, + sampled, + }) +} - Some(SentryTrace::new(trace_id, parent_span_id, parent_sampled)) +impl Display for HeaderParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let msg = match self { + HeaderParseError::Missing => "missing", + HeaderParseError::Invalid => "invalid", + }; + + write!(f, "{msg} sentry-trace header") + } } +impl Error for HeaderParseError {} + +#[expect(deprecated, reason = "backwards-compatible impl")] +impl SentryTrace { + /// Creates a new [`SentryTrace`] from the provided parameters + pub fn new(trace_id: TraceId, span_id: SpanId, sampled: Option) -> Self { + Self { + trace_id, + span_id, + sampled, + } + } +} + +#[expect(deprecated, reason = "backwards-compatible impl")] +impl From for TracePropagationContext { + fn from(trace: SentryTrace) -> Self { + Self { + trace_id: trace.trace_id, + span_id: trace.span_id, + sampled: trace.sampled, + } + } +} + +#[expect(deprecated, reason = "backwards-compatible impl")] impl std::fmt::Display for SentryTrace { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}-{}", self.trace_id, self.span_id)?; @@ -51,38 +190,35 @@ impl std::fmt::Display for SentryTrace { } } -/// Extracts distributed tracing metadata from headers (or, generally, key-value pairs), -/// considering the values for `sentry-trace`. -pub fn parse<'a, I: IntoIterator>(headers: I) -> Option { - let mut trace = None; - for (k, v) in headers.into_iter() { - if k.eq_ignore_ascii_case("sentry-trace") { - trace = parse_sentry_trace(v); - break; - } - } - trace -} - #[cfg(test)] mod tests { - use std::str::FromStr as _; - use super::*; #[test] fn parses_sentry_trace() { - let trace_id = protocol::TraceId::from_str("09e04486820349518ac7b5d2adbf6ba5").unwrap(); - let parent_trace_id = protocol::SpanId::from_str("9cf635fa5b870b3a").unwrap(); + let trace_id = "09e04486820349518ac7b5d2adbf6ba5".parse().unwrap(); + let parent_trace_id = "9cf635fa5b870b3a".parse().unwrap(); - let trace = parse_sentry_trace("09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0"); + let trace = TracePropagationContext::try_from_headers([( + "sentry-trace", + "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0", + )]) + .expect("should parse successfully"); assert_eq!( trace, - Some(SentryTrace::new(trace_id, parent_trace_id, Some(false))) + TracePropagationContext { + trace_id, + span_id: parent_trace_id, + sampled: Some(false), + } ); - let trace = SentryTrace::new(Default::default(), Default::default(), None); - let parsed = parse_sentry_trace(&trace.to_string()); - assert_eq!(parsed, Some(trace)); + let trace = TracePropagationContext::new(Default::default(), Default::default()); + let parsed = TracePropagationContext::try_from_headers([( + "sentry-trace", + trace.sentry_trace_header().as_str(), + )]) + .expect("should parse successfully"); + assert_eq!(parsed, trace); } } diff --git a/sentry-core/src/performance/mod.rs b/sentry-core/src/performance/mod.rs index a790b32f4..ffa3e55a1 100644 --- a/sentry-core/src/performance/mod.rs +++ b/sentry-core/src/performance/mod.rs @@ -15,7 +15,9 @@ use crate::{protocol, Hub}; #[cfg(feature = "client")] use crate::Client; -pub use self::headers::{parse as parse_headers, SentryTrace}; +#[expect(deprecated, reason = "backwards-compatibility re-export")] +pub use self::headers::{parse_sentry_trace_header as parse_headers, SentryTrace}; +pub use self::headers::{HeaderParseError, TracePropagationContext}; mod headers; @@ -186,9 +188,9 @@ impl TransactionContext { op: &str, headers: I, ) -> Self { - parse_headers(headers) - .map(|sentry_trace| Self::continue_from_sentry_trace(name, op, &sentry_trace, None)) - .unwrap_or_else(|| Self { + TracePropagationContext::try_from_headers(headers) + .map(|context| Self::continue_from_trace_propagation_context(name, op, &context, None)) + .unwrap_or_else(|_| Self { name: name.into(), op: op.into(), trace_id: Default::default(), @@ -201,18 +203,38 @@ impl TransactionContext { /// Creates a new Transaction Context based on the provided distributed tracing data, /// optionally creating the `TransactionContext` with the provided `span_id`. + #[deprecated = "use `TransactionContext::continue_from_trace_propagation_context` instead"] + #[expect(deprecated, reason = "backwards-compatible method")] pub fn continue_from_sentry_trace( name: &str, op: &str, sentry_trace: &SentryTrace, span_id: Option, ) -> Self { + let context = (*sentry_trace).into(); + Self::continue_from_trace_propagation_context(name, op, &context, span_id) + } + + /// Creates a new Transaction Context based on the provided trace propagation context, + /// optionally creating the `TransactionContext` with the provided `span_id`. + pub fn continue_from_trace_propagation_context( + name: &str, + op: &str, + context: &TracePropagationContext, + span_id: Option, + ) -> Self { + let &TracePropagationContext { + trace_id, + span_id: context_span_id, + sampled, + } = context; + Self { name: name.into(), op: op.into(), - trace_id: sentry_trace.trace_id, - parent_span_id: Some(sentry_trace.span_id), - sampled: sentry_trace.sampled, + trace_id, + parent_span_id: Some(context_span_id), + sampled, span_id: span_id.unwrap_or_default(), custom: None, } @@ -834,13 +856,10 @@ impl Transaction { /// trace's distributed tracing headers. pub fn iter_headers(&self) -> TraceHeadersIter { let inner = self.inner.lock().unwrap(); - let trace = SentryTrace::new( - inner.context.trace_id, - inner.context.span_id, - Some(inner.sampled), - ); + let trace = TracePropagationContext::new(inner.context.trace_id, inner.context.span_id) + .with_sampled(inner.sampled); TraceHeadersIter { - sentry_trace: Some(trace.to_string()), + sentry_trace: Some(trace.sentry_trace_header()), } } @@ -1121,9 +1140,10 @@ impl Span { /// trace's distributed tracing headers. pub fn iter_headers(&self) -> TraceHeadersIter { let span = self.span.lock().unwrap(); - let trace = SentryTrace::new(span.trace_id, span.span_id, Some(self.sampled)); + let trace = + TracePropagationContext::new(span.trace_id, span.span_id).with_sampled(self.sampled); TraceHeadersIter { - sentry_trace: Some(trace.to_string()), + sentry_trace: Some(trace.sentry_trace_header()), } } @@ -1272,7 +1292,8 @@ mod tests { let span = trx.start_child("noop", "noop"); let header = span.iter_headers().next().unwrap().1; - let parsed = parse_headers([("sentry-trace", header.as_str())]).unwrap(); + let parsed = + TracePropagationContext::try_from_headers([("sentry-trace", header.as_str())]).unwrap(); assert_eq!( &parsed.trace_id.to_string(), diff --git a/sentry-core/src/scope/real.rs b/sentry-core/src/scope/real.rs index 76482e1b9..2eac5914d 100644 --- a/sentry-core/src/scope/real.rs +++ b/sentry-core/src/scope/real.rs @@ -20,7 +20,7 @@ use crate::protocol::{ }; #[cfg(feature = "release-health")] use crate::session::Session; -use crate::{Client, SentryTrace, TraceHeader, TraceHeadersIter}; +use crate::{Client, TraceHeader, TraceHeadersIter, TracePropagationContext}; #[derive(Debug)] pub struct Stack { @@ -64,7 +64,7 @@ pub struct Scope { pub(crate) session: Arc>>, pub(crate) span: Arc>, pub(crate) attachments: Arc>, - pub(crate) propagation_context: SentryTrace, + pub(crate) propagation_context: TracePropagationContext, } impl fmt::Debug for Scope { @@ -482,12 +482,11 @@ impl Scope { if let Some(span) = self.get_span() { span.iter_headers() } else { - let data = SentryTrace::new( + let data = TracePropagationContext::new( self.propagation_context.trace_id, self.propagation_context.span_id, - None, ); - TraceHeadersIter::new(data.to_string()) + TraceHeadersIter::new(data.sentry_trace_header()) } } } diff --git a/sentry-opentelemetry/src/processor.rs b/sentry-opentelemetry/src/processor.rs index 77b143875..d2a9e62ce 100644 --- a/sentry-opentelemetry/src/processor.rs +++ b/sentry-opentelemetry/src/processor.rs @@ -19,8 +19,7 @@ use opentelemetry_sdk::error::OTelSdkResult; use opentelemetry_sdk::trace::{Span, SpanData, SpanProcessor}; use opentelemetry_sdk::Resource; -use sentry_core::SentryTrace; -use sentry_core::{TransactionContext, TransactionOrSpan}; +use sentry_core::{TracePropagationContext, TransactionContext, TransactionOrSpan}; use crate::converters::{ convert_span_id, convert_span_kind, convert_span_status, convert_trace_id, convert_value, @@ -128,12 +127,12 @@ impl SpanProcessor for SentrySpanProcessor { )) } else { let sentry_ctx = { - if let Some(sentry_trace) = ctx.get::() { + if let Some(trace_context) = ctx.get::() { // continue remote trace - TransactionContext::continue_from_sentry_trace( + TransactionContext::continue_from_trace_propagation_context( span_description, span_op, - sentry_trace, + trace_context, Some(convert_span_id(&span_id)), ) } else { diff --git a/sentry-opentelemetry/src/propagator.rs b/sentry-opentelemetry/src/propagator.rs index 396f95f7b..116e8c7c1 100644 --- a/sentry-opentelemetry/src/propagator.rs +++ b/sentry-opentelemetry/src/propagator.rs @@ -19,8 +19,7 @@ use opentelemetry::{ trace::TraceContextExt, Context, SpanId, TraceId, }; -use sentry_core::parse_headers; -use sentry_core::SentryTrace; +use sentry_core::TracePropagationContext; use crate::converters::{convert_span_id, convert_trace_id}; @@ -57,12 +56,10 @@ impl TextMapPropagator for SentryPropagator { if trace_id == TraceId::INVALID || span_id == SpanId::INVALID { return; } - let sentry_trace = SentryTrace::new( - convert_trace_id(&trace_id), - convert_span_id(&span_id), - Some(sampled), - ); - injector.set(SENTRY_TRACE_KEY, sentry_trace.to_string()); + let trace_context = + TracePropagationContext::new(convert_trace_id(&trace_id), convert_span_id(&span_id)) + .with_sampled(sampled); + injector.set(SENTRY_TRACE_KEY, trace_context.sentry_trace_header()); } fn extract_with_context(&self, ctx: &Context, extractor: &dyn Extractor) -> Context { @@ -70,8 +67,8 @@ impl TextMapPropagator for SentryPropagator { let pairs = keys .iter() .filter_map(|&key| extractor.get(key).map(|value| (key, value))); - if let Some(sentry_trace) = parse_headers(pairs) { - return ctx.with_value(sentry_trace); + if let Ok(trace_context) = TracePropagationContext::try_from_headers(pairs) { + return ctx.with_value(trace_context); } ctx.clone() }