Skip to content

Commit 35aa0aa

Browse files
feat(metrics): Add trace metric enrichment with default and user attributes (RUST-169)
Add metric enrichment in sentry-core so emitted trace metrics include: - Trace/span association: trace_id from active span or propagation context, span_id from active span when not explicitly set. - Default SDK attributes: sentry.environment, sentry.release, sentry.sdk.name, sentry.sdk.version, and server.address. - User PII attributes (user.id, user.name, user.email) gated by send_default_pii. - before_send_metric callback for filtering/modifying metrics. Attribute merges use or_insert to preserve explicitly set metric attributes, matching the behavior specified in https://develop.sentry.dev/sdk/telemetry/metrics/#default-attributes and https://develop.sentry.dev/sdk/telemetry/metrics/#user-attributes. Scope and client enrichment logic is based on the approach from #997. Co-authored-by: Joris Bayer <joris.bayer@sentry.io> Closes #1024 Closes [RUST-169](https://linear.app/getsentry/issue/RUST-169/add-trace-metric-default-and-user-attribute-enrichment-in-sentry-core)
1 parent 21cd8f2 commit 35aa0aa

File tree

5 files changed

+420
-13
lines changed

5 files changed

+420
-13
lines changed

sentry-core/src/client.rs

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::any::TypeId;
22
use std::borrow::Cow;
3-
#[cfg(feature = "logs")]
3+
#[cfg(any(feature = "logs", feature = "metrics"))]
44
use std::collections::BTreeMap;
55
use std::fmt;
66
use std::panic::RefUnwindSafe;
@@ -24,6 +24,8 @@ use crate::SessionMode;
2424
use crate::{ClientOptions, Envelope, Hub, Integration, Scope, Transport};
2525
#[cfg(feature = "logs")]
2626
use sentry_types::protocol::v7::Context;
27+
#[cfg(all(feature = "metrics", not(feature = "logs")))]
28+
use sentry_types::protocol::v7::LogAttribute;
2729
#[cfg(feature = "metrics")]
2830
use sentry_types::protocol::v7::Metric;
2931
#[cfg(feature = "logs")]
@@ -65,6 +67,8 @@ pub struct Client {
6567
metrics_batcher: RwLock<Option<Batcher<Metric>>>,
6668
#[cfg(feature = "logs")]
6769
default_log_attributes: Option<BTreeMap<String, LogAttribute>>,
70+
#[cfg(feature = "metrics")]
71+
default_metric_attributes: Option<BTreeMap<String, LogAttribute>>,
6872
integrations: Vec<(TypeId, Arc<dyn Integration>)>,
6973
pub(crate) sdk_info: ClientSdkInfo,
7074
}
@@ -113,6 +117,8 @@ impl Clone for Client {
113117
metrics_batcher,
114118
#[cfg(feature = "logs")]
115119
default_log_attributes: self.default_log_attributes.clone(),
120+
#[cfg(feature = "metrics")]
121+
default_metric_attributes: self.default_metric_attributes.clone(),
116122
integrations: self.integrations.clone(),
117123
sdk_info: self.sdk_info.clone(),
118124
}
@@ -208,13 +214,18 @@ impl Client {
208214
metrics_batcher,
209215
#[cfg(feature = "logs")]
210216
default_log_attributes: None,
217+
#[cfg(feature = "metrics")]
218+
default_metric_attributes: None,
211219
integrations,
212220
sdk_info,
213221
};
214222

215223
#[cfg(feature = "logs")]
216224
client.cache_default_log_attributes();
217225

226+
#[cfg(feature = "metrics")]
227+
client.cache_default_metric_attributes();
228+
218229
client
219230
}
220231

@@ -269,6 +280,35 @@ impl Client {
269280
self.default_log_attributes = Some(attributes);
270281
}
271282

