Skip to content

Add option to enable propagation of parent span tags #746

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions sentry-core/src/performance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,22 @@ pub struct Transaction {
pub(crate) inner: TransactionArc,
}

/// Iterable for a transaction's [tags attributes](protocol::Transaction::tags).
pub struct TransactionTags<'a>(MutexGuard<'a, TransactionInner>);
impl<'a> TransactionTags<'a> {
#[inline]
/// Returns iterator over available tags, if transaction is sampled
pub fn iter(&'a self) -> Option<impl Iterator<Item = (&'a String, &'a String)> + 'a> {
self.0.transaction.as_ref().map(|tx| tx.tags.iter())
}

#[inline]
/// Converts self into data pointer
pub fn into_data(self) -> TransactionData<'a> {
TransactionData(self.0)
}
}

/// Iterable for a transaction's [data attributes](protocol::TraceContext::data).
pub struct TransactionData<'a>(MutexGuard<'a, TransactionInner>);

Expand Down Expand Up @@ -746,6 +762,12 @@ impl Transaction {
TransactionData(self.inner.lock().unwrap())
}

/// Returns an iterating accessor to the transaction's
/// [tags attributes](protocol::Transaction::tags).
pub fn tags(&self) -> TransactionTags {
TransactionTags(self.inner.lock().unwrap())
}

/// Get the TransactionContext of the Transaction.
///
/// Note that this clones the underlying value.
Expand Down Expand Up @@ -895,6 +917,38 @@ impl Transaction {
}
}

/// A smart pointer to a span's [`tags` field](protocol::Span::tags).
pub struct Tags<'a>(MutexGuard<'a, protocol::Span>);

impl<'a> Tags<'a> {
/// Set some tag to be sent with this Span.
pub fn set_tag(&mut self, key: String, value: String) {
self.0.tags.insert(key, value);
}

#[inline]
/// Moves pointer to [`data` field](protocol::Span::data)
pub fn into_data(self) -> Data<'a> {
Data(self.0)
}
}

impl Deref for Tags<'_> {
type Target = BTreeMap<String, String>;

#[inline]
fn deref(&self) -> &Self::Target {
&self.0.tags
}
}

impl DerefMut for Tags<'_> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0.tags
}
}

/// A smart pointer to a span's [`data` field](protocol::Span::data).
pub struct Data<'a>(MutexGuard<'a, protocol::Span>);

Expand Down Expand Up @@ -965,6 +1019,11 @@ impl Span {
Data(self.span.lock().unwrap())
}

/// Returns a smart pointer to the span's [`tags` field](protocol::Span::tags).
pub fn tags(&self) -> Tags {
Tags(self.span.lock().unwrap())
}

/// Get the TransactionContext of the Span.
///
/// Note that this clones the underlying value.
Expand Down
77 changes: 57 additions & 20 deletions sentry-tracing/src/converters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;

use super::layer::SentrySpanData;
use crate::TAGS_PREFIX;
use crate::{SpanPropagation, TAGS_PREFIX};

