Skip to content

Commit 6ff6961

Browse files
feat: Added compact mode for span links (#3681)
1 parent 430d1dd commit 6ff6961

File tree

6 files changed

+506
-372
lines changed

6 files changed

+506
-372
lines changed

lib/spans/span-event.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,14 @@ class SpanEvent {
318318
* @returns {SpanEvent|null} the span after applying the rules, or null if dropped
319319
*/
320320
applyPartialTraceRules({ isEntry, partialTrace }) {
321+
// in essential and compact mode - we need to remove the non intrinsic attributes from span links early in this process because there can be an entry span,
322+
// llm span exit span or span with no entity relationship attributes can have non intrinsics on it.
323+
if (partialTrace.type !== PARTIAL_TYPES.REDUCED) {
324+
for (const link of this.spanLinks) {
325+
link.removeNonIntrinsicsAttrs()
326+
}
327+
}
328+
321329
if (isEntry) {
322330
this.addIntrinsicAttribute('nr.pg', true)
323331
logger.trace('Span %s is an entry point, keeping span unchanged.', this.intrinsics.name)

lib/spans/span-link.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ class SpanLink {
8686
filterNulls(this.agentAttributes.get(DESTINATIONS.TRANS_SEGMENT))
8787
]
8888
}
89+
90+
removeNonIntrinsicsAttrs() {
91+
this.userAttributes = Object.create(null)
92+
this.agentAttributes = Object.create(null)
93+
}
8994
}
9095

9196
function filterNulls(inputObj) {

lib/transaction/trace/partial-trace.js

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88
const logger = require('../../logger').child({ component: 'partial-trace' })
99
const { PARTIAL_TRACE } = require('../../metrics/names')
10-
const { Attributes } = require('#agentlib/attributes.js')
11-
10+
const { SPAN_EVENTS } = require('#agentlib/metrics/names.js')
1211
/**
1312
* A PartialTrace manages span processing for partial granularity traces.
1413
* It handles span reparenting, compaction logic, and finalization of span events.
@@ -58,6 +57,7 @@ class PartialTrace {
5857
* Runs a span through partial tracing rules.
5958
* If the span is null, it indicates that the span was dropped,
6059
* and we must keep track of its id and parentId for potential reparenting.
60+
* Also reparents span links from dropped spans.
6161
*
6262
* @param {object} params to function
6363
* @param {string} params.span to apply partial rules to
@@ -66,16 +66,15 @@ class PartialTrace {
6666
addSpan({ span, isEntry }) {
6767
const id = span.id
6868
const parentId = span.parentId
69+
70+
// capture necessary information for reparenting span links before span is potentially dropped
6971
const spanLinks = span.spanLinks
72+
const exitSpan = span.isExitSpan
73+
const hasEntityAttrs = span.hasEntityRelationshipAttrs
74+
7075
span = span.applyPartialTraceRules({ isEntry, partialTrace: this })
7176
this.createMetrics(!!span)
7277

73-
if (spanLinks && spanLinks.length > 0) {
74-
if (this.type === 'essential') {
75-
this.removeNonIntrinsicsAttrs(spanLinks)
76-
}
77-
}
78-
7978
if (span) {
8079
// span was not dropped, add to trace until all spans have been processed
8180
this.spans.push(span)
@@ -88,41 +87,34 @@ class PartialTrace {
8887

8988
// span was dropped but we still need to move its span links to the last kept span
9089
// spanLinks were captured before the span was dropped
91-
if (spanLinks && spanLinks.length > 0) {
92-
this.reparentSpanLinks(spanLinks)
93-
}
90+
this.reparentSpanLinks(this.spans.at(-1), spanLinks)
91+
} else if ((this.type === 'compact') && (!exitSpan || !hasEntityAttrs)) {
92+
// we still need to reparent links for dropped spans in compact mode that are not exit
93+
// span or spans without entity relationship attributes
94+
this.reparentSpanLinks(this.spans.at(-1), spanLinks)
9495
}
9596
}
9697

9798
/**
98-
* Iterates over the span links from a span and removes all non-intrinsic attributes.
99-
*
100-
* @param {SpanLink[]} spanLinks an array of span links to reparent to last kept span
101-
*/
102-
removeNonIntrinsicsAttrs(spanLinks) {
103-
for (const link of spanLinks) {
104-
link.userAttributes = new Attributes({ scope: Attributes.SCOPE_SEGMENT })
105-
link.agentAttributes = new Attributes({ scope: Attributes.SCOPE_SEGMENT })
106-
}
107-
}
108-
109-
/**
110-
* Iterates over the span links from a dropped span and reassigns them to the last kept span.
99+
* Iterates over the span links from a dropped span and reassigns them to the span to reparent to.
111100
* The id intrinsic attribute will also be updated to the value of the last kept span id.
112101
*
113-
* @param {SpanLink[]} spanLinks an array of span links to reparent to last kept span
102+
* @param {SpanEvent} span the span to reparent span links to
103+
* @param {SpanLink[]} spanLinksToReparent an array of span links to reparent to last kept span
114104
*/
115-
reparentSpanLinks(spanLinks) {
116-
const lastSpan = this.spans.at(-1)
117-
105+
reparentSpanLinks(span, spanLinksToReparent = []) {
118106
// The id intrinsics attribute needs to be updated to equal the id of the new span the
119107
// span links are moving to.
120-
for (const link of spanLinks) {
121-
link.intrinsics.id = lastSpan.id
122-
}
108+
for (const link of spanLinksToReparent) {
109+
if (span.spanLinks.length === 100) {
110+
logger.trace('Span links limit reached. Not moving additional span links.')
111+
this.transaction.agent.metrics.getOrCreateMetric(SPAN_EVENTS.LINKS_DROPPED).incrementCallCount()
112+
return
113+
}
123114

124-
// move the span links events to the last kept span
125-
Array.prototype.push.apply(lastSpan.spanLinks, spanLinks)
115+
link.intrinsics.id = span.id
116+
span.spanLinks.push(link)
117+
}
126118
}
127119

128120
/**
@@ -195,7 +187,7 @@ class PartialTrace {
195187
/**
196188
* Checks if span was the retained exit span for a given entity and it has other spans that talked to the same
197189
* entity. It will then sort all timestamps for spans that got dropped and calculate the `nr.durations` and assign
198-
* `nr.ids` for all dropped spans to same entity.
190+
* `nr.ids` for all dropped spans to same entity. It will also reparent the span links from dropped spans to the retained exit span.
199191
*
200192
* @param {SpanEvent} span to check if it has to calculate `nr.durations` and `nr.ids`
201193
*/
@@ -221,7 +213,8 @@ class PartialTrace {
221213
totalDuration: 0,
222214
currentStart: null,
223215
currentEnd: null,
224-
errorSpan: null
216+
errorSpan: null,
217+
spanLinks: []
225218
}
226219

227220
// timestamps must be sorted to accurately calculate overlapping durations
@@ -238,13 +231,17 @@ class PartialTrace {
238231
} else {
239232
meta.droppedIds++
240233
}
234+
235+
Array.prototype.push.apply(meta.spanLinks, sameEntitySpan.spanLinks)
241236
}
242237

243238
this.compactionError(meta, sameEntitySpan)
244239
this.calculateDuration(meta, sameEntitySpan)
245240
}
246241

247242
this.assignCompactAttrs({ span, meta })
243+
244+
this.reparentSpanLinks(span, meta.spanLinks)
248245
}
249246

250247
assignCompactAttrs({ span, meta }) {

test/unit/spans/helpers.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const SpanLink = require('#agentlib/spans/span-link.js')
9+
const SpanEvent = require('#agentlib/spans/span-event.js')
10+
const sinon = require('sinon')
11+
const assert = require('node:assert')
12+
13+
const TIMESTAMP = 1765285200000 // 2025-12-09T09:00:00.000-04:00
14+
15+
function setupPartialTransaction(tx, partialType) {
16+
tx.priority = 42
17+
tx.sampled = true
18+
tx.partialType = partialType
19+
tx.createPartialTrace()
20+
}
21+
22+
function createSpanLink({ segment, spanId, traceId, linkSpanId, linkTraceId, testAttr }) {
23+
const link = new SpanLink({
24+
link: {
25+
attributes: { test: testAttr },
26+
context: { spanId: linkSpanId, traceId: linkTraceId }
27+
},
28+
spanContext: {
29+
spanId,
30+
traceId
31+
},
32+
timestamp: TIMESTAMP
33+
})
34+
segment.spanLinks.push(link)
35+
return link
36+
}
37+
38+
function addSegment({ spanEventAggregator, tx, segment, parentId = '1', isEntry = false }) {
39+
spanEventAggregator.addSegment({ segment, transaction: tx, parentId, isEntry })
40+
}
41+
42+
function stubEntityRelationship(hasEntity) {
43+
return sinon.stub(SpanEvent.prototype, 'hasEntityRelationshipAttrs').get(() => hasEntity)
44+
}
45+
46+
function createSegment(agent, id, name, parent, tx) {
47+
return agent.tracer.createSegment({
48+
id,
49+
name,
50+
parent,
51+
transaction: tx
52+
})
53+
}
54+
55+
function setupPartialTrace({ agent, partialType, tx, addRootSpanLink = false, add99SpanLinks = false, addExitSpan = false, numOfChildSegments = 2 }) {
56+
setupPartialTransaction(tx, partialType)
57+
58+
// setup root segment and span links on root if needed
59+
const rootSegment = agent.tracer.getSegment()
60+
61+
if (add99SpanLinks) {
62+
for (let i = 0; i < 99; i += 1) {
63+
rootSegment.addSpanLink({ fake: 'link' })
64+
}
65+
}
66+
67+
if (addRootSpanLink) {
68+
createSpanLink({ segment: rootSegment, spanId: rootSegment.id, traceId: 'trace1', linkSpanId: rootSegment.id, linkTraceId: 'trace1', testAttr: 'rootTest' })
69+
}
70+
71+
// setup child 1 segment with span link
72+
const child1Name = addExitSpan ? 'MessageBroker/api.example.com/users' : 'child1-segment'
73+
const child1Segment = createSegment(agent, 'child1', child1Name, rootSegment, tx)
74+
createSpanLink({ segment: child1Segment, spanId: 'span1', traceId: 'trace1', linkSpanId: 'parent1', linkTraceId: 'trace1', testAttr: 'test1' })
75+
76+
if (numOfChildSegments === 1) {
77+
return { rootSegment, child1Segment }
78+
}
79+
80+
// setup child 2 segment with span links if needed
81+
const child2Segment = createSegment(agent, 'child2', 'child2-segment', child1Segment, tx)
82+
createSpanLink({ segment: child2Segment, spanId: 'span2', traceId: 'trace2', linkSpanId: 'parent2', linkTraceId: 'trace1', testAttr: 'test2' })
83+
84+
return { rootSegment, child1Segment, child2Segment }
85+
}
86+
87+
function setupPartialTraceForCompactCompression(agent, tx) {
88+
setupPartialTransaction(tx, 'compact')
89+
90+
const reparentSpanLinkSpy = sinon.spy(tx.partialTrace, 'reparentSpanLinks')
91+
const rootSegment = agent.tracer.getSegment()
92+
93+
// first exit span to a message broker
94+
const child1Segment = createSegment(agent, 'child1', 'MessageBroker/api.example.com/users', rootSegment, tx)
95+
96+
// entry span to another service
97+
const child2Segment = createSegment(agent, 'child2', 'child2-segment', child1Segment, tx)
98+
99+
// second exit span to the same message broker
100+
const child3Segment = createSegment(agent, 'child3', 'MessageBroker/api.example.com/users', rootSegment, tx)
101+
102+
// entry span to another service
103+
const child4Segment = createSegment(agent, 'child4', 'child4-segment', child3Segment, tx)
104+
105+
createSpanLink({ segment: child1Segment, spanId: 'span1', traceId: 'trace1', linkSpanId: 'parent1', linkTraceId: 'trace1', testAttr: 'test1' })
106+
createSpanLink({ segment: child3Segment, spanId: 'span1', traceId: 'trace1', linkSpanId: 'parent1', linkTraceId: 'trace1', testAttr: 'test2' })
107+
108+
return { rootSegment, child1Segment, child2Segment, child3Segment, child4Segment, reparentSpanLinkSpy }
109+
}
110+
111+
function addSegmentsForCompactCompression({ spanEventAggregator, tx, segments }) {
112+
addSegment({ spanEventAggregator, tx, segment: segments.rootSegment, isEntry: true })
113+
tx.baseSegment = segments.rootSegment
114+
115+
// simulate that the segment has entity relationship attrs to keep the span
116+
const hasEntityStub = stubEntityRelationship(true)
117+
118+
addSegment({ spanEventAggregator, tx, segment: segments.child1Segment, parentId: segments.rootSegment.id })
119+
addSegment({ spanEventAggregator, tx, segment: segments.child2Segment, parentId: segments.child1Segment.id, isEntry: true })
120+
addSegment({ spanEventAggregator, tx, segment: segments.child3Segment, parentId: segments.rootSegment.id })
121+
addSegment({ spanEventAggregator, tx, segment: segments.child4Segment, parentId: segments.child3Segment.id, isEntry: true })
122+
hasEntityStub.restore()
123+
}
124+
125+
function assertSpanLinkAttributes(spanLinks) {
126+
for (let i = 0; i < spanLinks.length; i += 1) {
127+
const link = spanLinks[i]
128+
assert.equal(Object.keys(link.userAttributes).length, 0)
129+
assert.equal(Object.keys(link.agentAttributes).length, 0)
130+
}
131+
}
132+
133+
module.exports = {
134+
setupPartialTrace,
135+
stubEntityRelationship,
136+
assertSpanLinkAttributes,
137+
addSegment,
138+
createSegment,
139+
createSpanLink,
140+
setupPartialTraceForCompactCompression,
141+
addSegmentsForCompactCompression
142+
}

0 commit comments

Comments
 (0)