283+
#[cfg(feature = "metrics")]
284+
fn cache_default_metric_attributes(&mut self) {
285+
let mut attributes = BTreeMap::new();
286+
287+
if let Some(environment) = self.options.environment.as_ref() {
288+
attributes.insert("sentry.environment".to_owned(), environment.clone().into());
289+
}
290+
291+
if let Some(release) = self.options.release.as_ref() {
292+
attributes.insert("sentry.release".to_owned(), release.clone().into());
293+
}
294+
295+
attributes.insert(
296+
"sentry.sdk.name".to_owned(),
297+
self.sdk_info.name.to_owned().into(),
298+
);
299+
300+
attributes.insert(
301+
"sentry.sdk.version".to_owned(),
302+
self.sdk_info.version.to_owned().into(),
303+
);
304+
305+
if let Some(server) = &self.options.server_name {
306+
attributes.insert("server.address".to_owned(), server.clone().into());
307+
}
308+
309+
self.default_metric_attributes = Some(attributes);
310+
}
311+
272312
pub(crate) fn get_integration<I>(&self) -> Option<&I>
273313
where
274314
I: Integration,
@@ -524,15 +564,37 @@ impl Client {
524564

525565
/// Captures a metric and sends it to Sentry.
526566
#[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);
567+
pub fn capture_metric(&self, metric: Metric, scope: &Scope) {
568+
if !self.options.enable_metrics {
569+
return;
535570
}
571+
if let Some(metric) = self.prepare_metric(metric, scope) {
572+
if let Some(ref batcher) = *self.metrics_batcher.read().unwrap() {
573+
batcher.enqueue(metric);
574+
}
575+
}
576+
}
577+
578+
/// Prepares a metric to be sent, setting the `trace_id` and other default attributes, and
579+
/// processing it through `before_send_metric`.
580+
#[cfg(feature = "metrics")]
581+
fn prepare_metric(&self, mut metric: Metric, scope: &Scope) -> Option<Metric> {
582+
scope.apply_to_metric(&mut metric, self.options.send_default_pii);
583+
584+
if let Some(default_attributes) = self.default_metric_attributes.as_ref() {
585+
for (key, val) in default_attributes.iter() {
586+
metric
587+
.attributes
588+
.entry(key.to_owned())
589+
.or_insert(val.clone());
590+
}
591+
}
592+
593+
if let Some(ref func) = self.options.before_send_metric {
594+
metric = func(metric)?;
595+
}
596+
597+
Some(metric)
536598
}
537599
}
538600

sentry-core/src/clientoptions.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use crate::constants::USER_AGENT;
77
use crate::performance::TracesSampler;
88
#[cfg(feature = "logs")]
99
use crate::protocol::Log;
10+
#[cfg(feature = "metrics")]
11+
use crate::protocol::Metric;
1012
use crate::protocol::{Breadcrumb, Event};
1113
use crate::types::Dsn;
1214
use crate::{Integration, IntoDsn, TransportFactory};
@@ -175,6 +177,9 @@ pub struct ClientOptions {
175177
/// Determines whether captured metrics should be sent to Sentry (defaults to false).
176178
#[cfg(feature = "metrics")]
177179
pub enable_metrics: bool,
180+
/// Callback that is executed for each Metric before sending.
181+
#[cfg(feature = "metrics")]
182+
pub before_send_metric: Option<BeforeCallback<Metric>>,
178183
// Other options not documented in Unified API
179184
/// Disable SSL verification.
180185
///
@@ -282,7 +287,16 @@ impl fmt::Debug for ClientOptions {
282287
.field("before_send_log", &before_send_log);
283288

284289
#[cfg(feature = "metrics")]
285-
debug_struct.field("enable_metrics", &self.enable_metrics);
290+
{
291+
let before_send_metric = {
292+
#[derive(Debug)]
293+
struct BeforeSendMetric;
294+
self.before_send_metric.as_ref().map(|_| BeforeSendMetric)
295+
};
296+
debug_struct
297+
.field("enable_metrics", &self.enable_metrics)
298+
.field("before_send_metric", &before_send_metric);
299+
}
286300

287301
debug_struct.field("user_agent", &self.user_agent).finish()
288302
}
@@ -325,6 +339,8 @@ impl Default for ClientOptions {
325339
before_send_log: None,
326340
#[cfg(feature = "metrics")]
327341
enable_metrics: false,
342+
#[cfg(feature = "metrics")]
343+
before_send_metric: None,
328344
}
329345
}
330346
}

