Skip to content

Commit

Permalink
opentelemetry-contrib api enhancements with new_span benchmark (open-…
Browse files Browse the repository at this point in the history
  • Loading branch information
shaun-cox authored Sep 13, 2023
1 parent c36db50 commit bfb61de
Show file tree
Hide file tree
Showing 10 changed files with 536 additions and 59 deletions.
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
// these are words that are always correct and can be thought of as our
// workspace dictionary.
"words": [
"hasher",
"opentelemetry",
"OTLP",
"quantile",
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/.vscode/
/target/
*/target/
**/*.rs.bk
Expand Down
20 changes: 14 additions & 6 deletions opentelemetry-contrib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[features]
api = []
default = []
base64_format = ["base64", "binary_propagator"]
binary_propagator = []
Expand All @@ -31,17 +32,24 @@ rt-async-std = ["async-std", "opentelemetry_sdk/rt-async-std"]
async-std = { version = "1.10", optional = true }
async-trait = { version = "0.1", optional = true }
base64 = { version = "0.13", optional = true }
futures-core = { version = "0.3", optional = true }
futures-util = { version = "0.3", optional = true, default-features = false }
once_cell = "1.17.1"
opentelemetry = { version = "0.21", path = "../opentelemetry" }
opentelemetry_sdk = { version = "0.20", path = "../opentelemetry-sdk" }
opentelemetry-semantic-conventions = { version = "0.12", path = "../opentelemetry-semantic-conventions", optional = true }
opentelemetry_sdk = { version = "0.20", optional = true, path = "../opentelemetry-sdk" }
opentelemetry-semantic-conventions = { version = "0.12", optional = true, path = "../opentelemetry-semantic-conventions" }
serde_json = { version = "1", optional = true }
tokio = { version = "1.0", features = ["fs", "io-util"], optional = true }

# futures
futures-core = { version = "0.3", optional = true }
futures-util = { version = "0.3", optional = true, default-features = false }

[dev-dependencies]
base64 = "0.13"
criterion = { version = "0.5", features = ["html_reports"] }
futures-util = { version = "0.3", default-features = false, features = ["std"] }
opentelemetry_sdk = { path = "../opentelemetry-sdk", features = ["trace", "testing"] }
[target.'cfg(not(target_os = "windows"))'.dev-dependencies]
pprof = { version = "0.12", features = ["flamegraph", "criterion"] }

[[bench]]
name = "new_span"
harness = false
required-features = ["api"]
183 changes: 183 additions & 0 deletions opentelemetry-contrib/benches/new_span.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use futures_util::future::BoxFuture;
use opentelemetry::{
global::BoxedTracer,
trace::{
mark_span_as_active, noop::NoopTracer, SpanBuilder, SpanContext, SpanId,
TraceContextExt as _, TraceFlags, TraceId, TraceState, Tracer as _, TracerProvider as _,
},
Context, ContextGuard,
};
use opentelemetry_contrib::trace::{
new_span_if_parent_sampled, new_span_if_recording, TracerSource,
};
use opentelemetry_sdk::{
export::trace::{ExportResult, SpanData, SpanExporter},
trace::{config, Sampler, TracerProvider},
};
#[cfg(not(target_os = "windows"))]
use pprof::criterion::{Output, PProfProfiler};
use std::fmt::Display;

fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("new_span");
group.throughput(Throughput::Elements(1));
for env in [
Environment::InContext,
Environment::NoContext,
Environment::NoSdk,
] {
let (_provider, tracer, _guard) = env.setup();

for api in [Api::Alt, Api::Spec] {
let param = format!("{env}/{api}");
group.bench_function(
BenchmarkId::new("if_parent_sampled", param.clone()),
// m2max, in-cx/alt: 530ns
// m2max, no-cx/alt: 5.9ns
// m2max, no-sdk/alt: 5.9ns
// m2max, in-cx/spec: 505ns
// m2max, no-cx/spec: 255ns
// m2max, no-sdk/spec: 170ns
|b| match api {
Api::Alt => b.iter(|| {
new_span_if_parent_sampled(
|| SpanBuilder::from_name("new_span"),
TracerSource::borrowed(&tracer),
)
.map(|cx| cx.attach())
}),
Api::Spec => b.iter(|| mark_span_as_active(tracer.start("new_span"))),
},
);
group.bench_function(
BenchmarkId::new("if_recording", param.clone()),
// m2max, in-cx/alt: 8ns
// m2max, no-cx/alt: 5.9ns
// m2max, no-sdk/alt: 5.9ns
// m2max, in-cx/spec: 31ns
// m2max, no-cx/spec: 5.8ns
// m2max, no-sdk/spec: 5.7ns
|b| match api {
Api::Alt => b.iter(|| {
new_span_if_recording(
|| SpanBuilder::from_name("new_span"),
TracerSource::borrowed(&tracer),
)
.map(|cx| cx.attach())
}),
Api::Spec => b.iter(|| {
Context::current()
.span()
.is_recording()
.then(|| mark_span_as_active(tracer.start("new_span")))
}),
},
);
}
}
}

#[derive(Copy, Clone)]
enum Api {
/// An alternative way which may be faster than what the spec recommends.
Alt,
/// The recommended way as proposed by the current opentelemetry specification.
Spec,
}

impl Api {
const fn as_str(self) -> &'static str {
match self {
Api::Alt => "alt",
Api::Spec => "spec",
}
}
}

impl Display for Api {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}

#[derive(Copy, Clone)]
enum Environment {
/// There is an active span being sampled in the current context.
InContext,
/// There is no span in context (or there is not context).
NoContext,
/// An SDK has not been configured, so instrumentation should be noop.
NoSdk,
}

impl Environment {
const fn as_str(self) -> &'static str {
match self {
Environment::InContext => "in-cx",
Environment::NoContext => "no-cx",
Environment::NoSdk => "no-sdk",
}
}

fn setup(&self) -> (Option<TracerProvider>, BoxedTracer, Option<ContextGuard>) {
match self {
Environment::InContext => {
let guard = Context::current()
.with_remote_span_context(SpanContext::new(
TraceId::from(0x09251969),
SpanId::from(0x08171969),
TraceFlags::SAMPLED,
true,
TraceState::default(),
))
.attach();
let (provider, tracer) = parent_sampled_tracer(Sampler::AlwaysOff);
(Some(provider), tracer, Some(guard))
}
Environment::NoContext => {
let (provider, tracer) = parent_sampled_tracer(Sampler::AlwaysOff);
(Some(provider), tracer, None)
}
Environment::NoSdk => (None, BoxedTracer::new(Box::new(NoopTracer::new())), None),
}
}
}

impl Display for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}

fn parent_sampled_tracer(inner_sampler: Sampler) -> (TracerProvider, BoxedTracer) {
let provider = TracerProvider::builder()
.with_config(config().with_sampler(Sampler::ParentBased(Box::new(inner_sampler))))
.with_simple_exporter(NoopExporter)
.build();
let tracer = provider.tracer(module_path!());
(provider, BoxedTracer::new(Box::new(tracer)))
}

#[derive(Debug)]
struct NoopExporter;

impl SpanExporter for NoopExporter {
fn export(&mut self, _spans: Vec<SpanData>) -> BoxFuture<'static, ExportResult> {
Box::pin(futures_util::future::ready(Ok(())))
}
}

#[cfg(not(target_os = "windows"))]
criterion_group! {
name = benches;
config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
targets = criterion_benchmark
}
#[cfg(target_os = "windows")]
criterion_group! {
name = benches;
config = Criterion::default();
targets = criterion_benchmark
}
criterion_main!(benches);
161 changes: 161 additions & 0 deletions opentelemetry-contrib/src/trace/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use super::TracerSource;
use opentelemetry::{
trace::{SpanBuilder, TraceContextExt as _, Tracer as _},
Context,
};
use std::{
fmt::{Debug, Formatter},
ops::{Deref, DerefMut},
};

/// Lazily creates a new span only if the current context has an active span,
/// which will used as the new span's parent.
///
/// This is useful for instrumenting library crates whose activities would be
/// undesirable to see as root spans, by themselves, outside of any application
/// context.
///
/// # Examples
///
/// ```
/// use opentelemetry::trace::{SpanBuilder};
/// use opentelemetry_contrib::trace::{new_span_if_parent_sampled, TracerSource};
///
/// fn my_lib_fn() {
/// let _guard = new_span_if_parent_sampled(
/// || SpanBuilder::from_name("my span"),
/// TracerSource::lazy(&|| opentelemetry::global::tracer(module_path!())),
/// )
/// .map(|cx| cx.attach());
/// }
/// ```
pub fn new_span_if_parent_sampled(
builder_fn: impl Fn() -> SpanBuilder,
tracer: TracerSource<'_>,
) -> Option<Context> {
Context::map_current(|current| {
current.span().span_context().is_sampled().then(|| {
let builder = builder_fn();
let span = tracer.get().build_with_context(builder, current);
current.with_span(span)
})
})
}

/// Lazily creates a new span only if the current context has a recording span,
/// which will used as the new span's parent.
///
/// This is useful for instrumenting library crates whose activities would be
/// undesirable to see as root spans, by themselves, outside of any application
/// context.
///
/// # Examples
///
/// ```
/// use opentelemetry::trace::{SpanBuilder};
/// use opentelemetry_contrib::trace::{new_span_if_recording, TracerSource};
///
/// fn my_lib_fn() {
/// let _guard = new_span_if_recording(
/// || SpanBuilder::from_name("my span"),
/// TracerSource::lazy(&|| opentelemetry::global::tracer(module_path!())),
/// )
/// .map(|cx| cx.attach());
/// }
/// ```
pub fn new_span_if_recording(
builder_fn: impl Fn() -> SpanBuilder,
tracer: TracerSource<'_>,
) -> Option<Context> {
Context::map_current(|current| {
current.span().is_recording().then(|| {
let builder = builder_fn();
let span = tracer.get().build_with_context(builder, current);
current.with_span(span)
})
})
}

/// Carries anything with an optional `opentelemetry::Context`.
///
/// A `Contextualized<T>` is a smart pointer which owns and instance of `T` and
/// dereferences to it automatically. The instance of `T` and its associated
/// optional `Context` can be reacquired using the `Into` trait for the associated
/// tuple type.
///
/// This type is mostly useful when sending `T`'s through channels with logical
/// context propagation.
///
/// # Examples
///
/// ```
/// use opentelemetry::trace::{SpanBuilder, TraceContextExt as _};
/// use opentelemetry_contrib::trace::{new_span_if_parent_sampled, Contextualized, TracerSource};
/// enum Message{Command};
/// let (tx, rx) = std::sync::mpsc::channel();
///
/// let cx = new_span_if_parent_sampled(
/// || SpanBuilder::from_name("my command"),
/// TracerSource::lazy(&|| opentelemetry::global::tracer(module_path!())),
/// );
/// tx.send(Contextualized::new(Message::Command, cx));
///
/// let msg = rx.recv().unwrap();
/// let (msg, cx) = msg.into_inner();
/// let _guard = cx.filter(|cx| cx.has_active_span()).map(|cx| {
/// cx.span().add_event("command received", vec![]);
/// cx.attach()
/// });
/// ```
pub struct Contextualized<T>(T, Option<Context>);

impl<T> Contextualized<T> {
/// Creates a new instance using the specified value and optional context.
pub fn new(value: T, cx: Option<Context>) -> Self {
Self(value, cx)
}

/// Creates a new instance using the specified value and current context if
/// it has an active span.
pub fn pass_thru(value: T) -> Self {
Self::new(
value,
Context::map_current(|current| current.has_active_span().then(|| current.clone())),
)
}

/// Convert self into its constituent parts, returning a tuple.
pub fn into_inner(self) -> (T, Option<Context>) {
(self.0, self.1)
}
}

impl<T: Clone> Clone for Contextualized<T> {
fn clone(&self) -> Self {
Self(self.0.clone(), self.1.clone())
}
}

impl<T: Debug> Debug for Contextualized<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Contextualized")
.field(&self.0)
.field(&self.1)
.finish()
}
}

impl<T> Deref for Contextualized<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T> DerefMut for Contextualized<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
Loading

0 comments on commit bfb61de

Please sign in to comment.