Skip to content

Commit

Permalink
prevent panics with per-layer filters
Browse files Browse the repository at this point in the history
  • Loading branch information
guswynn committed Jan 9, 2024
1 parent b2013d0 commit f9eab2f
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 27 deletions.
70 changes: 43 additions & 27 deletions src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,16 +759,30 @@ where
/// [`span`]: tracing::Span
/// [`Registry`]: tracing_subscriber::Registry
fn parent_context(&self, attrs: &Attributes<'_>, ctx: &Context<'_, S>) -> OtelContext {
// If a span is specified, it _should_ exist in the underlying `Registry`.
if let Some(parent) = attrs.parent() {
let span = ctx.span(parent).expect("Span not found, this is a bug");
let mut extensions = span.extensions_mut();
extensions
.get_mut::<OtelData>()
.map(|builder| self.tracer.sampled_context(builder))
.unwrap_or_default()
// A span can have an _explicit_ parent that is NOT seen by this `Layer` (for which
// `Context::span` returns `None`. This happens if the parent span is filtered away
// from the layer by a per-layer filter. In that case, we fall-through to the `else`
// case, and consider this span a root span.
//
// This is likely rare, as most users who use explicit parents will configure their
// filters so that children and parents are both seen, but its not guaranteed. Also,
// if users configure their filter with a `reload` filter, its possible that a parent
// and child have different filters as they are created with a filter change
// in-between.
//
// In these case, we prefer to emit a smaller span tree instead of panicking.
if let Some(span) = ctx.span(parent) {
let mut extensions = span.extensions_mut();
return extensions
.get_mut::<OtelData>()
.map(|builder| self.tracer.sampled_context(builder))
.unwrap_or_default();
}
}

// Else if the span is inferred from context, look up any available current span.
} else if attrs.is_contextual() {
if attrs.is_contextual() {
ctx.lookup_current()
.and_then(|span| {
let mut extensions = span.extensions_mut();
Expand Down Expand Up @@ -948,25 +962,27 @@ where
.get_mut::<OtelData>()
.expect("Missing otel data span extensions");

let follows_span = ctx
.span(follows)
.expect("Span to follow not found, this is a bug");
let mut follows_extensions = follows_span.extensions_mut();
let follows_data = follows_extensions
.get_mut::<OtelData>()
.expect("Missing otel data span extensions");

let follows_context = self
.tracer
.sampled_context(follows_data)
.span()
.span_context()
.clone();
let follows_link = otel::Link::new(follows_context, Vec::new());
if let Some(ref mut links) = data.builder.links {
links.push(follows_link);
} else {
data.builder.links = Some(vec![follows_link]);
// The follows span may be filtered away from this layer, in which case
// we just drop the data, as opposed to panicking. This uses the same
// reasoning as `parent_context` above.
if let Some(follows_span) = ctx.span(follows) {
let mut follows_extensions = follows_span.extensions_mut();
let follows_data = follows_extensions
.get_mut::<OtelData>()
.expect("Missing otel data span extensions");

let follows_context = self
.tracer
.sampled_context(follows_data)
.span()
.span_context()
.clone();
let follows_link = otel::Link::new(follows_context, Vec::new());
if let Some(ref mut links) = data.builder.links {
links.push(follows_link);
} else {
data.builder.links = Some(vec![follows_link]);
}
}
}

Expand Down
67 changes: 67 additions & 0 deletions tests/filtered.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use futures_util::future::BoxFuture;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_sdk::{
export::trace::{ExportResult, SpanData, SpanExporter},
trace::{Tracer, TracerProvider},
};
use std::sync::{Arc, Mutex};
use tracing::level_filters::LevelFilter;
use tracing::Subscriber;
use tracing_opentelemetry::layer;
use tracing_subscriber::prelude::*;

#[derive(Clone, Default, Debug)]
struct TestExporter(Arc<Mutex<Vec<SpanData>>>);

impl SpanExporter for TestExporter {
fn export(&mut self, mut batch: Vec<SpanData>) -> BoxFuture<'static, ExportResult> {
let spans = self.0.clone();
Box::pin(async move {
if let Ok(mut inner) = spans.lock() {
inner.append(&mut batch);
}
Ok(())
})
}
}

fn test_tracer() -> (Tracer, TracerProvider, TestExporter, impl Subscriber) {
let exporter = TestExporter::default();
let provider = TracerProvider::builder()
.with_simple_exporter(exporter.clone())
.build();
let tracer = provider.tracer("test");

let subscriber = tracing_subscriber::registry()
.with(
layer()
.with_tracer(tracer.clone())
// DEBUG-level, so `trace_spans` are skipped.
.with_filter(LevelFilter::DEBUG),
)
// This is REQUIRED so that the tracing fast path doesn't filter
// out trace spans at their callsite.
.with(tracing_subscriber::fmt::layer().with_filter(LevelFilter::TRACE));

(tracer, provider, exporter, subscriber)
}

#[test]
fn trace_filtered() {
let (_tracer, provider, exporter, subscriber) = test_tracer();

tracing::subscriber::with_default(subscriber, || {
// Neither of these should panic

let root = tracing::trace_span!("root");
tracing::debug_span!(parent: &root, "child");

let root = tracing::trace_span!("root");
root.in_scope(|| tracing::debug_span!("child"));
});

drop(provider); // flush all spans
let spans = exporter.0.lock().unwrap();
// Only the child spans are reported.
assert_eq!(spans.len(), 2);
}

0 comments on commit f9eab2f

Please sign in to comment.