Skip to content

Commit 0ebbcf6

Browse files
feat(metrics): Attach default SDK attributes to metrics
Set these metric attributes by default: - `sentry.environment` - `sentry.release` - `sentry.sdk.name` - `sentry.sdk.version` - `server.address` Preserve explicitly set metric attributes when applying those defaults. Co-authored-by: Joris Bayer <joris.bayer@sentry.io> Closes #1059 Closes [RUST-187](https://linear.app/getsentry/issue/RUST-187/add-trace-metric-default-attribute-enrichment-in-sentry-core)
1 parent e2abb12 commit 0ebbcf6

File tree

2 files changed

+151
-6
lines changed

2 files changed

+151
-6
lines changed

sentry-core/src/client.rs

Lines changed: 42 additions & 4 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,10 +24,12 @@ 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(feature = "logs")]
28+
use sentry_types::protocol::v7::Log;
29+
#[cfg(any(feature = "logs", feature = "metrics"))]
30+
use sentry_types::protocol::v7::LogAttribute;
2731
#[cfg(feature = "metrics")]
2832
use sentry_types::protocol::v7::Metric;
29-
#[cfg(feature = "logs")]
30-
use sentry_types::protocol::v7::{Log, LogAttribute};
3133

3234
impl<T: Into<ClientOptions>> From<T> for Client {
3335
fn from(o: T) -> Client {
@@ -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,
@@ -537,10 +570,15 @@ impl Client {
537570
}
538571
}
539572

540-
/// Prepares a metric to be sent, setting trace association data from the scope.
573+
/// Prepares a metric to be sent, setting trace association data and default attributes.
541574
#[cfg(feature = "metrics")]
542575
fn prepare_metric(&self, mut metric: Metric, scope: &Scope) -> Option<Metric> {
543576
scope.apply_to_metric(&mut metric);
577+
578+
for (key, val) in &self.default_metric_attributes {
579+
metric.attributes.entry(key.clone()).or_insert(val.clone());
580+
}
581+
544582
Some(metric)
545583
}
546584
}

sentry-core/tests/metrics.rs

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use std::time::SystemTime;
55

66
use anyhow::{Context, Result};
77

8-
use sentry::protocol::MetricType;
9-
use sentry_core::protocol::{EnvelopeItem, ItemContainer};
8+
use sentry::protocol::{LogAttribute, MetricType};
9+
use sentry_core::protocol::{Envelope, EnvelopeItem, ItemContainer, Value};
1010
use sentry_core::test;
1111
use sentry_core::{ClientOptions, Hub, TransactionContext};
1212
use sentry_types::protocol::v7::Metric;
@@ -283,6 +283,93 @@ fn metrics_span_id_from_active_span() {
283283
);
284284
}
285285

286+
/// Test that default SDK attributes are attached to metrics.
287+
#[test]
288+
fn default_attributes_attached() {
289+
let options = ClientOptions {
290+
enable_metrics: true,
291+
environment: Some("test-env".into()),
292+
release: Some("1.0.0".into()),
293+
server_name: Some("test-server".into()),
294+
..Default::default()
295+
};
296+
297+
let envelopes = test::with_captured_envelopes_options(|| capture_test_metric("test"), options);
298+
let metric = extract_single_metric(envelopes).expect("expected a single-metric envelope");
299+
300+
let expected_attributes = [
301+
("sentry.environment", "test-env"),
302+
("sentry.release", "1.0.0"),
303+
("sentry.sdk.name", "sentry.rust"),
304+
("sentry.sdk.version", env!("CARGO_PKG_VERSION")),
305+
("server.address", "test-server"),
306+
]
307+
.into_iter()
308+
.map(|(attribute, value)| (attribute.into(), value.into()))
309+
.collect();
310+
311+
assert_eq!(metric.attributes, expected_attributes);
312+
}
313+
314+
/// Test that optional default attributes are omitted when not configured.
315+
#[test]
316+
fn optional_default_attributes_omitted_when_not_configured() {
317+
let options = ClientOptions {
318+
enable_metrics: true,
319+
..Default::default()
320+
};
321+
322+
let envelopes = test::with_captured_envelopes_options(|| capture_test_metric("test"), options);
323+
let metric = extract_single_metric(envelopes).expect("expected a single-metric envelope");
324+
325+
let expected_attributes = [
326+
// Importantly, no other attributes should be set.
327+
("sentry.sdk.name", "sentry.rust"),
328+
("sentry.sdk.version", env!("CARGO_PKG_VERSION")),
329+
]
330+
.into_iter()
331+
.map(|(attribute, value)| (attribute.into(), value.into()))
332+
.collect();
333+
334+
assert_eq!(metric.attributes, expected_attributes);
335+
}
336+
337+
/// Test that explicitly set metric attributes are not overwritten by defaults.
338+
#[test]
339+
fn default_attributes_do_not_overwrite_explicit() {
340+
let options = ClientOptions {
341+
enable_metrics: true,
342+
environment: Some("default-env".into()),
343+
..Default::default()
344+
};
345+
346+
let envelopes = test::with_captured_envelopes_options(
347+
|| {
348+
let mut metric = test_metric("test");
349+
metric.attributes.insert(
350+
"sentry.environment".into(),
351+
LogAttribute(Value::from("custom-env")),
352+
);
353+
Hub::current().capture_metric(metric);
354+
},
355+
options,
356+
);
357+
let metric = extract_single_metric(envelopes).expect("expected a single-metric envelope");
358+
359+
let expected_attributes = [
360+
// Check the environment is the one set directly on the metric
361+
("sentry.environment", "custom-env"),
362+
// The other default attributes also stay
363+
("sentry.sdk.name", "sentry.rust"),
364+
("sentry.sdk.version", env!("CARGO_PKG_VERSION")),
365+
]
366+
.into_iter()
367+
.map(|(attribute, value)| (attribute.into(), value.into()))
368+
.collect();
369+
370+
assert_eq!(metric.attributes, expected_attributes);
371+
}
372+
286373
/// Returns a [`Metric`] with [type `Counter`](MetricType),
287374
/// the provided name, and a value of `1.0`.
288375
fn test_metric<S>(name: S) -> Metric
@@ -309,6 +396,26 @@ where
309396
Hub::current().capture_metric(test_metric(name))
310397
}
311398

399+
/// Helper to extract the single metric from a list of captured envelopes.
400+
///
401+
/// Asserts that the envelope contains only a single item, which contains only
402+
/// a single metrics item, and returns that metrics item, or an error if failed.
403+
fn extract_single_metric<I>(envelopes: I) -> Result<Metric>
404+
where
405+
I: IntoIterator<Item = Envelope>,
406+
{
407+
envelopes
408+
.try_into_only_item()
409+
.context("expected exactly one envelope")?
410+
.into_items()
411+
.try_into_only_item()
412+
.context("expected exactly one item")?
413+
.into_metrics()
414+
.context("expected a metrics item")?
415+
.try_into_only_item()
416+
.context("expected exactly one metric")
417+
}
418+
312419
/// Extension trait for iterators allowing conversion to only item.
313420
trait TryIntoOnlyElementExt<I> {
314421
type Item;

0 commit comments

Comments
 (0)