Skip to content

Commit ad6ddfe

Browse files
Add API for automatically recording trace spans duration to a histogram
This adds an API for automatically tracking span durations with a `TimeHistogram`. The durations are stored regardless of if the underlying span is sampled.
1 parent cd7061c commit ad6ddfe

File tree

4 files changed

+323
-16
lines changed

4 files changed

+323
-16
lines changed

foundations-macros/src/span_fn.rs

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ struct Options {
4444

4545
#[darling(default = "Options::default_async_local")]
4646
async_local: bool,
47+
48+
#[darling(default = "Options::default_time_histogram")]
49+
with_time_histogram: Option<Expr>,
4750
}
4851

4952
impl Options {
@@ -54,6 +57,10 @@ impl Options {
5457
fn default_async_local() -> bool {
5558
false
5659
}
60+
61+
fn default_time_histogram() -> Option<Expr> {
62+
None
63+
}
5764
}
5865

5966
struct Args {
@@ -113,10 +120,19 @@ fn expand_from_parsed(args: Args, item_fn: ItemFn) -> TokenStream2 {
113120
let span_name = args.span_name.as_tokens();
114121
let crate_path = &args.options.crate_path;
115122

116-
quote!(
117-
let __span = #crate_path::telemetry::tracing::span(#span_name);
118-
#block
119-
)
123+
if let Some(timer) = args.options.with_time_histogram {
124+
quote!(
125+
let __span = #crate_path::telemetry::tracing::span(#span_name)
126+
.into_context()
127+
.with_time_histogram(#timer);
128+
#block
129+
)
130+
} else {
131+
quote!(
132+
let __span = #crate_path::telemetry::tracing::span(#span_name);
133+
#block
134+
)
135+
}
120136
}),
121137
};
122138

@@ -182,13 +198,22 @@ fn wrap_with_span(args: &Args, block: TokenStream2) -> TokenStream2 {
182198

183199
let span_name = args.span_name.as_tokens();
184200
let crate_path = &args.options.crate_path;
185-
186-
quote!(
187-
#crate_path::telemetry::tracing::span(#span_name)
188-
.into_context()
189-
.#apply_fn(#block)
190-
.await
191-
)
201+
if let Some(timer) = &args.options.with_time_histogram {
202+
quote!(
203+
#crate_path::telemetry::tracing::span(#span_name)
204+
.into_context()
205+
.with_time_histogram(#timer)
206+
.#apply_fn(#block)
207+
.await
208+
)
209+
} else {
210+
quote!(
211+
#crate_path::telemetry::tracing::span(#span_name)
212+
.into_context()
213+
.#apply_fn(#block)
214+
.await
215+
)
216+
}
192217
}
193218

194219
#[cfg(test)]
@@ -482,4 +507,69 @@ mod tests {
482507

483508
assert_eq!(actual, expected);
484509
}
510+
511+
#[test]
512+
fn span_sync_fn_with_metric_tracker() {
513+
let args = parse_attr! {
514+
#[span_fn("sync_span", with_time_histogram = ::foo::bar::metrics::time_histogram(label = false))]
515+
};
516+
517+
let item_fn = parse_quote! {
518+
fn do_sync() -> io::Result<String> {
519+
do_something_else();
520+
521+
Ok("foo".into())
522+
}
523+
};
524+
525+
let actual = expand_from_parsed(args, item_fn).to_string();
526+
527+
let expected = code_str! {
528+
fn do_sync<>() -> io::Result<String> {
529+
let __span = ::foundations::telemetry::tracing::span("sync_span")
530+
.into_context()
531+
.with_time_histogram(::foo::bar::metrics::time_histogram(label = false));
532+
{
533+
do_something_else();
534+
535+
Ok("foo".into())
536+
}
537+
}
538+
};
539+
540+
assert_eq!(actual, expected);
541+
}
542+
543+
#[test]
544+
fn span_async_fn_with_metric_tracker() {
545+
let args = parse_attr! {
546+
#[span_fn("async_span", with_time_histogram = ::foo::bar::metrics::time_histogram(label = false))]
547+
};
548+
549+
let item_fn = parse_quote! {
550+
async fn do_async() -> io::Result<String> {
551+
do_something_else().await;
552+
553+
Ok("foo".into())
554+
}
555+
};
556+
557+
let actual = expand_from_parsed(args, item_fn).to_string();
558+
559+
let expected = code_str! {
560+
async fn do_async<>() -> io::Result<String> {
561+
::foundations::telemetry::tracing::span("async_span")
562+
.into_context()
563+
.with_time_histogram(::foo::bar::metrics::time_histogram(label = false))
564+
.apply(async move {{
565+
do_something_else().await;
566+
567+
Ok("foo".into())
568+
}})
569+
.await
570+
}
571+
};
572+
573+
assert_eq!(actual, expected);
574+
}
485575
}

foundations/src/telemetry/telemetry_context.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use prometools::histogram::TimeHistogram;
2+
13
use super::TelemetryScope;
24
use crate::utils::feature_use;
35
use std::future::Future;
@@ -226,6 +228,54 @@ impl TelemetryContext {
226228
TestTelemetryContext::new()
227229
}
228230

231+
/// Track the duration of the current `span` in this `TelemetryContext`. The span's duration
232+
/// will be inserted into `histogram`, regardless of whether `span` was sampled or not.
233+
///
234+
/// # Examples
235+
/// ```
236+
/// use foundations::telemetry::TelemetryContext;
237+
/// use foundations::telemetry::metrics::{metrics, HistogramBuilder, TimeHistogram};
238+
/// use foundations::telemetry::tracing::{self, test_trace};
239+
///
240+
/// #[metrics]
241+
/// mod test_metrics {
242+
/// #[ctor = HistogramBuilder { buckets: &[1E-4, 2E-4] }]
243+
/// pub fn histogram(label: bool) -> TimeHistogram;
244+
/// }
245+
///
246+
/// #[tokio::main]
247+
/// async fn main() {
248+
/// // Test context is used for demonstration purposes to show the resulting traces.
249+
/// let ctx = TelemetryContext::test();
250+
/// let hist = test_metrics::histogram(true);
251+
/// let clone = hist.clone();
252+
///
253+
/// {
254+
/// let _scope = ctx.scope();
255+
/// let _root = tracing::span("root");
256+
/// let telemetry_ctx = TelemetryContext::current().with_time_histogram(clone);
257+
///
258+
/// let handle = std::thread::spawn(move || {
259+
/// let _scope = telemetry_ctx.scope();
260+
/// let _child = tracing::span("child");
261+
/// });
262+
///
263+
/// handle.join();
264+
/// }
265+
///
266+
/// let snapshot = hist.snapshot();
267+
/// assert_eq!(snapshot.count(), 1);
268+
/// }
269+
/// ```
270+
#[cfg(all(feature = "metrics", feature = "tracing"))]
271+
pub fn with_time_histogram(mut self, histogram: TimeHistogram) -> Self {
272+
if let Some(inner) = self.span.as_mut() {
273+
inner.track_with_time_histogram(histogram);
274+
}
275+
276+
self
277+
}
278+
229279
/// Wraps a future with the telemetry context.
230280
///
231281
/// [`TelemetryScope`] can't be used across `await` points to propagate the telemetry context,

foundations/src/telemetry/tracing/internal.rs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use cf_rustracing::sampler::BoxSampler;
66
use cf_rustracing::tag::Tag;
77
use cf_rustracing_jaeger::span::{Span, SpanContext, SpanContextState};
88
use parking_lot::RwLock;
9+
use prometools::histogram::{HistogramTimer, TimeHistogram};
910
use rand::{self, Rng};
1011
use std::borrow::Cow;
1112
use std::error::Error;
@@ -54,14 +55,31 @@ impl From<SharedSpanHandle> for Arc<RwLock<Span>> {
5455
}
5556
}
5657

