diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index 235e30e7..d6e786ff 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -1,6 +1,6 @@ use std::any::TypeId; use std::borrow::Cow; -#[cfg(feature = "logs")] +#[cfg(any(feature = "logs", feature = "metrics"))] use std::collections::BTreeMap; use std::fmt; use std::panic::RefUnwindSafe; @@ -24,10 +24,12 @@ use crate::SessionMode; use crate::{ClientOptions, Envelope, Hub, Integration, Scope, Transport}; #[cfg(feature = "logs")] use sentry_types::protocol::v7::Context; +#[cfg(feature = "logs")] +use sentry_types::protocol::v7::Log; +#[cfg(any(feature = "logs", feature = "metrics"))] +use sentry_types::protocol::v7::LogAttribute; #[cfg(feature = "metrics")] use sentry_types::protocol::v7::Metric; -#[cfg(feature = "logs")] -use sentry_types::protocol::v7::{Log, LogAttribute}; impl> From for Client { fn from(o: T) -> Client { @@ -65,6 +67,8 @@ pub struct Client { metrics_batcher: RwLock>>, #[cfg(feature = "logs")] default_log_attributes: Option>, + #[cfg(feature = "metrics")] + default_metric_attributes: BTreeMap, LogAttribute>, integrations: Vec<(TypeId, Arc)>, pub(crate) sdk_info: ClientSdkInfo, } @@ -113,6 +117,8 @@ impl Clone for Client { metrics_batcher, #[cfg(feature = "logs")] default_log_attributes: self.default_log_attributes.clone(), + #[cfg(feature = "metrics")] + default_metric_attributes: self.default_metric_attributes.clone(), integrations: self.integrations.clone(), sdk_info: self.sdk_info.clone(), } @@ -208,6 +214,8 @@ impl Client { metrics_batcher, #[cfg(feature = "logs")] default_log_attributes: None, + #[cfg(feature = "metrics")] + default_metric_attributes: Default::default(), integrations, sdk_info, }; @@ -215,6 +223,9 @@ impl Client { #[cfg(feature = "logs")] client.cache_default_log_attributes(); + #[cfg(feature = "metrics")] + client.cache_default_metric_attributes(); + client } @@ -269,6 +280,28 @@ impl Client { self.default_log_attributes = Some(attributes); } + #[cfg(feature = "metrics")] + fn cache_default_metric_attributes(&mut self) { + let always_present_attributes = [ + ("sentry.sdk.name", &self.sdk_info.name), + ("sentry.sdk.version", &self.sdk_info.version), + ] + .into_iter() + .map(|(name, value)| (name.into(), value.as_str().into())); + + let maybe_present_attributes = [ + ("sentry.environment", &self.options.environment), + ("sentry.release", &self.options.release), + ("server.address", &self.options.server_name), + ] + .into_iter() + .filter_map(|(name, value)| value.clone().map(|value| (name.into(), value.into()))); + + self.default_metric_attributes = maybe_present_attributes + .chain(always_present_attributes) + .collect(); + } + pub(crate) fn get_integration(&self) -> Option<&I> where I: Integration, @@ -537,10 +570,15 @@ impl Client { } } - /// Prepares a metric to be sent, setting trace association data from the scope. + /// Prepares a metric to be sent, setting trace association data and default attributes. #[cfg(feature = "metrics")] fn prepare_metric(&self, mut metric: Metric, scope: &Scope) -> Option { scope.apply_to_metric(&mut metric); + + for (key, val) in &self.default_metric_attributes { + metric.attributes.entry(key.clone()).or_insert(val.clone()); + } + Some(metric) } } diff --git a/sentry-core/tests/metrics.rs b/sentry-core/tests/metrics.rs index 063b1559..882b2501 100644 --- a/sentry-core/tests/metrics.rs +++ b/sentry-core/tests/metrics.rs @@ -5,8 +5,8 @@ use std::time::SystemTime; use anyhow::{Context, Result}; -use sentry::protocol::MetricType; -use sentry_core::protocol::{EnvelopeItem, ItemContainer}; +use sentry::protocol::{LogAttribute, MetricType}; +use sentry_core::protocol::{Envelope, EnvelopeItem, ItemContainer, Value}; use sentry_core::test; use sentry_core::{ClientOptions, Hub, TransactionContext}; use sentry_types::protocol::v7::Metric; @@ -283,6 +283,93 @@ fn metrics_span_id_from_active_span() { ); } +/// Test that default SDK attributes are attached to metrics. +#[test] +fn default_attributes_attached() { + let options = ClientOptions { + enable_metrics: true, + environment: Some("test-env".into()), + release: Some("1.0.0".into()), + server_name: Some("test-server".into()), + ..Default::default() + }; + + let envelopes = test::with_captured_envelopes_options(|| capture_test_metric("test"), options); + let metric = extract_single_metric(envelopes).expect("expected a single-metric envelope"); + + let expected_attributes = [ + ("sentry.environment", "test-env"), + ("sentry.release", "1.0.0"), + ("sentry.sdk.name", "sentry.rust"), + ("sentry.sdk.version", env!("CARGO_PKG_VERSION")), + ("server.address", "test-server"), + ] + .into_iter() + .map(|(attribute, value)| (attribute.into(), value.into())) + .collect(); + + assert_eq!(metric.attributes, expected_attributes); +} + +/// Test that optional default attributes are omitted when not configured. +#[test] +fn optional_default_attributes_omitted_when_not_configured() { + let options = ClientOptions { + enable_metrics: true, + ..Default::default() + }; + + let envelopes = test::with_captured_envelopes_options(|| capture_test_metric("test"), options); + let metric = extract_single_metric(envelopes).expect("expected a single-metric envelope"); + + let expected_attributes = [ + // Importantly, no other attributes should be set. + ("sentry.sdk.name", "sentry.rust"), + ("sentry.sdk.version", env!("CARGO_PKG_VERSION")), + ] + .into_iter() + .map(|(attribute, value)| (attribute.into(), value.into())) + .collect(); + + assert_eq!(metric.attributes, expected_attributes); +} + +/// Test that explicitly set metric attributes are not overwritten by defaults. +#[test] +fn default_attributes_do_not_overwrite_explicit() { + let options = ClientOptions { + enable_metrics: true, + environment: Some("default-env".into()), + ..Default::default() + }; + + let envelopes = test::with_captured_envelopes_options( + || { + let mut metric = test_metric("test"); + metric.attributes.insert( + "sentry.environment".into(), + LogAttribute(Value::from("custom-env")), + ); + Hub::current().capture_metric(metric); + }, + options, + ); + let metric = extract_single_metric(envelopes).expect("expected a single-metric envelope"); + + let expected_attributes = [ + // Check the environment is the one set directly on the metric + ("sentry.environment", "custom-env"), + // The other default attributes also stay + ("sentry.sdk.name", "sentry.rust"), + ("sentry.sdk.version", env!("CARGO_PKG_VERSION")), + ] + .into_iter() + .map(|(attribute, value)| (attribute.into(), value.into())) + .collect(); + + assert_eq!(metric.attributes, expected_attributes); +} + /// Returns a [`Metric`] with [type `Counter`](MetricType), /// the provided name, and a value of `1.0`. fn test_metric(name: S) -> Metric @@ -309,6 +396,26 @@ where Hub::current().capture_metric(test_metric(name)) } +/// Helper to extract the single metric from a list of captured envelopes. +/// +/// Asserts that the envelope contains only a single item, which contains only +/// a single metrics item, and returns that metrics item, or an error if failed. +fn extract_single_metric(envelopes: I) -> Result +where + I: IntoIterator, +{ + envelopes + .try_into_only_item() + .context("expected exactly one envelope")? + .into_items() + .try_into_only_item() + .context("expected exactly one item")? + .into_metrics() + .context("expected a metrics item")? + .try_into_only_item() + .context("expected exactly one metric") +} + /// Extension trait for iterators allowing conversion to only item. trait TryIntoOnlyElementExt { type Item;