diff --git a/relay-server/src/metrics_extraction/spans/mod.rs b/relay-server/src/metrics_extraction/spans/mod.rs index 32b758ee5f..9265c7a4f2 100644 --- a/relay-server/src/metrics_extraction/spans/mod.rs +++ b/relay-server/src/metrics_extraction/spans/mod.rs @@ -1,19 +1,23 @@ +use crate::metrics_extraction::spans::types::{SpanMetric, SpanTagKey}; use crate::metrics_extraction::transactions::types::ExtractMetricsError; use crate::metrics_extraction::utils::extract_http_status_code; use crate::metrics_extraction::utils::http_status_code_from_span; use crate::metrics_extraction::utils::{ extract_transaction_op, get_eventuser_tag, get_trace_context, }; +use crate::metrics_extraction::IntoMetric; use itertools::Itertools; use once_cell::sync::Lazy; use regex::Regex; -use relay_common::{DurationUnit, EventType, MetricUnit, UnixTimestamp}; +use relay_common::{EventType, UnixTimestamp}; use relay_filter::csp::SchemeDomainPort; use relay_general::protocol::Event; use relay_general::types::{Annotated, Value}; -use relay_metrics::{AggregatorConfig, Metric, MetricNamespace, MetricValue}; +use relay_metrics::{AggregatorConfig, Metric}; use std::collections::BTreeMap; +mod types; + /// Extracts metrics from the spans of the given transaction, and sets common /// tags for all the metrics and spans. If a span already contains a tag /// extracted for a metric, the tag value is overwritten. @@ -49,22 +53,22 @@ pub(crate) fn extract_span_metrics( let mut databag = BTreeMap::new(); if let Some(release) = event.release.as_str() { - databag.insert("release".to_owned(), release.to_owned()); + databag.insert(SpanTagKey::Release, release.to_owned()); } if let Some(user) = event.user.value().and_then(get_eventuser_tag) { - databag.insert("user".to_owned(), user); + databag.insert(SpanTagKey::User, user); } // Collect the shared tags for all the metrics and spans on this transaction. let mut shared_tags = BTreeMap::new(); if let Some(environment) = event.environment.as_str() { - shared_tags.insert("environment".to_owned(), environment.to_owned()); + shared_tags.insert(SpanTagKey::Environment, environment.to_owned()); } if let Some(transaction_name) = event.transaction.value() { - shared_tags.insert("transaction".to_owned(), transaction_name.to_owned()); + shared_tags.insert(SpanTagKey::Transaction, transaction_name.to_owned()); let transaction_method_from_request = event .request @@ -75,18 +79,18 @@ pub(crate) fn extract_span_metrics( if let Some(transaction_method) = transaction_method_from_request .or(http_method_from_transaction_name(transaction_name).map(|m| m.to_uppercase())) { - shared_tags.insert("transaction.method".to_owned(), transaction_method); + shared_tags.insert(SpanTagKey::TransactionMethod, transaction_method); } } if let Some(trace_context) = get_trace_context(event) { if let Some(op) = extract_transaction_op(trace_context) { - shared_tags.insert("transaction.op".to_owned(), op.to_lowercase()); + shared_tags.insert(SpanTagKey::TransactionOp, op.to_lowercase()); } } if let Some(transaction_http_status_code) = extract_http_status_code(event) { - shared_tags.insert("http.status_code".to_owned(), transaction_http_status_code); + shared_tags.insert(SpanTagKey::HttpStatusCode, transaction_http_status_code); } let Some(spans) = event.spans.value_mut() else { return Ok(()) }; @@ -101,23 +105,20 @@ pub(crate) fn extract_span_metrics( .and_then(|data| data.get("description.scrubbed")) .and_then(|value| value.as_str()) { - span_tags.insert( - "span.description".to_owned(), - scrubbed_description.to_owned(), - ); + span_tags.insert(SpanTagKey::Description, scrubbed_description.to_owned()); let mut span_group = format!("{:?}", md5::compute(scrubbed_description)); span_group.truncate(16); - span_tags.insert("span.group".to_owned(), span_group); + span_tags.insert(SpanTagKey::Group, span_group); } if let Some(unsanitized_span_op) = span.op.value() { let span_op = unsanitized_span_op.to_owned().to_lowercase(); - span_tags.insert("span.op".to_owned(), span_op.to_owned()); + span_tags.insert(SpanTagKey::SpanOp, span_op.to_owned()); if let Some(category) = span_op_to_category(&span_op) { - span_tags.insert("span.category".to_owned(), category.to_owned()); + span_tags.insert(SpanTagKey::Category, category.to_owned()); } let span_module = if span_op.starts_with("http") { @@ -131,7 +132,7 @@ pub(crate) fn extract_span_metrics( }; if let Some(module) = span_module { - span_tags.insert("span.module".to_owned(), module.to_owned()); + span_tags.insert(SpanTagKey::Module, module.to_owned()); } // TODO(iker): we're relying on the existance of `http.method` @@ -163,7 +164,7 @@ pub(crate) fn extract_span_metrics( }; if let Some(act) = action { - span_tags.insert("span.action".to_owned(), act); + span_tags.insert(SpanTagKey::Action, act); } let domain = if span_op == "http.client" { @@ -181,7 +182,7 @@ pub(crate) fn extract_span_metrics( }; if let Some(dom) = domain { - span_tags.insert("span.domain".to_owned(), dom.to_owned()); + span_tags.insert(SpanTagKey::Domain, dom.to_owned()); } } @@ -191,15 +192,15 @@ pub(crate) fn extract_span_metrics( .and_then(|v| v.get("db.system")) .and_then(|system| system.as_str()); if let Some(sys) = system { - span_tags.insert("span.system".to_owned(), sys.to_lowercase()); + span_tags.insert(SpanTagKey::System, sys.to_lowercase()); } if let Some(span_status) = span.status.value() { - span_tags.insert("span.status".to_owned(), span_status.to_string()); + span_tags.insert(SpanTagKey::Status, span_status.to_string()); } if let Some(status_code) = http_status_code_from_span(span) { - span_tags.insert("span.status_code".to_owned(), status_code); + span_tags.insert(SpanTagKey::StatusCode, status_code); } // Even if we emit metrics, we want this info to be duplicated in every span. @@ -207,25 +208,24 @@ pub(crate) fn extract_span_metrics( let it = span_tags .clone() .into_iter() - .map(|(k, v)| (k, Annotated::new(Value::String(v)))); + .map(|(k, v)| (k.to_string(), Annotated::new(Value::String(v)))); it.chain( databag .clone() .into_iter() - .map(|(k, v)| (k, Annotated::new(Value::String(v)))), + .map(|(k, v)| (k.to_string(), Annotated::new(Value::String(v)))), ) }); if let Some(user) = event.user.value() { - if let Some(value) = get_eventuser_tag(user) { - metrics.push(Metric::new_mri( - MetricNamespace::Spans, - "user", - MetricUnit::None, - MetricValue::set_from_str(&value), - timestamp, - span_tags.clone(), - )); + if let Some(user_tag) = get_eventuser_tag(user) { + metrics.push( + SpanMetric::User { + value: user_tag, + tags: span_tags.clone(), + } + .into_metric(timestamp), + ); } } @@ -233,14 +233,13 @@ pub(crate) fn extract_span_metrics( // NOTE(iker): this exclusive time doesn't consider all cases, // such as sub-transactions. We accept these limitations for // now. - metrics.push(Metric::new_mri( - MetricNamespace::Spans, - "exclusive_time", - MetricUnit::Duration(DurationUnit::MilliSecond), - MetricValue::Distribution(*exclusive_time), - timestamp, - span_tags.clone(), - )); + metrics.push( + SpanMetric::ExclusiveTime { + value: *exclusive_time, + tags: span_tags.clone(), + } + .into_metric(timestamp), + ); } if let (Some(&span_start), Some(&span_end)) = @@ -248,16 +247,13 @@ pub(crate) fn extract_span_metrics( { // The `duration` of a span. This metric also serves as the // counter metric `throughput`. - metrics.push(Metric::new_mri( - MetricNamespace::Spans, - "duration", - MetricUnit::Duration(DurationUnit::MilliSecond), - MetricValue::Distribution(relay_common::chrono_to_positive_millis( - span_end - span_start, - )), - timestamp, - span_tags.clone(), - )); + metrics.push( + SpanMetric::Duration { + value: span_end - span_start, + tags: span_tags.clone(), + } + .into_metric(timestamp), + ); }; } } diff --git a/relay-server/src/metrics_extraction/spans/types.rs b/relay-server/src/metrics_extraction/spans/types.rs new file mode 100644 index 0000000000..dd3e3002e1 --- /dev/null +++ b/relay-server/src/metrics_extraction/spans/types.rs @@ -0,0 +1,155 @@ +use std::collections::BTreeMap; +use std::fmt::Display; + +use chrono::Duration; +use relay_common::{DurationUnit, MetricUnit, UnixTimestamp}; +use relay_metrics::{Metric, MetricNamespace, MetricValue}; + +use crate::metrics_extraction::IntoMetric; + +#[derive(Clone, Debug)] +pub(crate) enum SpanMetric { + User { + value: String, + tags: BTreeMap, + }, + Duration { + value: Duration, + tags: BTreeMap, + }, + ExclusiveTime { + value: f64, + tags: BTreeMap, + }, +} + +impl IntoMetric for SpanMetric { + fn into_metric(self, timestamp: UnixTimestamp) -> Metric { + let namespace = MetricNamespace::Spans; + + match self { + SpanMetric::User { value, tags } => Metric::new_mri( + namespace, + "user", + MetricUnit::None, + MetricValue::set_from_str(&value), + timestamp, + span_tag_mapping_to_string_mapping(tags), + ), + SpanMetric::Duration { value, tags } => Metric::new_mri( + namespace, + "duration", + MetricUnit::Duration(DurationUnit::MilliSecond), + MetricValue::Distribution(relay_common::chrono_to_positive_millis(value)), + timestamp, + span_tag_mapping_to_string_mapping(tags), + ), + SpanMetric::ExclusiveTime { value, tags } => Metric::new_mri( + namespace, + "exclusive_time", + MetricUnit::Duration(DurationUnit::MilliSecond), + MetricValue::Distribution(value), + timestamp, + span_tag_mapping_to_string_mapping(tags), + ), + } + } +} + +/// Returns the same mapping of tags, with the tag keys as strings. +/// +/// Rust doesn't allow to add implementations on foreign methods (BTreeMap), so +/// this method is a workaround to parse [`SpanTagKey`]s to strings to strings. +fn span_tag_mapping_to_string_mapping( + tags: BTreeMap, +) -> BTreeMap { + tags.into_iter().map(|(k, v)| (k.to_string(), v)).collect() +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum SpanTagKey { + // Specific to a transaction + Release, + User, + Environment, + Transaction, + TransactionMethod, + TransactionOp, + HttpStatusCode, + + // Specific to spans + Description, + Group, + SpanOp, + Category, + Module, + Action, + Domain, + System, + Status, + StatusCode, +} + +impl Display for SpanTagKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self { + SpanTagKey::Release => "release", + SpanTagKey::User => "user", + SpanTagKey::Environment => "environment", + SpanTagKey::Transaction => "transaction", + SpanTagKey::TransactionMethod => "transaction.method", + SpanTagKey::TransactionOp => "transaction.op", + SpanTagKey::HttpStatusCode => "http.status_code", + + SpanTagKey::Description => "span.description", + SpanTagKey::Group => "span.group", + SpanTagKey::SpanOp => "span.op", + SpanTagKey::Category => "span.category", + SpanTagKey::Module => "span.module", + SpanTagKey::Action => "span.action", + SpanTagKey::Domain => "span.domain", + SpanTagKey::System => "span.system", + SpanTagKey::Status => "span.status", + SpanTagKey::StatusCode => "span.status_code", + }; + write!(f, "{name}") + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use chrono::{TimeZone, Utc}; + use relay_common::UnixTimestamp; + + use crate::metrics_extraction::spans::types::{SpanMetric, SpanTagKey}; + use crate::metrics_extraction::IntoMetric; + + #[test] + fn test_span_metric_conversion() { + let timestamp = + UnixTimestamp::from_datetime(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap()) + .unwrap(); + + let tags = BTreeMap::from([(SpanTagKey::Release, "1.2.3".to_owned())]); + let metric = SpanMetric::User { + value: "usertag".to_owned(), + tags, + }; + let converted = metric.into_metric(timestamp); + + insta::assert_debug_snapshot!(converted, @r#" + Metric { + name: "s:spans/user@none", + value: Set( + 1473472266, + ), + timestamp: UnixTimestamp(946684800), + tags: { + "release": "1.2.3", + }, + } + "#); + } +}