/// Converts a [`tracing_core::Level`] to a Sentry [`Level`]
fn convert_tracing_level(level: &tracing_core::Level) -> Level {
Expand Down Expand Up @@ -54,39 +54,55 @@ fn extract_event_data(event: &tracing_core::Event) -> (Option<String>, FieldVisi

fn extract_event_data_with_context<S>(
event: &tracing_core::Event,
ctx: Option<Context<S>>,
ctx: Context<S>,
propagation: Option<SpanPropagation>,
) -> (Option<String>, FieldVisitor)
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
let (message, mut visitor) = extract_event_data(event);

// Add the context fields of every parent span.
let current_span = ctx.as_ref().and_then(|ctx| {
// Add the context fields of every parent span, if propagation is enabled
let propagation_span = propagation.and_then(|propagation| {
event
.parent()
.and_then(|id| ctx.span(id))
.or_else(|| ctx.lookup_current())
.and_then(|id| ctx.span(id).map(|span| (propagation, span)))
.or_else(|| ctx.lookup_current().map(|span| (propagation, span)))
});
if let Some(span) = current_span {
if let Some((propagation, span)) = propagation_span {
for span in span.scope() {
let name = span.name();
let ext = span.extensions();
if let Some(span_data) = ext.get::<SentrySpanData>() {
match &span_data.sentry_span {
TransactionOrSpan::Span(span) => {
for (key, value) in span.data().iter() {
if key != "message" {
let key = format!("{}:{}", name, key);
visitor.json_values.insert(key, value.clone());
let tags = span.tags();

if propagation.is_tags_enabled() {
for (key, value) in tags.iter() {
visitor.propagate_span_tag(key, value);
}
}

if propagation.is_attrs_enabled() {
for (key, value) in tags.into_data().iter() {
visitor.propagate_span_attr(key, value, name);
}
}
}
TransactionOrSpan::Transaction(transaction) => {
for (key, value) in transaction.data().iter() {
if key != "message" {
let key = format!("{}:{}", name, key);
visitor.json_values.insert(key, value.clone());
let tags = transaction.tags();
if propagation.is_tags_enabled() {
if let Some(tags) = tags.iter() {
for (key, value) in tags {
visitor.propagate_span_tag(key, value);
}
}
}

if propagation.is_attrs_enabled() {
for (key, value) in tags.into_data().iter() {
visitor.propagate_span_attr(key, value, name);
}
}
}
Expand All @@ -106,6 +122,19 @@ pub(crate) struct FieldVisitor {
}

impl FieldVisitor {
fn propagate_span_tag(&mut self, key: &str, value: &str) {
//Propagate tags as it is, it will be extracted later on
let tag = format!("{TAGS_PREFIX}{key}");
self.json_values.entry(tag).or_insert_with(|| value.into());
}

fn propagate_span_attr(&mut self, key: &str, value: &Value, span_name: &str) {
if key != "message" {
let key = format!("{}:{}", span_name, key);
self.json_values.insert(key, value.clone());
}
}

fn record<T: Into<Value>>(&mut self, field: &Field, value: T) {
self.json_values
.insert(field.name().to_owned(), value.into());
Expand Down Expand Up @@ -144,12 +173,19 @@ impl Visit for FieldVisitor {
/// Creates a [`Breadcrumb`] from a given [`tracing_core::Event`]
pub fn breadcrumb_from_event<'context, S>(
event: &tracing_core::Event,
ctx: impl Into<Option<Context<'context, S>>>,
ctx: Context<'context, S>,
mut propagation: Option<SpanPropagation>,
) -> Breadcrumb
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
let (message, visitor) = extract_event_data_with_context(event, ctx.into());
if let Some(propagation) = propagation.as_mut() {
if propagation.is_attrs_enabled() {
//Breadcrumb has no tags, so propagate only attributes
*propagation = SpanPropagation::Attributes;
}
}
let (message, visitor) = extract_event_data_with_context(event, ctx, propagation);

let FieldVisitor {
exceptions,
Expand Down Expand Up @@ -232,10 +268,11 @@ fn contexts_from_event(
context
}

/// Creates an [`Event`] (possibly carrying an exception) from a given [`tracing_core::Event`]
/// Creates an [`Event`] from a given [`tracing_core::Event`]
pub fn event_from_event<'context, S>(
event: &tracing_core::Event,
ctx: impl Into<Option<Context<'context, S>>>,
ctx: Context<'context, S>,
propagation: Option<SpanPropagation>,
) -> Event<'static>
where
S: Subscriber + for<'a> LookupSpan<'a>,
Expand All @@ -245,7 +282,7 @@ where
// information for this. However, it may contain a serialized error which we can parse to emit
// an exception record.
#[allow(unused_mut)]
let (mut message, visitor) = extract_event_data_with_context(event, ctx.into());
let (mut message, visitor) = extract_event_data_with_context(event, ctx, propagation);
let FieldVisitor {
mut exceptions,
mut json_values,
Expand Down
35 changes: 21 additions & 14 deletions sentry-tracing/src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use tracing_subscriber::layer::{Context, Layer};
use tracing_subscriber::registry::LookupSpan;

use crate::converters::*;
use crate::TAGS_PREFIX;
use crate::{SpanPropagation, TAGS_PREFIX};

/// The action that Sentry should perform for a [`Metadata`]
#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -68,7 +68,7 @@ pub struct SentryLayer<S> {

span_filter: Box<dyn Fn(&Metadata) -> bool + Send + Sync>,

with_span_attributes: bool,
span_propagation: Option<SpanPropagation>,
}

impl<S> SentryLayer<S> {
Expand Down Expand Up @@ -121,8 +121,14 @@ impl<S> SentryLayer<S> {
/// the [traces_sample_rate][sentry_core::ClientOptions::traces_sample_rate] to `1.0`
/// while configuring your sentry client.
#[must_use]
pub fn enable_span_attributes(mut self) -> Self {
self.with_span_attributes = true;
pub fn enable_span_attributes(self) -> Self {
self.enable_span_propagation(SpanPropagation::Attributes)
}

#[must_use]
/// Configures span propagation for events' creation
pub fn enable_span_propagation(mut self, propagation: SpanPropagation) -> Self {
self.span_propagation = Some(propagation);
self
}
}
Expand All @@ -138,7 +144,7 @@ where

span_filter: Box::new(default_span_filter),

with_span_attributes: false,
span_propagation: None,
}
}
}
Expand Down Expand Up @@ -207,16 +213,17 @@ where
fn on_event(&self, event: &Event, ctx: Context<'_, S>) {
let item = match &self.event_mapper {
Some(mapper) => mapper(event, ctx),
None => {
let span_ctx = self.with_span_attributes.then_some(ctx);
match (self.event_filter)(event.metadata()) {
EventFilter::Ignore => EventMapping::Ignore,
EventFilter::Breadcrumb => {
EventMapping::Breadcrumb(breadcrumb_from_event(event, span_ctx))
}
EventFilter::Event => EventMapping::Event(event_from_event(event, span_ctx)),
None => match (self.event_filter)(event.metadata()) {
EventFilter::Ignore => EventMapping::Ignore,
EventFilter::Breadcrumb => EventMapping::Breadcrumb(breadcrumb_from_event(
event,
ctx,
self.span_propagation,
)),
EventFilter::Event => {
EventMapping::Event(event_from_event(event, ctx, self.span_propagation))
}
}
},
};

match item {
Expand Down
29 changes: 29 additions & 0 deletions sentry-tracing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,32 @@ pub use converters::*;
pub use layer::*;

const TAGS_PREFIX: &str = "tags.";

#[derive(Debug, Clone, Copy)]
/// Controls propagation of span data when creating event
///
/// Note that the root span is considered a [transaction][sentry_core::protocol::Transaction]
/// so its context will only be grabbed only if you set the transaction to be sampled.
/// The most straightforward way to do this is to set
/// the [traces_sample_rate][sentry_core::ClientOptions::traces_sample_rate] to `1.0`
/// while configuring your sentry client.
pub enum SpanPropagation {
/// Collects all attributes prefixed with span name
Attributes,
/// Accumulates tags from within attributes as event tags, without overriding existing tags
Tags,
/// Collects both tags and attributes
All,
}

impl SpanPropagation {
#[inline(always)]
pub(crate) const fn is_tags_enabled(&self) -> bool {
matches!(self, Self::Tags | Self::All)
}

#[inline(always)]
pub(crate) const fn is_attrs_enabled(&self) -> bool {
matches!(self, Self::Attributes | Self::All)
}
}
3 changes: 2 additions & 1 deletion sentry-tracing/tests/shared.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use sentry::{ClientOptions, Hub};
use sentry_core::test::TestTransport;
use sentry_tracing::SpanPropagation;

use std::sync::Arc;

Expand All @@ -17,7 +18,7 @@ pub fn init_sentry(traces_sample_rate: f32) -> Arc<TestTransport> {
Hub::current().bind_client(Some(Arc::new(options.into())));

let _ = tracing_subscriber::registry()
.with(sentry_tracing::layer().enable_span_attributes())
.with(sentry_tracing::layer().enable_span_propagation(SpanPropagation::All))
.try_init();

transport
Expand Down
7 changes: 7 additions & 0 deletions sentry-tracing/tests/smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ fn should_instrument_function_with_event() {
unexpected => panic!("Expected event, but got {:#?}", unexpected),
};

//Event must be get tag attached
let event_tag = event
.tags
.get("tag")
.expect("event should be associated with span's tag");
assert_eq!(event_tag, "key");

//Validate transaction is created
let trace = match event.contexts.get("trace").expect("to get 'trace' context") {
sentry::protocol::Context::Trace(trace) => trace,
Expand Down