Skip to content

Commit f89ed61

Browse files
authored
feat(agentless): add compute_stats, trace_root, and top_level tags to agentless encoder (#7716)
feat(agentless): add compute_stats, trace_root, and top_level tags to agentless encoder Enriches agentless JSON-encoded spans with metadata required by the intake to compute APM stats server-side: - _dd.compute_stats on the first span of each trace - _trace_root on root spans (parent_id == 0) - _top_level on top-level spans Co-Authored-By: Claude Opus 4.6 <[email protected]> Co-authored-by: bryan.english <[email protected]>
1 parent 0009c80 commit f89ed61

File tree

2 files changed

+91
-5
lines changed

2 files changed

+91
-5
lines changed

packages/dd-trace/src/encode/agentless-json.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
'use strict'
22

33
const log = require('../log')
4+
const { TOP_LEVEL_KEY } = require('../constants')
45
const { truncateSpan, normalizeSpan } = require('./tags-processors')
56

67
/**
78
* Formats a span for JSON encoding.
89
* @param {object} span - The span to format
10+
* @param {boolean} isFirstSpan - Whether this is the first span in the trace
911
* @returns {object} The formatted span
1012
*/
11-
function formatSpan (span) {
13+
function formatSpan (span, isFirstSpan) {
1214
span = normalizeSpan(truncateSpan(span, false))
1315

1416
if (span.span_events) {
1517
span.meta.events = JSON.stringify(span.span_events)
1618
delete span.span_events
1719
}
1820

21+
if (isFirstSpan) {
22+
span.meta['_dd.compute_stats'] = '1'
23+
}
24+
25+
if (span.parent_id?.toString(10) === '0') {
26+
span.metrics._trace_root = 1
27+
}
28+
29+
if (span.metrics[TOP_LEVEL_KEY]) {
30+
span.metrics._top_level = 1
31+
}
32+
1933
return span
2034
}
2135

@@ -83,7 +97,7 @@ class AgentlessJSONEncoder {
8397
encode (trace) {
8498
for (const span of trace) {
8599
try {
86-
const formattedSpan = formatSpan(span)
100+
const formattedSpan = formatSpan(span, this._spanCount === 0)
87101
const jsonSpan = spanToJSON(formattedSpan)
88102

89103
this._spans.push(jsonSpan)

packages/dd-trace/test/encode/agentless-json.spec.js

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const { AgentlessJSONEncoder } = require('../../src/encode/agentless-json')
1111
describe('AgentlessJSONEncoder', () => {
1212
let encoder
1313
let data
14+
let childSpan
1415

1516
beforeEach(() => {
1617
encoder = new AgentlessJSONEncoder()
@@ -33,6 +34,20 @@ describe('AgentlessJSONEncoder', () => {
3334
duration: 5000000,
3435
links: [],
3536
}]
37+
childSpan = {
38+
trace_id: id('1234abcd1234abcd'),
39+
span_id: id('aaaa000000000001'),
40+
parent_id: id('5678efab5678efab'),
41+
name: 'child',
42+
resource: 'child-resource',
43+
service: 'test-service',
44+
error: 0,
45+
meta: {},
46+
metrics: {},
47+
start: 1234567891000000000,
48+
duration: 1000000,
49+
links: [],
50+
}
3651
})
3752

3853
describe('encode', () => {
@@ -89,18 +104,20 @@ describe('AgentlessJSONEncoder', () => {
89104
// Start time is converted from nanoseconds to seconds for intake format
90105
assert.strictEqual(span.start, 1234567890)
91106
assert.strictEqual(span.duration, 5000000)
92-
assert.deepStrictEqual(span.meta, { foo: 'bar' })
93-
assert.deepStrictEqual(span.metrics, { example: 1.5 })
107+
assert.deepStrictEqual(span.meta, { foo: 'bar', '_dd.compute_stats': '1' })
108+
assert.deepStrictEqual(span.metrics, { example: 1.5, _trace_root: 1 })
94109
})
95110

96111
it('should handle multiple spans in one trace', () => {
97112
encoder.encode(data)
98-
encoder.encode(data)
113+
encoder.encode([childSpan])
99114

100115
const buffer = encoder.makePayload()
101116
const decoded = JSON.parse(buffer.toString())
102117

103118
assert.strictEqual(decoded.spans.length, 2)
119+
assert.strictEqual(decoded.spans[0].meta['_dd.compute_stats'], '1')
120+
assert.strictEqual(decoded.spans[1].meta['_dd.compute_stats'], undefined)
104121
})
105122

106123
it('should handle spans without optional fields', () => {
@@ -155,6 +172,61 @@ describe('AgentlessJSONEncoder', () => {
155172
assert.deepStrictEqual(decoded.spans[0].links, [{ trace_id: 'abc123', span_id: 'def456' }])
156173
})
157174

175+
it('should set _dd.compute_stats on the first span only', () => {
176+
encoder.encode([data[0], childSpan])
177+
178+
const buffer = encoder.makePayload()
179+
const decoded = JSON.parse(buffer.toString())
180+
181+
assert.strictEqual(decoded.spans[0].meta['_dd.compute_stats'], '1')
182+
assert.strictEqual(decoded.spans[1].meta['_dd.compute_stats'], undefined)
183+
})
184+
185+
it('should set _trace_root on spans with zero parent_id', () => {
186+
encoder.encode([data[0], childSpan])
187+
188+
const buffer = encoder.makePayload()
189+
const decoded = JSON.parse(buffer.toString())
190+
191+
assert.strictEqual(decoded.spans[0].metrics._trace_root, 1)
192+
assert.strictEqual(decoded.spans[1].metrics._trace_root, undefined)
193+
})
194+
195+
it('should set _top_level on spans marked as top-level', () => {
196+
data[0].metrics['_dd.top_level'] = 1
197+
198+
encoder.encode([data[0], childSpan])
199+
200+
const buffer = encoder.makePayload()
201+
const decoded = JSON.parse(buffer.toString())
202+
203+
assert.strictEqual(decoded.spans[0].metrics._top_level, 1)
204+
assert.strictEqual(decoded.spans[1].metrics._top_level, undefined)
205+
})
206+
207+
it('should not set _top_level when _dd.top_level is 0', () => {
208+
data[0].metrics['_dd.top_level'] = 0
209+
210+
encoder.encode(data)
211+
212+
const buffer = encoder.makePayload()
213+
const decoded = JSON.parse(buffer.toString())
214+
215+
assert.strictEqual(decoded.spans[0].metrics._top_level, undefined)
216+
})
217+
218+
it('should set _dd.compute_stats on next span when first span is malformed', () => {
219+
const badSpan = { name: 'bad' }
220+
221+
encoder.encode([badSpan, childSpan])
222+
223+
const buffer = encoder.makePayload()
224+
const decoded = JSON.parse(buffer.toString())
225+
226+
assert.strictEqual(decoded.spans.length, 1)
227+
assert.strictEqual(decoded.spans[0].meta['_dd.compute_stats'], '1')
228+
})
229+
158230
it('should skip malformed spans and continue encoding', () => {
159231
const goodSpan = data[0]
160232
const badSpan = { name: 'bad' } // Missing required ID fields

0 commit comments

Comments
 (0)