Skip to content

Commit d250b0b

Browse files
authored
feat(spanv2): Double write to legacy attributes for backwards compatibility (#5490)
1 parent 8beee3b commit d250b0b

File tree

6 files changed

+286
-7
lines changed

6 files changed

+286
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
**Features**:
6+
7+
- Double write to legacy attributes for backwards compatibility. ([#5490](https://github.com/getsentry/relay/pull/5490))
8+
39
## 25.12.0
410

511
**Features**:

relay-conventions/src/consts.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ convention_attributes!(
6565
RPC_SERVICE => "rpc.service",
6666
SEGMENT_ID => "sentry.segment.id",
6767
SEGMENT_NAME => "sentry.segment.name",
68+
SENTRY_ACTION => "sentry.action",
69+
SENTRY_DOMAIN => "sentry.domain",
70+
SENTRY_GROUP => "sentry.group",
71+
SENTRY_NORMALIZED_DESCRIPTION => "sentry.normalized_description",
6872
SERVER_ADDRESS => "server.address",
6973
SPAN_KIND => "sentry.kind",
7074
STATUS_MESSAGE => "sentry.status.message",
@@ -83,6 +87,11 @@ convention_attributes!(
8387
///
8488
/// Really do not add to this list, at all, ever. The only reason this opt-out even exists to make a
8589
/// transition easier for attributes which Relay already uses but aren't yet in conventions.
86-
mod not_yet_defined {}
87-
#[expect(unused, reason = "no special attributes at the moment")]
90+
mod not_yet_defined {
91+
// The legacy http request method attribute used by transactions spans.
92+
// Could not be added to sentry conventions at the time due to an attribute naming conflict that
93+
// requires updating the sentry conventions code gen.
94+
// TODO: replace with conventions defined attribute name once the conventions code gen is updated.
95+
pub const LEGACY_HTTP_REQUEST_METHOD: &str = "http.request_method";
96+
}
8897
pub use self::not_yet_defined::*;

relay-event-normalization/src/eap/mod.rs

Lines changed: 258 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,22 +425,32 @@ fn normalize_http_attributes(
425425

426426
// Skip normalization if not an http span.
427427
// This is equivalent to conditionally scrubbing by span category in the V1 pipeline.
428-
if !attributes.contains_key(HTTP_REQUEST_METHOD) {
428+
if !attributes.contains_key(HTTP_REQUEST_METHOD)
429+
&& !attributes.contains_key(LEGACY_HTTP_REQUEST_METHOD)
430+
{
429431
return;
430432
}
431433

432434
let op = attributes.get_value(OP).and_then(|v| v.as_str());
433435

434436
let method = attributes
435437
.get_value(HTTP_REQUEST_METHOD)
438+
.or_else(|| attributes.get_value(LEGACY_HTTP_REQUEST_METHOD))
436439
.and_then(|v| v.as_str());
437440

438441
let server_address = attributes
439442
.get_value(SERVER_ADDRESS)
440443
.and_then(|v| v.as_str());
441444

442-
let url = attributes.get_value(URL_FULL).and_then(|v| v.as_str());
443-
445+
let url: Option<&str> = attributes
446+
.get_value(URL_FULL)
447+
.and_then(|v| v.as_str())
448+
.or_else(|| {
449+
attributes
450+
.get_value(DESCRIPTION)
451+
.and_then(|v| v.as_str())
452+
.and_then(|description| description.split_once(' ').map(|(_, url)| url))
453+
});
444454
let url_scheme = attributes.get_value(URL_SCHEME).and_then(|v| v.as_str());
445455

446456
// If the span op is "http.client" and the method and url are present,
@@ -452,7 +462,7 @@ fn normalize_http_attributes(
452462
.and_then(|scrubbed_http| domain_from_scrubbed_http(&scrubbed_http));
453463

454464
if let Some(domain) = domain_from_scrubbed_http {
455-
(Some(domain), None)
465+
(Some(domain), url.map(String::from))
456466
} else {
457467
domain_from_server_address(server_address, url_scheme)
458468
}
@@ -477,6 +487,40 @@ fn normalize_http_attributes(
477487
}
478488
}
479489

490+
/// Double writes sentry conventions attributes into legacy attributes.
491+
///
492+
/// This achieves backwards compatibility as it allows products to continue using legacy attributes
493+
/// while we accumulate spans that conform to sentry conventions.
494+
///
495+
/// This function is called after attribute value normalization (`normalize_attribute_values`) as it
496+
/// clones normalized attributes into legacy attributes.
497+
pub fn write_legacy_attributes(attributes: &mut Annotated<Attributes>) {
498+
let Some(attributes) = attributes.value_mut() else {
499+
return;
500+
};
501+
502+
// Map of new sentry conventions attributes to legacy SpanV1 attributes
503+
let current_to_legacy_attributes = [
504+
// DB attributes
505+
(NORMALIZED_DB_QUERY, SENTRY_NORMALIZED_DESCRIPTION),
506+
(NORMALIZED_DB_QUERY_HASH, SENTRY_GROUP),
507+
(DB_OPERATION_NAME, SENTRY_ACTION),
508+
(DB_COLLECTION_NAME, SENTRY_DOMAIN),
509+
// HTTP attributes
510+
(SERVER_ADDRESS, SENTRY_DOMAIN),
511+
(HTTP_REQUEST_METHOD, SENTRY_ACTION),
512+
];
513+
514+
for (current_attribute, legacy_attribute) in current_to_legacy_attributes {
515+
if attributes.contains_key(current_attribute) {
516+
let Some(attr) = attributes.get_attribute(current_attribute) else {
517+
continue;
518+
};
519+
attributes.insert(legacy_attribute, attr.value.clone());
520+
}
521+
}
522+
}
523+
480524
#[cfg(test)]
481525
mod tests {
482526
use relay_protocol::SerializableAnnotated;
@@ -1346,4 +1390,214 @@ mod tests {
13461390
}
13471391
"#);
13481392
}
1393+
1394+
#[test]
1395+
fn test_normalize_db_attributes_from_legacy_attributes() {
1396+
let mut attributes = Annotated::<Attributes>::from_json(
1397+
r#"
1398+
{
1399+
"sentry.op": {
1400+
"type": "string",
1401+
"value": "db"
1402+
},
1403+
"db.system.name": {
1404+
"type": "string",
1405+
"value": "mongodb"
1406+
},
1407+
"sentry.description": {
1408+
"type": "string",
1409+
"value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1410+
},
1411+
"db.operation.name": {
1412+
"type": "string",
1413+
"value": "find"
1414+
},
1415+
"db.collection.name": {
1416+
"type": "string",
1417+
"value": "documents"
1418+
}
1419+
}
1420+
"#,
1421+
)
1422+
.unwrap();
1423+
1424+
normalize_db_attributes(&mut attributes);
1425+
1426+
insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1427+
{
1428+
"db.collection.name": {
1429+
"type": "string",
1430+
"value": "documents"
1431+
},
1432+
"db.operation.name": {
1433+
"type": "string",
1434+
"value": "FIND"
1435+
},
1436+
"db.system.name": {
1437+
"type": "string",
1438+
"value": "mongodb"
1439+
},
1440+
"sentry.description": {
1441+
"type": "string",
1442+
"value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1443+
},
1444+
"sentry.normalized_db_query": {
1445+
"type": "string",
1446+
"value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1447+
},
1448+
"sentry.normalized_db_query.hash": {
1449+
"type": "string",
1450+
"value": "aedc5c7e8cec726b"
1451+
},
1452+
"sentry.op": {
1453+
"type": "string",
1454+
"value": "db"
1455+
}
1456+
}
1457+
"#);
1458+
}
1459+
1460+
#[test]
1461+
fn test_normalize_http_attributes_from_legacy_attributes() {
1462+
let mut attributes = Annotated::<Attributes>::from_json(
1463+
r#"
1464+
{
1465+
"sentry.op": {
1466+
"type": "string",
1467+
"value": "http.client"
1468+
},
1469+
"http.request_method": {
1470+
"type": "string",
1471+
"value": "GET"
1472+
},
1473+
"sentry.description": {
1474+
"type": "string",
1475+
"value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
1476+
}
1477+
}
1478+
"#,
1479+
)
1480+
.unwrap();
1481+
1482+
normalize_http_attributes(&mut attributes, &[]);
1483+
1484+
insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1485+
{
1486+
"http.request.method": {
1487+
"type": "string",
1488+
"value": "GET"
1489+
},
1490+
"http.request_method": {
1491+
"type": "string",
1492+
"value": "GET"
1493+
},
1494+
"sentry.description": {
1495+
"type": "string",
1496+
"value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
1497+
},
1498+
"sentry.op": {
1499+
"type": "string",
1500+
"value": "http.client"
1501+
},
1502+
"server.address": {
1503+
"type": "string",
1504+
"value": "*.xn--85x722f.xn--55qx5d.cn"
1505+
},
1506+
"url.full": {
1507+
"type": "string",
1508+
"value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1509+
}
1510+
}
1511+
"#);
1512+
}
1513+
1514+
#[test]
1515+
fn test_write_legacy_attributes() {
1516+
let mut attributes = Annotated::<Attributes>::from_json(
1517+
r#"
1518+
{
1519+
"db.collection.name": {
1520+
"type": "string",
1521+
"value": "documents"
1522+
},
1523+
"db.operation.name": {
1524+
"type": "string",
1525+
"value": "FIND"
1526+
},
1527+
"db.query.text": {
1528+
"type": "string",
1529+
"value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1530+
},
1531+
"db.system.name": {
1532+
"type": "string",
1533+
"value": "mongodb"
1534+
},
1535+
"sentry.normalized_db_query": {
1536+
"type": "string",
1537+
"value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1538+
},
1539+
"sentry.normalized_db_query.hash": {
1540+
"type": "string",
1541+
"value": "aedc5c7e8cec726b"
1542+
},
1543+
"sentry.op": {
1544+
"type": "string",
1545+
"value": "db"
1546+
}
1547+
}
1548+
"#,
1549+
)
1550+
.unwrap();
1551+
1552+
write_legacy_attributes(&mut attributes);
1553+
1554+
insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1555+
{
1556+
"db.collection.name": {
1557+
"type": "string",
1558+
"value": "documents"
1559+
},
1560+
"db.operation.name": {
1561+
"type": "string",
1562+
"value": "FIND"
1563+
},
1564+
"db.query.text": {
1565+
"type": "string",
1566+
"value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1567+
},
1568+
"db.system.name": {
1569+
"type": "string",
1570+
"value": "mongodb"
1571+
},
1572+
"sentry.action": {
1573+
"type": "string",
1574+
"value": "FIND"
1575+
},
1576+
"sentry.domain": {
1577+
"type": "string",
1578+
"value": "documents"
1579+
},
1580+
"sentry.group": {
1581+
"type": "string",
1582+
"value": "aedc5c7e8cec726b"
1583+
},
1584+
"sentry.normalized_db_query": {
1585+
"type": "string",
1586+
"value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1587+
},
1588+
"sentry.normalized_db_query.hash": {
1589+
"type": "string",
1590+
"value": "aedc5c7e8cec726b"
1591+
},
1592+
"sentry.normalized_description": {
1593+
"type": "string",
1594+
"value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1595+
},
1596+
"sentry.op": {
1597+
"type": "string",
1598+
"value": "db"
1599+
}
1600+
}
1601+
"#);
1602+
}
13491603
}

relay-server/src/processing/spans/process.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ fn normalize_span(
177177
}
178178
eap::normalize_ai(&mut span.attributes, duration, model_costs);
179179
eap::normalize_attribute_values(&mut span.attributes, allowed_hosts);
180+
eap::write_legacy_attributes(&mut span.attributes);
180181
};
181182

182183
process_value(span, &mut TrimmingProcessor::new(), ProcessingState::root())?;

tests/integration/test_spansv2.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,19 +1092,26 @@ def test_spansv2_attribute_normalization(
10921092
"sentry.op": {"type": "string", "value": "db"},
10931093
"db.system.name": {"type": "string", "value": "mysql"},
10941094
"db.operation.name": {"type": "string", "value": "SELECT"},
1095+
"sentry.action": {"type": "string", "value": "SELECT"},
10951096
"db.query.text": {
10961097
"type": "string",
10971098
"value": "SELECT id FROM users WHERE id = 1 AND name = 'Test'",
10981099
},
10991100
"db.collection.name": {"type": "string", "value": "users"},
1101+
"sentry.domain": {"type": "string", "value": "users"},
11001102
"sentry.normalized_db_query": {
11011103
"type": "string",
11021104
"value": "SELECT id FROM users WHERE id = %s AND name = %s",
11031105
},
1106+
"sentry.normalized_description": {
1107+
"type": "string",
1108+
"value": "SELECT id FROM users WHERE id = %s AND name = %s",
1109+
},
11041110
"sentry.normalized_db_query.hash": {
11051111
"type": "string",
11061112
"value": "f79af0ba3d26284c",
11071113
},
1114+
"sentry.group": {"type": "string", "value": "f79af0ba3d26284c"},
11081115
"sentry.observed_timestamp_nanos": {
11091116
"type": "string",
11101117
"value": time_within(ts, expect_resolution="ns"),
@@ -1126,7 +1133,9 @@ def test_spansv2_attribute_normalization(
11261133
"value": time_within(ts, expect_resolution="ns"),
11271134
},
11281135
"http.request.method": {"type": "string", "value": "GET"},
1136+
"sentry.action": {"type": "string", "value": "GET"},
11291137
"server.address": {"type": "string", "value": "*.service.io"},
1138+
"sentry.domain": {"type": "string", "value": "*.service.io"},
11301139
"url.full": {
11311140
"type": "string",
11321141
"value": "https://www.service.io/users/01234-qwerty/settings/98765-adfghj",

0 commit comments

Comments
 (0)