57-
#[derive(Debug, Clone)]
58+
#[derive(Clone)]
5859
pub(crate) struct SharedSpan {
5960
// NOTE: we intentionally use a lock without poisoning here to not
6061
// panic the threads if they just share telemetry with failed thread.
6162
pub(crate) inner: SharedSpanHandle,
6263
// NOTE: store sampling flag separately, so we don't need to acquire lock
6364
// every time we need to check the flag.
6465
is_sampled: bool,
66+
timer: Option<Arc<HistogramTimer>>,
67+
}
68+
69+
impl SharedSpan {
70+
#[cfg(all(feature = "metrics", feature = "tracing"))]
71+
pub(crate) fn track_with_time_histogram(&mut self, timer: TimeHistogram) {
72+
let _ = self.timer.insert(Arc::new(timer.start_timer()));
73+
}
74+
}
75+
76+
impl std::fmt::Debug for SharedSpan {
77+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78+
f.debug_struct("SharedSpan")
79+
.field("inner", &self.inner)
80+
.field("is_sampled", &self.is_sampled)
81+
.finish()
82+
}
6583
}
6684

6785
impl From<Span> for SharedSpan {
@@ -71,6 +89,7 @@ impl From<Span> for SharedSpan {
7189
Self {
7290
inner: SharedSpanHandle::new(inner),
7391
is_sampled,
92+
timer: None,
7493
}
7594
}
7695
}
@@ -83,12 +102,22 @@ pub fn write_current_span(write_fn: impl FnOnce(&mut Span)) {
83102
}
84103
}
85104

86-
pub(crate) fn create_span(name: impl Into<Cow<'static, str>>) -> SharedSpan {
87-
match current_span() {
105+
pub(crate) fn create_span(
106+
name: impl Into<Cow<'static, str>>,
107+
histogram: Option<TimeHistogram>,
108+
) -> SharedSpan {
109+
let span = match current_span() {
88110
Some(parent) => parent.inner.read().child(name, |o| o.start()),
89111
None => start_trace(name, Default::default()),
112+
};
113+
114+
let timer = histogram.map(|t| Arc::new(t.start_timer()));
115+
116+
SharedSpan {
117+
is_sampled: span.is_sampled(),
118+
inner: SharedSpanHandle::new(span),
119+
timer,
90120
}
91-
.into()
92121
}
93122

94123
pub(crate) fn current_span() -> Option<SharedSpan> {

0 commit comments

Comments
 (0)