Skip to content

Commit 4a95362

Browse files
# This is a combination of 6 commits.
# This is the 1st commit message: 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 4a95362

File tree

6 files changed

+446
-20
lines changed

6 files changed

+446
-20
lines changed

sentry-core/src/client.rs

Lines changed: 61 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: BTreeMap<Cow<'static, str>, 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: Default::default(),
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,28 @@ 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 always_present_attributes = [
286+
("sentry.sdk.name", &self.sdk_info.name),
287+
("sentry.sdk.version", &self.sdk_info.version),
288+
]
289+
.into_iter()
290+
.map(|(name, value)| (name.into(), value.as_str().into()));
291+
292+
let maybe_present_attributes = [
293+
("sentry.environment", &self.options.environment),
294+
("sentry.release", &self.options.release),
295+
("server.address", &self.options.server_name),
296+
]
297+
.into_iter()
298+
.filter_map(|(name, value)| value.clone().map(|value| (name.into(), value.into())));
299+
300+
self.default_metric_attributes = maybe_present_attributes
301+
.chain(always_present_attributes)
302+
.collect();
303+
}
304+
272305
pub(crate) fn get_integration<I>(&self) -> Option<&I>
273306
where
274307
I: Integration,
@@ -524,15 +557,34 @@ impl Client {
524557

525558
/// Captures a metric and sends it to Sentry.
526559
#[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);
560+
pub fn capture_metric(&self, metric: Metric, scope: &Scope) {
561+
if let Some(metric) = self.prepare_metric(metric, scope) {
562+
if let Some(batcher) = self
563+
.metrics_batcher
564+
.read()
565+
.expect("metrics batcher lock could not be acquired")
566+
.as_ref()
567+
{
568+
batcher.enqueue(metric);
569+
}
570+
}
571+
}
572+
573+
/// Prepares a metric to be sent, setting the `trace_id` and other default attributes, and
574+
/// processing it through `before_send_metric`.
575+
#[cfg(feature = "metrics")]
576+
fn prepare_metric(&self, mut metric: Metric, scope: &Scope) -> Option<Metric> {
577+
scope.apply_to_metric(&mut metric, self.options.send_default_pii);
578+
579+
for (key, val) in &self.default_metric_attributes {
580+
metric.attributes.entry(key.clone()).or_insert(val.clone());
535581
}
582+
583+
if let Some(ref func) = self.options.before_send_metric {
584+
metric = func(metric)?;
585+
}
586+
587+
Some(metric)
536588
}
537589
}
538590

sentry-core/src/clientoptions.rs

Lines changed: 16 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
///
@@ -235,6 +240,12 @@ impl fmt::Debug for ClientOptions {
235240
struct BeforeSendLog;
236241
self.before_send_log.as_ref().map(|_| BeforeSendLog)
237242
};
243+
#[cfg(feature = "metrics")]
244+
let before_send_metric = {
245+
#[derive(Debug)]
246+
struct BeforeSendMetric;
247+
self.before_send_metric.as_ref().map(|_| BeforeSendMetric)
248+
};
238249
#[derive(Debug)]
239250
struct TransportFactory;
240251

@@ -282,7 +293,9 @@ impl fmt::Debug for ClientOptions {
282293
.field("before_send_log", &before_send_log);
283294

284295
#[cfg(feature = "metrics")]
285-
debug_struct.field("enable_metrics", &self.enable_metrics);
296+
debug_struct
297+
.field("enable_metrics", &self.enable_metrics)
298+
.field("before_send_metric", &before_send_metric);
286299

287300
debug_struct.field("user_agent", &self.user_agent).finish()
288301
}
@@ -325,6 +338,8 @@ impl Default for ClientOptions {
325338
before_send_log: None,
326339
#[cfg(feature = "metrics")]
327340
enable_metrics: false,
341+
#[cfg(feature = "metrics")]
342+
before_send_metric: None,
328343
}
329344
}
330345
}

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: 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: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ use std::sync::Mutex;
66
use std::sync::{Arc, PoisonError, RwLock};
77

88
use crate::performance::TransactionOrSpan;
9+
#[cfg(feature = "logs")]
10+
use crate::protocol::Log;
11+
#[cfg(any(feature = "logs", feature = "metrics"))]
12+
use crate::protocol::LogAttribute;
13+
#[cfg(feature = "metrics")]
14+
use crate::protocol::Metric;
915
use crate::protocol::{
1016
Attachment, Breadcrumb, Context, Event, Level, TraceContext, Transaction, User, Value,
1117
};
12-
#[cfg(feature = "logs")]
13-
use crate::protocol::{Log, LogAttribute};
1418
#[cfg(feature = "release-health")]
1519
use crate::session::Session;
1620
use crate::{Client, SentryTrace, TraceHeader, TraceHeadersIter};
@@ -399,6 +403,28 @@ 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+
metric.trace_id = (*self.span)
412+
.as_ref()
413+
.map(|span| span.get_trace_context().trace_id)
414+
.unwrap_or(self.propagation_context.trace_id);
415+
416+
metric.span_id = self.get_span().map(|ts| ts.span_id());
417+
418+
let should_add_user_attributes = send_default_pii && !metric.has_any_user_attributes();
419+
420+
if let Some(user) = should_add_user_attributes
421+
.then_some(self.user.as_deref())
422+
.flatten()
423+
{
424+
metric.apply_user_attributes(user);
425+
}
426+
}
427+
402428
/// Set the given [`TransactionOrSpan`] as the active span for this scope.
403429
pub fn set_span(&mut self, span: Option<TransactionOrSpan>) {
404430
self.span = Arc::new(span);
@@ -444,3 +470,48 @@ impl Scope {
444470
}
445471
}
446472
}
473+
474+
#[cfg(feature = "metrics")]
475+
trait MetricExt {
476+
fn insert_attribute<K, V>(&mut self, key: K, value: V)
477+
where
478+
K: Into<Cow<'static, str>>,
479+
V: Into<Value>;
480+
481+
fn attribute(&self, key: &str) -> Option<&LogAttribute>;
482+
483+
/// Applies user attributes from provided [`User`].
484+
fn apply_user_attributes(&mut self, user: &User) {
485+
[
486+
("user.id", user.id.as_deref()),
487+
("user.name", user.username.as_deref()),
488+
("user.email", user.email.as_deref()),
489+
]
490+
.into_iter()
491+
.flat_map(|(attribute, value)| value.map(|v| (attribute, v)))
492+
.for_each(|(attribute, value)| self.insert_attribute(attribute, value));
493+
}
494+
495+
/// Checks if any user attributes are on this metric
496+
fn has_any_user_attributes(&self) -> bool {
497+
["user.id", "user.name", "user.email"]
498+
.into_iter()
499+
.any(|key| self.attribute(key).is_some())
500+
}
501+
}
502+
503+
#[cfg(feature = "metrics")]
504+
impl MetricExt for Metric {
505+
fn insert_attribute<K, V>(&mut self, key: K, value: V)
506+
where
507+
K: Into<Cow<'static, str>>,
508+
V: Into<Value>,
509+
{
510+
self.attributes
511+
.insert(key.into(), LogAttribute(value.into()));
512+
}
513+
514+
fn attribute(&self, key: &str) -> Option<&LogAttribute> {
515+
self.attributes.get(key)
516+
}
517+
}

0 commit comments

Comments
 (0)