Skip to content

Commit e2abb12

Browse files
feat(metrics): Associate trace data with metrics
Set trace and span id on metrics. Co-authored-by: Joris Bayer <joris.bayer@sentry.io> Closes #1058 Closes [RUST-186](https://linear.app/getsentry/issue/RUST-186/add-trace-metric-tracing-association-in-sentry-core)
1 parent 21cd8f2 commit e2abb12

File tree

5 files changed

+128
-9
lines changed

5 files changed

+128
-9
lines changed

sentry-core/src/client.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -524,16 +524,25 @@ impl Client {
524524

525525
/// Captures a metric and sends it to Sentry.
526526
#[cfg(feature = "metrics")]
527-
pub fn capture_metric(&self, metric: Metric, _: &Scope) {
528-
if let Some(batcher) = self
529-
.metrics_batcher
530-
.read()
531-
.expect("metrics batcher lock could not be acquired")
532-
.as_ref()
533-
{
534-
batcher.enqueue(metric);
527+
pub fn capture_metric(&self, metric: Metric, scope: &Scope) {
528+
if let Some(metric) = self.prepare_metric(metric, scope) {
529+
if let Some(batcher) = self
530+
.metrics_batcher
531+
.read()
532+
.expect("metrics batcher lock could not be acquired")
533+
.as_ref()
534+
{
535+
batcher.enqueue(metric);
536+
}
535537
}
536538
}
539+
540+
/// Prepares a metric to be sent, setting trace association data from the scope.
541+
#[cfg(feature = "metrics")]
542+
fn prepare_metric(&self, mut metric: Metric, scope: &Scope) -> Option<Metric> {
543+
scope.apply_to_metric(&mut metric);
544+
Some(metric)
545+
}
537546
}
538547

539548
// Make this unwind safe. It's not out of the box because of the

sentry-core/src/performance.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,15 @@ impl TransactionOrSpan {
586586
TransactionOrSpan::Span(span) => span.finish(),
587587
}
588588
}
589+
590+
/// Get the span ID of this [`TransactionOrSpan`].
591+
#[cfg(feature = "metrics")]
592+
pub(crate) fn span_id(&self) -> SpanId {
593+
match self {
594+
TransactionOrSpan::Transaction(transaction) => transaction.get_trace_context().span_id,
595+
TransactionOrSpan::Span(span) => span.get_span_id(),
596+
}
597+
}
589598
}
590599

591600
#[derive(Debug)]

sentry-core/src/scope/noop.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::fmt;
22

33
#[cfg(feature = "logs")]
44
use crate::protocol::Log;
5+
#[cfg(feature = "metrics")]
6+
use crate::protocol::Metric;
57
use crate::protocol::{Context, Event, Level, User, Value};
68
use crate::TransactionOrSpan;
79

@@ -119,6 +121,13 @@ impl Scope {
119121
minimal_unreachable!();
120122
}
121123

124+
/// Applies the contained scoped data to fill a trace metric.
125+
#[cfg(feature = "metrics")]
126+
pub fn apply_to_metric(&self, metric: &mut Metric) {
127+
let _metric = metric;
128+
minimal_unreachable!();
129+
}
130+
122131
/// Set the given [`TransactionOrSpan`] as the active span for this scope.
123132
pub fn set_span(&mut self, span: Option<TransactionOrSpan>) {
124133
let _ = span;

sentry-core/src/scope/real.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use std::sync::Mutex;
66
use std::sync::{Arc, PoisonError, RwLock};
77

88
use crate::performance::TransactionOrSpan;
9+
#[cfg(feature = "metrics")]
10+
use crate::protocol::Metric;
911
use crate::protocol::{
1012
Attachment, Breadcrumb, Context, Event, Level, TraceContext, Transaction, User, Value,
1113
};
@@ -399,6 +401,17 @@ impl Scope {
399401
}
400402
}
401403