sentry-core/src/scope/noop.rs

Lines changed: 10 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,14 @@ 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, send_default_pii: bool) {
127+
let _metric = metric;
128+
let _send_default_pii = send_default_pii;
129+
minimal_unreachable!();
130+
}
131+
122132
/// Set the given [`TransactionOrSpan`] as the active span for this scope.
123133
pub fn set_span(&mut self, span: Option<TransactionOrSpan>) {
124134
let _ = span;

sentry-core/src/scope/real.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ use crate::protocol::{
1010
Attachment, Breadcrumb, Context, Event, Level, TraceContext, Transaction, User, Value,
1111
};
1212
#[cfg(feature = "logs")]
13-
use crate::protocol::{Log, LogAttribute};
13+
use crate::protocol::Log;
14+
#[cfg(any(feature = "logs", feature = "metrics"))]
15+
use crate::protocol::LogAttribute;
16+
#[cfg(feature = "metrics")]
17+
use crate::protocol::Metric;
1418
#[cfg(feature = "release-health")]
1519
use crate::session::Session;
1620
use crate::{Client, SentryTrace, TraceHeader, TraceHeadersIter};
@@ -399,6 +403,59 @@ impl Scope {
399403
}
400404
}
401405

406+
/// Applies the contained scoped data to a trace metric, setting the `trace_id`, `span_id`,
407+
/// and certain default attributes. User PII attributes are only attached when
408+
/// `send_default_pii` is `true`.
409+
#[cfg(feature = "metrics")]
410+
pub fn apply_to_metric(&self, metric: &mut Metric, send_default_pii: bool) {
411+
if let Some(span) = self.span.as_ref() {
412+
metric.trace_id = span.get_trace_context().trace_id;
413+
} else {
414+
metric.trace_id = self.propagation_context.trace_id;
415+
}
416+
417+
if metric.span_id.is_none() {
418+
if let Some(span) = self.get_span() {
419+
let span_id = match span {
420+
crate::TransactionOrSpan::Transaction(transaction) => {
421+
transaction.get_trace_context().span_id
422+
}
423+
crate::TransactionOrSpan::Span(span) => span.get_span_id(),
424+
};
425+
metric.span_id = Some(span_id);
426+
}
427+
}
428+
429+
if send_default_pii {
430+
if let Some(user) = self.user.as_ref() {
431+
if !metric.attributes.contains_key("user.id") {
432+
if let Some(id) = user.id.as_ref() {
433+
metric
434+
.attributes
435+
.insert("user.id".to_owned(), LogAttribute(id.to_owned().into()));
436+
}
437+
}
438+
439+
if !metric.attributes.contains_key("user.name") {
440+
if let Some(name) = user.username.as_ref() {
441+
metric
442+
.attributes
443+
.insert("user.name".to_owned(), LogAttribute(name.to_owned().into()));
444+
}
445+
}
446+
447+
if !metric.attributes.contains_key("user.email") {
448+
if let Some(email) = user.email.as_ref() {
449+
metric.attributes.insert(
450+
"user.email".to_owned(),
451+
LogAttribute(email.to_owned().into()),
452+
);
453+
}
454+
}
455+
}
456+
}
457+
}
458+
402459
/// Set the given [`TransactionOrSpan`] as the active span for this scope.
403460
pub fn set_span(&mut self, span: Option<TransactionOrSpan>) {
404461
self.span = Arc::new(span);

0 commit comments

Comments
 (0)