Skip to content

Commit

Permalink
feat(spans): Infer span.op if not defined (#4056)
Browse files Browse the repository at this point in the history
Add global option that allows inferring the span op, with rules like
this:

```json
{
    "span_op_defaults": {
       "rules": [{
            "condition": {
                "op": "not",
                "inner": {
                    "op": "eq",
                    "name": "span.data.messaging\\.system",
                    "value": null,
                },
            },
            "value": "message"
        }]
    }
}
```
  • Loading branch information
jjbayer authored Sep 24, 2024
1 parent e09b7e1 commit 3973629
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

- Add a config option to add default tags to all Relay Sentry events. ([#3944](https://github.com/getsentry/relay/pull/3944))
- Automatically derive `client.address` and `user.geo` for standalone spans. ([#4047](https://github.com/getsentry/relay/pull/4047))
- Configurable span.op inference. ([#4056](https://github.com/getsentry/relay/pull/4056))

**Internal:**

Expand Down
4 changes: 4 additions & 0 deletions py/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Add `spanOpDefaults` to global config. ([#4056](https://github.com/getsentry/relay/pull/4056))

## 0.9.1

- Add REPLAY_VIDEO entry to DataCategory. ([#3847](https://github.com/getsentry/relay/pull/3847))
Expand Down
1 change: 1 addition & 0 deletions relay-cabi/src/processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ pub unsafe extern "C" fn relay_store_normalizer_normalize_event(
replay_id: config.replay_id,
span_allowed_hosts: &[], // only supported in relay
scrub_mongo_description: ScrubMongoDescription::Disabled, // only supported in relay
span_op_defaults: Default::default(), // only supported in relay
};
normalize_event(&mut event, &normalization_config);

Expand Down
11 changes: 9 additions & 2 deletions relay-dynamic-config/src/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::io::BufReader;
use std::path::Path;

use relay_base_schema::metrics::MetricNamespace;
use relay_event_normalization::{MeasurementsConfig, ModelCosts};
use relay_event_normalization::{MeasurementsConfig, ModelCosts, SpanOpDefaults};
use relay_filter::GenericFiltersConfig;
use relay_quotas::Quota;
use serde::{de, Deserialize, Serialize};
Expand Down Expand Up @@ -49,6 +49,13 @@ pub struct GlobalConfig {
/// Configuration for AI span measurements.
#[serde(skip_serializing_if = "is_missing")]
pub ai_model_costs: ErrorBoundary<ModelCosts>,

/// Configuration to derive the `span.op` from other span fields.
#[serde(
deserialize_with = "default_on_error",
skip_serializing_if = "is_default"
)]
pub span_op_defaults: SpanOpDefaults,
}

impl GlobalConfig {
Expand Down Expand Up @@ -337,7 +344,7 @@ pub enum BucketEncoding {

/// Returns `true` if this value is equal to `Default::default()`.
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
*t == T::default()
t == &T::default()
}

fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
Expand Down
16 changes: 11 additions & 5 deletions relay-event-normalization/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ use crate::span::tag_extraction::extract_span_tags_from_event;
use crate::utils::{self, get_event_user_tag, MAX_DURATION_MOBILE_MS};
use crate::{
breakdowns, event_error, legacy, mechanism, remove_other, schema, span, stacktrace,
transactions, trimming, user_agent, BreakdownsConfig, CombinedMeasurementsConfig, GeoIpLookup,
MaxChars, ModelCosts, PerformanceScoreConfig, RawUserAgentInfo, SpanDescriptionRule,
TransactionNameConfig,
transactions, trimming, user_agent, BorrowedSpanOpDefaults, BreakdownsConfig,
CombinedMeasurementsConfig, GeoIpLookup, MaxChars, ModelCosts, PerformanceScoreConfig,
RawUserAgentInfo, SpanDescriptionRule, TransactionNameConfig,
};

/// Configuration for [`normalize_event`].
Expand Down Expand Up @@ -161,6 +161,9 @@ pub struct NormalizationConfig<'a> {

/// Controls whether or not MongoDB span descriptions will be scrubbed.
pub scrub_mongo_description: ScrubMongoDescription,

/// Rules to infer `span.op` from other span fields.
pub span_op_defaults: BorrowedSpanOpDefaults<'a>,
}

impl<'a> Default for NormalizationConfig<'a> {
Expand Down Expand Up @@ -194,6 +197,7 @@ impl<'a> Default for NormalizationConfig<'a> {
replay_id: Default::default(),
span_allowed_hosts: Default::default(),
scrub_mongo_description: ScrubMongoDescription::Disabled,
span_op_defaults: Default::default(),
}
}
}
Expand Down Expand Up @@ -244,8 +248,10 @@ fn normalize(event: &mut Event, meta: &mut Meta, config: &NormalizationConfig) {
// (internally noops for non-transaction events).
// TODO: Parts of this processor should probably be a filter so we
// can revert some changes to ProcessingAction)
let mut transactions_processor =
transactions::TransactionsProcessor::new(config.transaction_name_config.clone());
let mut transactions_processor = transactions::TransactionsProcessor::new(
config.transaction_name_config,
config.span_op_defaults,
);
let _ = transactions_processor.process_event(event, meta, ProcessingState::root());

// Process security reports first to ensure all props.
Expand Down
144 changes: 129 additions & 15 deletions relay-event-normalization/src/transactions/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ use relay_event_schema::processor::{
self, ProcessValue, ProcessingResult, ProcessingState, Processor,
};
use relay_event_schema::protocol::{Event, Span, SpanStatus, TraceContext, TransactionSource};
use relay_protocol::{Annotated, Meta, Remark, RemarkType};
use relay_protocol::{Annotated, Meta, Remark, RemarkType, RuleCondition};
use serde::{Deserialize, Serialize};

use crate::regexes::TRANSACTION_NAME_NORMALIZER_REGEX;
use crate::TransactionNameRule;

/// Configuration for sanitizing unparameterized transaction names.
#[derive(Clone, Debug, Default)]
#[derive(Clone, Copy, Debug, Default)]
pub struct TransactionNameConfig<'r> {
/// Rules for identifier replacement that were discovered by Sentry's transaction clusterer.
pub rules: &'r [TransactionNameRule],
Expand Down Expand Up @@ -76,12 +77,27 @@ pub fn apply_transaction_rename_rules(
#[derive(Debug, Default)]
pub struct TransactionsProcessor<'r> {
name_config: TransactionNameConfig<'r>,
span_op_defaults: BorrowedSpanOpDefaults<'r>,
}

impl<'r> TransactionsProcessor<'r> {
/// Creates a new `TransactionsProcessor` instance.
pub fn new(name_config: TransactionNameConfig<'r>) -> Self {
Self { name_config }
pub fn new(
name_config: TransactionNameConfig<'r>,
span_op_defaults: BorrowedSpanOpDefaults<'r>,
) -> Self {
Self {
name_config,
span_op_defaults,
}
}

#[cfg(test)]
fn new_name_config(name_config: TransactionNameConfig<'r>) -> Self {
Self {
name_config,
..Default::default()
}
}

/// Returns `true` if the given transaction name should be treated as a URL.
Expand Down Expand Up @@ -163,14 +179,61 @@ impl Processor for TransactionsProcessor<'_> {
_meta: &mut Meta,
state: &ProcessingState<'_>,
) -> ProcessingResult {
span.op.get_or_insert_with(|| "default".to_owned());

if span.op.value().is_none() {
*span.op.value_mut() = Some(self.span_op_defaults.infer(span));
}
span.process_child_values(self, state)?;

Ok(())
}
}

/// Rules used to infer `span.op` from other span fields.
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct SpanOpDefaults {
/// List of rules to apply. First match wins.
pub rules: Vec<SpanOpDefaultRule>,
}

impl SpanOpDefaults {
/// Gets a borrowed version of this config.
pub fn borrow(&self) -> BorrowedSpanOpDefaults {
BorrowedSpanOpDefaults {
rules: self.rules.as_slice(),
}
}
}

/// Borrowed version of [`SpanOpDefaults`].
#[derive(Clone, Copy, Debug, Default)]
pub struct BorrowedSpanOpDefaults<'a> {
rules: &'a [SpanOpDefaultRule],
}

impl BorrowedSpanOpDefaults<'_> {
/// Infer the span op from a set of rules.
///
/// The first matching rule determines the span op.
/// If no rule matches, `"default"` is returned.
fn infer(&self, span: &Span) -> String {
for rule in self.rules {
if rule.condition.matches(span) {
return rule.value.clone();
}
}
"default".to_owned()
}
}

/// A rule to infer [`Span::op`] from other span fields.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct SpanOpDefaultRule {
/// When to set the given value.
pub condition: RuleCondition,
/// Value for the [`Span::op`]. Only set if omitted by the SDK.
pub value: String,
}

/// Span status codes for the Ruby Rack integration that indicate raw URLs being sent as
/// transaction names. These cases are considered as high-cardinality.
///
Expand Down Expand Up @@ -336,14 +399,14 @@ fn scrub_identifiers_with_regex(

#[cfg(test)]
mod tests {

use chrono::{Duration, TimeZone, Utc};
use insta::assert_debug_snapshot;
use itertools::Itertools;
use relay_common::glob2::LazyGlob;
use relay_event_schema::processor::process_value;
use relay_event_schema::protocol::{ClientSdkInfo, Contexts, SpanId, TraceId};
use relay_protocol::{assert_annotated_snapshot, get_value};
use serde_json::json;

use crate::validation::validate_event;
use crate::{EventValidationConfig, RedactionRule};
Expand Down Expand Up @@ -911,7 +974,7 @@ mod tests {

process_value(
&mut event,
&mut TransactionsProcessor::new(TransactionNameConfig {
&mut TransactionsProcessor::new_name_config(TransactionNameConfig {
rules: &[rule1, rule2, rule3],
}),
ProcessingState::root(),
Expand Down Expand Up @@ -992,7 +1055,7 @@ mod tests {

process_value(
&mut event,
&mut TransactionsProcessor::new(TransactionNameConfig {
&mut TransactionsProcessor::new_name_config(TransactionNameConfig {
rules: &[rule1, rule2, rule3],
}),
ProcessingState::root(),
Expand Down Expand Up @@ -1059,7 +1122,7 @@ mod tests {

let mut event = Annotated::<Event>::from_json(json).unwrap();

let mut processor = TransactionsProcessor::new(TransactionNameConfig {
let mut processor = TransactionsProcessor::new_name_config(TransactionNameConfig {
rules: rules.as_ref(),
});
process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
Expand Down Expand Up @@ -1174,7 +1237,7 @@ mod tests {
// This must not normalize transaction name, since it's disabled.
process_value(
&mut event,
&mut TransactionsProcessor::new(TransactionNameConfig {
&mut TransactionsProcessor::new_name_config(TransactionNameConfig {
rules: rules.as_ref(),
}),
ProcessingState::root(),
Expand Down Expand Up @@ -1231,7 +1294,7 @@ mod tests {

process_value(
&mut event,
&mut TransactionsProcessor::new(TransactionNameConfig {
&mut TransactionsProcessor::new_name_config(TransactionNameConfig {
rules: rules.as_ref(),
}),
ProcessingState::root(),
Expand Down Expand Up @@ -1316,7 +1379,7 @@ mod tests {

process_value(
&mut event,
&mut TransactionsProcessor::new(TransactionNameConfig { rules: &[rule] }),
&mut TransactionsProcessor::new_name_config(TransactionNameConfig { rules: &[rule] }),
ProcessingState::root(),
)
.unwrap();
Expand Down Expand Up @@ -1521,7 +1584,7 @@ mod tests {

process_value(
&mut event,
&mut TransactionsProcessor::new(TransactionNameConfig {
&mut TransactionsProcessor::new_name_config(TransactionNameConfig {
rules: &[TransactionNameRule {
pattern: LazyGlob::new("/remains/*/1234567890/".to_owned()),
expiry: Utc.with_ymd_and_hms(3000, 1, 1, 1, 1, 1).unwrap(),
Expand Down Expand Up @@ -1587,7 +1650,7 @@ mod tests {

process_value(
&mut event,
&mut TransactionsProcessor::new(TransactionNameConfig {
&mut TransactionsProcessor::new_name_config(TransactionNameConfig {
rules: &[TransactionNameRule {
pattern: LazyGlob::new("/remains/*/**".to_owned()),
expiry: Utc.with_ymd_and_hms(3000, 1, 1, 1, 1, 1).unwrap(),
Expand Down Expand Up @@ -1627,4 +1690,55 @@ mod tests {
},
]"#);
}

#[test]
fn test_infer_span_op_default() {
let span = Annotated::from_json(r#"{}"#).unwrap();
let defaults: SpanOpDefaults = serde_json::from_value(json!({
"rules": [{
"condition": {
"op": "not",
"inner": {
"op": "eq",
"name": "span.data.messaging\\.system",
"value": null,
},
},
"value": "message"
}]
}
))
.unwrap();
let op = defaults.borrow().infer(span.value().unwrap());
assert_eq!(&op, "default");
}

#[test]
fn test_infer_span_op_messaging() {
let span = Annotated::from_json(
r#"{
"data": {
"messaging.system": "activemq"
}
}"#,
)
.unwrap();
let defaults: SpanOpDefaults = serde_json::from_value(json!({
"rules": [{
"condition": {
"op": "not",
"inner": {
"op": "eq",
"name": "span.data.messaging\\.system",
"value": null,
},
},
"value": "message"
}]
}
))
.unwrap();
let op = defaults.borrow().infer(span.value().unwrap());
assert_eq!(&op, "message");
}
}
1 change: 1 addition & 0 deletions relay-server/src/services/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,7 @@ impl EnvelopeProcessorService {
} else {
ScrubMongoDescription::Disabled
},
span_op_defaults: global_config.span_op_defaults.borrow(),
};

metric!(timer(RelayTimers::EventProcessingNormalization), {
Expand Down
Loading

0 comments on commit 3973629

Please sign in to comment.