From bfb61de163c35a1dad79e980704cb924d4bdfcb5 Mon Sep 17 00:00:00 2001 From: Shaun Cox Date: Tue, 12 Sep 2023 20:49:10 -0500 Subject: [PATCH] opentelemetry-contrib api enhancements with new_span benchmark (#1232) --- .cspell.json | 1 + .gitignore | 1 + opentelemetry-contrib/Cargo.toml | 20 +- opentelemetry-contrib/benches/new_span.rs | 183 ++++++++++++++++++ opentelemetry-contrib/src/trace/context.rs | 161 +++++++++++++++ opentelemetry-contrib/src/trace/mod.rs | 10 + .../src/trace/tracer_source.rs | 58 ++++++ opentelemetry-sdk/benches/context.rs | 157 ++++++++++----- opentelemetry-sdk/src/trace/tracer.rs | 2 +- opentelemetry/src/context.rs | 2 +- 10 files changed, 536 insertions(+), 59 deletions(-) create mode 100644 opentelemetry-contrib/benches/new_span.rs create mode 100644 opentelemetry-contrib/src/trace/context.rs create mode 100644 opentelemetry-contrib/src/trace/tracer_source.rs diff --git a/.cspell.json b/.cspell.json index 666eeb02e7..c5d02ef9dd 100644 --- a/.cspell.json +++ b/.cspell.json @@ -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", diff --git a/.gitignore b/.gitignore index 4bf6b0939f..e64d25e133 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.vscode/ /target/ */target/ **/*.rs.bk diff --git a/opentelemetry-contrib/Cargo.toml b/opentelemetry-contrib/Cargo.toml index 65b2e2f334..02882c1934 100644 --- a/opentelemetry-contrib/Cargo.toml +++ b/opentelemetry-contrib/Cargo.toml @@ -19,6 +19,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] +api = [] default = [] base64_format = ["base64", "binary_propagator"] binary_propagator = [] @@ -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"] diff --git a/opentelemetry-contrib/benches/new_span.rs b/opentelemetry-contrib/benches/new_span.rs new file mode 100644 index 0000000000..35fc0cc980 --- /dev/null +++ b/opentelemetry-contrib/benches/new_span.rs @@ -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, BoxedTracer, Option) { + 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) -> 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); diff --git a/opentelemetry-contrib/src/trace/context.rs b/opentelemetry-contrib/src/trace/context.rs new file mode 100644 index 0000000000..79203a5cc2 --- /dev/null +++ b/opentelemetry-contrib/src/trace/context.rs @@ -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::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::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` 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, Option); + +impl Contextualized { + /// Creates a new instance using the specified value and optional context. + pub fn new(value: T, cx: Option) -> 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) { + (self.0, self.1) + } +} + +impl Clone for Contextualized { + fn clone(&self) -> Self { + Self(self.0.clone(), self.1.clone()) + } +} + +impl Debug for Contextualized { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Contextualized") + .field(&self.0) + .field(&self.1) + .finish() + } +} + +impl Deref for Contextualized { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Contextualized { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/opentelemetry-contrib/src/trace/mod.rs b/opentelemetry-contrib/src/trace/mod.rs index 6f9c4fef49..97ee0db72f 100644 --- a/opentelemetry-contrib/src/trace/mod.rs +++ b/opentelemetry-contrib/src/trace/mod.rs @@ -1,5 +1,15 @@ //! # Opentelemetry trace contrib //! +#[cfg(feature = "api")] +mod context; +#[cfg(feature = "api")] +pub use context::{new_span_if_parent_sampled, new_span_if_recording, Contextualized}; + pub mod exporter; pub mod propagator; + +#[cfg(feature = "api")] +mod tracer_source; +#[cfg(feature = "api")] +pub use tracer_source::TracerSource; diff --git a/opentelemetry-contrib/src/trace/tracer_source.rs b/opentelemetry-contrib/src/trace/tracer_source.rs new file mode 100644 index 0000000000..fdef67a13d --- /dev/null +++ b/opentelemetry-contrib/src/trace/tracer_source.rs @@ -0,0 +1,58 @@ +//! Abstracts away details for acquiring a `Tracer` by instrumented libraries. +use once_cell::sync::OnceCell; +use opentelemetry::global::BoxedTracer; +use std::fmt::Debug; + +/// Holds either a borrowed `BoxedTracer` or a factory that can produce one when +/// and if needed. +/// +/// This unifies handling of obtaining a `Tracer` by library code optimizing for +/// common cases when it will never be needed. +#[derive(Debug)] +pub struct TracerSource<'a> { + variant: Variant<'a>, + tracer: OnceCell, +} + +enum Variant<'a> { + Borrowed(&'a BoxedTracer), + Lazy(&'a dyn Fn() -> BoxedTracer), +} + +impl<'a> TracerSource<'a> { + /// Construct an instance by borrowing the specified `BoxedTracer`. + pub fn borrowed(tracer: &'a BoxedTracer) -> Self { + Self { + variant: Variant::Borrowed(tracer), + tracer: OnceCell::new(), + } + } + + /// Construct an instance which may lazily produce a `BoxedTracer` using + /// the specified factory function. + pub fn lazy(factory: &'a dyn Fn() -> BoxedTracer) -> Self { + Self { + variant: Variant::Lazy(factory), + tracer: OnceCell::new(), + } + } + + /// Get the associated `BoxedTracer`, producing it if necessary. + pub fn get(&self) -> &BoxedTracer { + use Variant::*; + match self.variant { + Borrowed(tracer) => tracer, + Lazy(factory) => self.tracer.get_or_init(factory), + } + } +} + +impl<'a> Debug for Variant<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Variant::*; + match self { + Borrowed(arg0) => f.debug_tuple("Borrowed").field(arg0).finish(), + Lazy(_arg0) => f.debug_tuple("Lazy").finish(), + } + } +} diff --git a/opentelemetry-sdk/benches/context.rs b/opentelemetry-sdk/benches/context.rs index 9e4d5dddb5..f5a1f7e2df 100644 --- a/opentelemetry-sdk/benches/context.rs +++ b/opentelemetry-sdk/benches/context.rs @@ -1,81 +1,136 @@ -use std::fmt::Display; - -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use futures_util::future::BoxFuture; use opentelemetry::{ - trace::{TraceContextExt, Tracer, TracerProvider}, - Context, + global::BoxedTracer, + trace::{ + noop::NoopTracer, SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState, + TracerProvider as _, + }, + Context, ContextGuard, }; use opentelemetry_sdk::{ export::trace::{ExportResult, SpanData, SpanExporter}, - trace as sdktrace, + trace::{config, Sampler, TracerProvider}, }; #[cfg(not(target_os = "windows"))] use pprof::criterion::{Output, PProfProfiler}; +use std::fmt::Display; fn criterion_benchmark(c: &mut Criterion) { - benchmark_group(c, BenchmarkParameter::NoActiveSpan); - benchmark_group(c, BenchmarkParameter::WithActiveSpan); -} - -fn benchmark_group(c: &mut Criterion, p: BenchmarkParameter) { - let _guard = match p { - BenchmarkParameter::NoActiveSpan => None, - BenchmarkParameter::WithActiveSpan => { - let (provider, tracer) = tracer(); - let guard = Context::current_with_span(tracer.start("span")).attach(); - Some((guard, provider)) - } - }; - let mut group = c.benchmark_group("context"); + group.throughput(Throughput::Elements(1)); + for env in [ + Environment::InContext, + Environment::NoContext, + Environment::NoSdk, + ] { + let (_provider, _tracer, _guard) = env.setup(); - group.bench_function(BenchmarkId::new("baseline current()", p), |b| { - b.iter(|| { - black_box(Context::current()); - }) - }); + for api in [Api::Alt, Api::Spec] { + let param = format!("{env}/{api}"); + group.bench_function( + BenchmarkId::new("has_active_span", param.clone()), + |b| match api { + Api::Alt => b.iter(|| Context::map_current(TraceContextExt::has_active_span)), + Api::Spec => b.iter(|| Context::current().has_active_span()), + }, + ); + group.bench_function( + BenchmarkId::new("is_sampled", param.clone()), + |b| match api { + Api::Alt => { + b.iter(|| Context::map_current(|cx| cx.span().span_context().is_sampled())) + } + Api::Spec => b.iter(|| Context::current().span().span_context().is_sampled()), + }, + ); + group.bench_function(BenchmarkId::new("is_recording", param), |b| match api { + Api::Alt => b.iter(|| Context::map_current(|cx| cx.span().is_recording())), + Api::Spec => b.iter(|| Context::current().span().is_recording()), + }); + } + } +} - group.bench_function(BenchmarkId::new("current().has_active_span()", p), |b| { - b.iter(|| { - black_box(Context::current().has_active_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, +} - group.bench_function( - BenchmarkId::new("map_current(|cx| cx.has_active_span())", p), - |b| { - b.iter(|| { - black_box(Context::map_current(|cx| cx.has_active_span())); - }) - }, - ); +impl Api { + const fn as_str(self) -> &'static str { + match self { + Api::Alt => "alt", + Api::Spec => "spec", + } + } +} - group.finish(); +impl Display for Api { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } } #[derive(Copy, Clone)] -enum BenchmarkParameter { - NoActiveSpan, - WithActiveSpan, +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 Display for BenchmarkParameter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - BenchmarkParameter::NoActiveSpan => write!(f, "no-active-span"), - BenchmarkParameter::WithActiveSpan => write!(f, "with-active-span"), +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, BoxedTracer, Option) { + 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), } } } -fn tracer() -> (sdktrace::TracerProvider, sdktrace::Tracer) { - let provider = sdktrace::TracerProvider::builder() - .with_config(sdktrace::config().with_sampler(sdktrace::Sampler::AlwaysOn)) +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, tracer) + (provider, BoxedTracer::new(Box::new(tracer))) } #[derive(Debug)] diff --git a/opentelemetry-sdk/src/trace/tracer.rs b/opentelemetry-sdk/src/trace/tracer.rs index 849604ad63..b3abe69dd8 100644 --- a/opentelemetry-sdk/src/trace/tracer.rs +++ b/opentelemetry-sdk/src/trace/tracer.rs @@ -171,7 +171,7 @@ impl opentelemetry::trace::Tracer for Tracer { .unwrap_or_else(|| config.id_generator.new_trace_id()); }; - // In order to accomodate use cases like `tracing-opentelemetry` we there is the ability + // In order to accommodate use cases like `tracing-opentelemetry` we there is the ability // to use pre-sampling. Otherwise, the standard method of sampling is followed. let sampling_decision = if let Some(sampling_result) = builder.sampling_result.take() { self.process_sampling_result(sampling_result, parent_cx) diff --git a/opentelemetry/src/context.rs b/opentelemetry/src/context.rs index 065d240d8a..a956839460 100644 --- a/opentelemetry/src/context.rs +++ b/opentelemetry/src/context.rs @@ -109,7 +109,7 @@ impl Context { Context::map_current(|cx| cx.clone()) } - /// Applys a function to the current context returning its value. + /// Applies a function to the current context returning its value. /// /// This can be used to build higher performing algebraic expressions for /// optionally creating a new context without the overhead of cloning the