404+
/// Applies the contained scoped data to a trace metric, setting the `trace_id` and `span_id`.
405+
#[cfg(feature = "metrics")]
406+
pub fn apply_to_metric(&self, metric: &mut Metric) {
407+
metric.trace_id = self
408+
.get_span()
409+
.map(|span| span.get_trace_context().trace_id)
410+
.unwrap_or(self.propagation_context.trace_id);
411+
412+
metric.span_id = self.get_span().map(|span| span.span_id());
413+
}
414+
402415
/// Set the given [`TransactionOrSpan`] as the active span for this scope.
403416
pub fn set_span(&mut self, span: Option<TransactionOrSpan>) {
404417
self.span = Arc::new(span);

sentry-core/tests/metrics.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use anyhow::{Context, Result};
88
use sentry::protocol::MetricType;
99
use sentry_core::protocol::{EnvelopeItem, ItemContainer};
1010
use sentry_core::test;
11-
use sentry_core::{ClientOptions, Hub};
11+
use sentry_core::{ClientOptions, Hub, TransactionContext};
1212
use sentry_types::protocol::v7::Metric;
1313

1414
/// Test that metrics are sent when metrics are enabled.
@@ -204,6 +204,85 @@ fn test_metrics_batching_over_limit() {
204204
)
205205
}
206206

207+
/// Test that metrics in the same scope share the same trace_id when no span is active.
208+
///
209+
/// This tests that trace ID is set from the propagation context when there is no active span.
210+
#[test]
211+
fn metrics_share_trace_id_without_active_span() {
212+
let options = ClientOptions {
213+
enable_metrics: true,
214+
..Default::default()
215+
};
216+
217+
let envelopes = test::with_captured_envelopes_options(
218+
|| {
219+
capture_test_metric("test-1");
220+
capture_test_metric("test-2");
221+
},
222+
options,
223+
);
224+
let envelope = envelopes
225+
.try_into_only_item()
226+
.expect("expected one envelope");
227+
let item = envelope
228+
.into_items()
229+
.try_into_only_item()
230+
.expect("expected one item");
231+
let metrics = item.into_metrics().expect("expected metrics item");
232+
233+
let [metric1, metric2] = metrics.as_slice() else {
234+
panic!("expected exactly two metrics");
235+
};
236+
237+
assert_eq!(
238+
metric1.trace_id, metric2.trace_id,
239+
"metrics in the same scope should share the same trace_id"
240+
);
241+
242+
assert!(metric1.span_id.is_none());
243+
assert!(metric2.span_id.is_none());
244+
}
245+
246+
/// Test that span_id is set from the active span when one is present.
247+
#[test]
248+
fn metrics_span_id_from_active_span() {
249+
let options = ClientOptions {
250+
enable_metrics: true,
251+
..Default::default()
252+
};
253+
254+
let mut expected_span_id = None;
255+
let envelopes = test::with_captured_envelopes_options(
256+
|| {
257+
let transaction_ctx = TransactionContext::new("test transaction", "test");
258+
expected_span_id = Some(transaction_ctx.span_id());
259+
let transaction = sentry_core::start_transaction(transaction_ctx);
260+
sentry_core::configure_scope(|scope| scope.set_span(Some(transaction.clone().into())));
261+
capture_test_metric("test");
262+
transaction.finish();
263+
},
264+
options,
265+
);
266+
267+
let expected_span_id = expected_span_id.expect("expected_span_id did not get set");
268+
269+
let envelope = envelopes
270+
.try_into_only_item()
271+
.expect("expected one envelope");
272+
let item = envelope
273+
.into_items()
274+
.try_into_only_item()
275+
.expect("expected one item");
276+
let mut metrics = item.into_metrics().expect("expected metrics item");
277+
let metric = metrics.pop().expect("expected one metric");
278+
279+
assert_eq!(
280+
metric.span_id,
281+
Some(expected_span_id),
282+
"span_id should be set from the active span"
283+
);
284+
}
285+
207286
/// Returns a [`Metric`] with [type `Counter`](MetricType),
208287
/// the provided name, and a value of `1.0`.
209288
fn test_metric<S>(name: S) -> Metric

0 commit comments

Comments
 (0)