Skip to content

Commit

Permalink
ref(spans): Introduce types for span metrics and tags (#2224)
Browse files Browse the repository at this point in the history
Span metrics and types were initially introduced as a hard-coded way of
directly typing strings. To avoid mistyping in the future, this PR
introduces types so that there's no need to write strings. The approach
it's pretty much the same as with transaction metrics, but a bit simpler
due to less strict requirements.
  • Loading branch information
iker-barriocanal committed Jun 20, 2023
1 parent cedfbed commit dcad1bd
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 51 deletions.
98 changes: 47 additions & 51 deletions relay-server/src/metrics_extraction/spans/mod.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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(()) };
Expand All @@ -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") {
Expand All @@ -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`
Expand Down Expand Up @@ -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" {
Expand All @@ -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());
}
}

Expand All @@ -191,73 +192,68 @@ 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.
span.data.get_or_insert_with(BTreeMap::new).extend({
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),
);
}
}

if let Some(exclusive_time) = span.exclusive_time.value() {
// 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)) =
(span.start_timestamp.value(), span.timestamp.value())
{
// 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),
);
};
}
}
Expand Down
155 changes: 155 additions & 0 deletions relay-server/src/metrics_extraction/spans/types.rs
Original file line number Diff line number Diff line change
@@ -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<SpanTagKey, String>,
},
Duration {
value: Duration,
tags: BTreeMap<SpanTagKey, String>,
},
ExclusiveTime {
value: f64,
tags: BTreeMap<SpanTagKey, String>,
},
}

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<SpanTagKey, String>,
) -> BTreeMap<String, String> {
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",
},
}
"#);
}
}

0 comments on commit dcad1bd

Please sign in to comment.