Skip to content

Commit bfb61de

Browse files
authored
opentelemetry-contrib api enhancements with new_span benchmark (#1232)
1 parent c36db50 commit bfb61de

File tree

10 files changed

+536
-59
lines changed

10 files changed

+536
-59
lines changed

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
// these are words that are always correct and can be thought of as our
2626
// workspace dictionary.
2727
"words": [
28+
"hasher",
2829
"opentelemetry",
2930
"OTLP",
3031
"quantile",

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/.vscode/
12
/target/
23
*/target/
34
**/*.rs.bk

opentelemetry-contrib/Cargo.toml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ all-features = true
1919
rustdoc-args = ["--cfg", "docsrs"]
2020

2121
[features]
22+
api = []
2223
default = []
2324
base64_format = ["base64", "binary_propagator"]
2425
binary_propagator = []
@@ -31,17 +32,24 @@ rt-async-std = ["async-std", "opentelemetry_sdk/rt-async-std"]
3132
async-std = { version = "1.10", optional = true }
3233
async-trait = { version = "0.1", optional = true }
3334
base64 = { version = "0.13", optional = true }
35+
futures-core = { version = "0.3", optional = true }
36+
futures-util = { version = "0.3", optional = true, default-features = false }
3437
once_cell = "1.17.1"
3538
opentelemetry = { version = "0.21", path = "../opentelemetry" }
36-
opentelemetry_sdk = { version = "0.20", path = "../opentelemetry-sdk" }
37-
opentelemetry-semantic-conventions = { version = "0.12", path = "../opentelemetry-semantic-conventions", optional = true }
39+
opentelemetry_sdk = { version = "0.20", optional = true, path = "../opentelemetry-sdk" }
40+
opentelemetry-semantic-conventions = { version = "0.12", optional = true, path = "../opentelemetry-semantic-conventions" }
3841
serde_json = { version = "1", optional = true }
3942
tokio = { version = "1.0", features = ["fs", "io-util"], optional = true }
4043

41-
# futures
42-
futures-core = { version = "0.3", optional = true }
43-
futures-util = { version = "0.3", optional = true, default-features = false }
44-
4544
[dev-dependencies]
4645
base64 = "0.13"
46+
criterion = { version = "0.5", features = ["html_reports"] }
47+
futures-util = { version = "0.3", default-features = false, features = ["std"] }
4748
opentelemetry_sdk = { path = "../opentelemetry-sdk", features = ["trace", "testing"] }
49+
[target.'cfg(not(target_os = "windows"))'.dev-dependencies]
50+
pprof = { version = "0.12", features = ["flamegraph", "criterion"] }
51+
52+
[[bench]]
53+
name = "new_span"
54+
harness = false
55+
required-features = ["api"]
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
2+
use futures_util::future::BoxFuture;
3+
use opentelemetry::{
4+
global::BoxedTracer,
5+
trace::{
6+
mark_span_as_active, noop::NoopTracer, SpanBuilder, SpanContext, SpanId,
7+
TraceContextExt as _, TraceFlags, TraceId, TraceState, Tracer as _, TracerProvider as _,
8+
},
9+
Context, ContextGuard,
10+
};
11+
use opentelemetry_contrib::trace::{
12+
new_span_if_parent_sampled, new_span_if_recording, TracerSource,
13+
};
14+
use opentelemetry_sdk::{
15+
export::trace::{ExportResult, SpanData, SpanExporter},
16+
trace::{config, Sampler, TracerProvider},
17+
};
18+
#[cfg(not(target_os = "windows"))]
19+
use pprof::criterion::{Output, PProfProfiler};
20+
use std::fmt::Display;
21+
22+
fn criterion_benchmark(c: &mut Criterion) {
23+
let mut group = c.benchmark_group("new_span");
24+
group.throughput(Throughput::Elements(1));
25+
for env in [
26+
Environment::InContext,
27+
Environment::NoContext,
28+
Environment::NoSdk,
29+
] {
30+
let (_provider, tracer, _guard) = env.setup();
31+
32+
for api in [Api::Alt, Api::Spec] {
33+
let param = format!("{env}/{api}");
34+
group.bench_function(
35+
BenchmarkId::new("if_parent_sampled", param.clone()),
36+
// m2max, in-cx/alt: 530ns
37+
// m2max, no-cx/alt: 5.9ns
38+
// m2max, no-sdk/alt: 5.9ns
39+
// m2max, in-cx/spec: 505ns
40+
// m2max, no-cx/spec: 255ns
41+
// m2max, no-sdk/spec: 170ns
42+
|b| match api {
43+
Api::Alt => b.iter(|| {
44+
new_span_if_parent_sampled(
45+
|| SpanBuilder::from_name("new_span"),
46+
TracerSource::borrowed(&tracer),
47+
)
48+
.map(|cx| cx.attach())
49+
}),
50+
Api::Spec => b.iter(|| mark_span_as_active(tracer.start("new_span"))),
51+
},
52+
);
53+
group.bench_function(
54+
BenchmarkId::new("if_recording", param.clone()),
55+
// m2max, in-cx/alt: 8ns
56+
// m2max, no-cx/alt: 5.9ns
57+
// m2max, no-sdk/alt: 5.9ns
58+
// m2max, in-cx/spec: 31ns
59+
// m2max, no-cx/spec: 5.8ns
60+
// m2max, no-sdk/spec: 5.7ns
61+
|b| match api {
62+
Api::Alt => b.iter(|| {
63+
new_span_if_recording(
64+
|| SpanBuilder::from_name("new_span"),
65+
TracerSource::borrowed(&tracer),
66+
)
67+
.map(|cx| cx.attach())
68+
}),
69+
Api::Spec => b.iter(|| {
70+
Context::current()
71+
.span()
72+
.is_recording()
73+
.then(|| mark_span_as_active(tracer.start("new_span")))
74+
}),
75+
},
76+
);
77+
}
78+
}
79+
}
80+
81+
#[derive(Copy, Clone)]
82+
enum Api {
83+
/// An alternative way which may be faster than what the spec recommends.
84+
Alt,
85+
/// The recommended way as proposed by the current opentelemetry specification.
86+
Spec,
87+
}
88+
89+
impl Api {
90+
const fn as_str(self) -> &'static str {
91+
match self {
92+
Api::Alt => "alt",
93+
Api::Spec => "spec",
94+
}
95+
}
96+
}
97+
98+
impl Display for Api {
99+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100+
write!(f, "{}", self.as_str())
101+
}
102+
}
103+
104+
#[derive(Copy, Clone)]
105+
enum Environment {
106+
/// There is an active span being sampled in the current context.
107+
InContext,
108+
/// There is no span in context (or there is not context).
109+
NoContext,
110+
/// An SDK has not been configured, so instrumentation should be noop.
111+
NoSdk,
112+
}
113+
114+
impl Environment {
115+
const fn as_str(self) -> &'static str {
116+
match self {
117+
Environment::InContext => "in-cx",
118+
Environment::NoContext => "no-cx",
119+
Environment::NoSdk => "no-sdk",
120+
}
121+
}
122+
123+
fn setup(&self) -> (Option<TracerProvider>, BoxedTracer, Option<ContextGuard>) {
124+
match self {
125+
Environment::InContext => {
126+
let guard = Context::current()
127+
.with_remote_span_context(SpanContext::new(
128+
TraceId::from(0x09251969),
129+
SpanId::from(0x08171969),
130+
TraceFlags::SAMPLED,
131+
true,
132+
TraceState::default(),
133+
))
134+
.attach();
135+
let (provider, tracer) = parent_sampled_tracer(Sampler::AlwaysOff);
136+
(Some(provider), tracer, Some(guard))
137+
}
138+
Environment::NoContext => {
139+
let (provider, tracer) = parent_sampled_tracer(Sampler::AlwaysOff);
140+
(Some(provider), tracer, None)
141+
}
142+
Environment::NoSdk => (None, BoxedTracer::new(Box::new(NoopTracer::new())), None),
143+
}
144+
}
145+
}
146+
147+
impl Display for Environment {
148+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149+
write!(f, "{}", self.as_str())
150+
}
151+
}
152+
153+
fn parent_sampled_tracer(inner_sampler: Sampler) -> (TracerProvider, BoxedTracer) {
154+
let provider = TracerProvider::builder()
155+
.with_config(config().with_sampler(Sampler::ParentBased(Box::new(inner_sampler))))
156+
.with_simple_exporter(NoopExporter)
157+
.build();
158+
let tracer = provider.tracer(module_path!());
159+
(provider, BoxedTracer::new(Box::new(tracer)))
160+
}
161+
162+
#[derive(Debug)]
163+
struct NoopExporter;
164+
165+
impl SpanExporter for NoopExporter {
166+
fn export(&mut self, _spans: Vec<SpanData>) -> BoxFuture<'static, ExportResult> {
167+
Box::pin(futures_util::future::ready(Ok(())))
168+
}
169+
}
170+
171+
#[cfg(not(target_os = "windows"))]
172+
criterion_group! {
173+
name = benches;
174+
config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
175+
targets = criterion_benchmark
176+
}
177+
#[cfg(target_os = "windows")]
178+
criterion_group! {
179+
name = benches;
180+
config = Criterion::default();
181+
targets = criterion_benchmark
182+
}
183+
criterion_main!(benches);
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
use super::TracerSource;
2+
use opentelemetry::{
3+
trace::{SpanBuilder, TraceContextExt as _, Tracer as _},
4+
Context,
5+
};
6+
use std::{
7+
fmt::{Debug, Formatter},
8+
ops::{Deref, DerefMut},
9+
};
10+
11+
/// Lazily creates a new span only if the current context has an active span,
12+
/// which will used as the new span's parent.
13+
///
14+
/// This is useful for instrumenting library crates whose activities would be
15+
/// undesirable to see as root spans, by themselves, outside of any application
16+
/// context.
17+
///
18+
/// # Examples
19+
///
20+
/// ```
21+
/// use opentelemetry::trace::{SpanBuilder};
22+
/// use opentelemetry_contrib::trace::{new_span_if_parent_sampled, TracerSource};
23+
///
24+
/// fn my_lib_fn() {
25+
/// let _guard = new_span_if_parent_sampled(
26+
/// || SpanBuilder::from_name("my span"),
27+
/// TracerSource::lazy(&|| opentelemetry::global::tracer(module_path!())),
28+
/// )
29+
/// .map(|cx| cx.attach());
30+
/// }
31+
/// ```
32+
pub fn new_span_if_parent_sampled(
33+
builder_fn: impl Fn() -> SpanBuilder,
34+
tracer: TracerSource<'_>,
35+
) -> Option<Context> {
36+
Context::map_current(|current| {
37+
current.span().span_context().is_sampled().then(|| {
38+
let builder = builder_fn();
39+
let span = tracer.get().build_with_context(builder, current);
40+
current.with_span(span)
41+
})
42+
})
43+
}
44+
45+
/// Lazily creates a new span only if the current context has a recording span,
46+
/// which will used as the new span's parent.
47+
///
48+
/// This is useful for instrumenting library crates whose activities would be
49+
/// undesirable to see as root spans, by themselves, outside of any application
50+
/// context.
51+
///
52+
/// # Examples
53+
///
54+
/// ```
55+
/// use opentelemetry::trace::{SpanBuilder};
56+
/// use opentelemetry_contrib::trace::{new_span_if_recording, TracerSource};
57+
///
58+
/// fn my_lib_fn() {
59+
/// let _guard = new_span_if_recording(
60+
/// || SpanBuilder::from_name("my span"),
61+
/// TracerSource::lazy(&|| opentelemetry::global::tracer(module_path!())),
62+
/// )
63+
/// .map(|cx| cx.attach());
64+
/// }
65+
/// ```
66+
pub fn new_span_if_recording(
67+
builder_fn: impl Fn() -> SpanBuilder,
68+
tracer: TracerSource<'_>,
69+
) -> Option<Context> {
70+
Context::map_current(|current| {
71+
current.span().is_recording().then(|| {
72+
let builder = builder_fn();
73+
let span = tracer.get().build_with_context(builder, current);
74+
current.with_span(span)
75+
})
76+
})
77+
}
78+
79+
/// Carries anything with an optional `opentelemetry::Context`.
80+
///
81+
/// A `Contextualized<T>` is a smart pointer which owns and instance of `T` and
82+
/// dereferences to it automatically. The instance of `T` and its associated
83+
/// optional `Context` can be reacquired using the `Into` trait for the associated
84+
/// tuple type.
85+
///
86+
/// This type is mostly useful when sending `T`'s through channels with logical
87+
/// context propagation.
88+
///
89+
/// # Examples
90+
///
91+
/// ```
92+
/// use opentelemetry::trace::{SpanBuilder, TraceContextExt as _};
93+
/// use opentelemetry_contrib::trace::{new_span_if_parent_sampled, Contextualized, TracerSource};
94+
95+
/// enum Message{Command};
96+
/// let (tx, rx) = std::sync::mpsc::channel();
97+
///
98+
/// let cx = new_span_if_parent_sampled(
99+
/// || SpanBuilder::from_name("my command"),
100+
/// TracerSource::lazy(&|| opentelemetry::global::tracer(module_path!())),
101+
/// );
102+
/// tx.send(Contextualized::new(Message::Command, cx));
103+
///
104+
/// let msg = rx.recv().unwrap();
105+
/// let (msg, cx) = msg.into_inner();
106+
/// let _guard = cx.filter(|cx| cx.has_active_span()).map(|cx| {
107+
/// cx.span().add_event("command received", vec![]);
108+
/// cx.attach()
109+
/// });
110+
/// ```
111+
pub struct Contextualized<T>(T, Option<Context>);
112+
113+
impl<T> Contextualized<T> {
114+
/// Creates a new instance using the specified value and optional context.
115+
pub fn new(value: T, cx: Option<Context>) -> Self {
116+
Self(value, cx)
117+
}
118+
119+
/// Creates a new instance using the specified value and current context if
120+
/// it has an active span.
121+
pub fn pass_thru(value: T) -> Self {
122+
Self::new(
123+
value,
124+
Context::map_current(|current| current.has_active_span().then(|| current.clone())),
125+
)
126+
}
127+
128+
/// Convert self into its constituent parts, returning a tuple.
129+
pub fn into_inner(self) -> (T, Option<Context>) {
130+
(self.0, self.1)
131+
}
132+
}
133+
134+
impl<T: Clone> Clone for Contextualized<T> {
135+
fn clone(&self) -> Self {
136+
Self(self.0.clone(), self.1.clone())
137+
}
138+
}
139+
140+
impl<T: Debug> Debug for Contextualized<T> {
141+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
142+
f.debug_tuple("Contextualized")
143+
.field(&self.0)
144+
.field(&self.1)
145+
.finish()
146+
}
147+
}
148+
149+
impl<T> Deref for Contextualized<T> {
150+
type Target = T;
151+
152+
fn deref(&self) -> &Self::Target {
153+
&self.0
154+
}
155+
}
156+
157+
impl<T> DerefMut for Contextualized<T> {
158+
fn deref_mut(&mut self) -> &mut Self::Target {
159+
&mut self.0
160+
}
161+
}

0 commit comments

Comments
 (0)