Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions sentry-core/src/client.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<T: Into<ClientOptions>> From<T> for Client {
fn from(o: T) -> Client {
Expand Down Expand Up @@ -65,6 +67,8 @@ pub struct Client {
metrics_batcher: RwLock<Option<Batcher<Metric>>>,
#[cfg(feature = "logs")]
default_log_attributes: Option<BTreeMap<String, LogAttribute>>,
#[cfg(feature = "metrics")]
default_metric_attributes: BTreeMap<Cow<'static, str>, LogAttribute>,
integrations: Vec<(TypeId, Arc<dyn Integration>)>,
pub(crate) sdk_info: ClientSdkInfo,
}
Expand Down Expand Up @@ -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(),
}
Expand Down Expand Up @@ -208,13 +214,18 @@ impl Client {
metrics_batcher,
#[cfg(feature = "logs")]
default_log_attributes: None,
#[cfg(feature = "metrics")]
default_metric_attributes: Default::default(),
integrations,
sdk_info,
};

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

#[cfg(feature = "metrics")]
client.cache_default_metric_attributes();

client
}

Expand Down Expand Up @@ -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<I>(&self) -> Option<&I>
where
I: Integration,
Expand Down Expand Up @@ -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<Metric> {
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)
}
}
Expand Down
111 changes: 109 additions & 2 deletions sentry-core/tests/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<S>(name: S) -> Metric
Expand All @@ -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<I>(envelopes: I) -> Result<Metric>
where
I: IntoIterator<Item = Envelope>,
{
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<I> {
type Item;
Expand Down
Loading