Skip to content

Commit e583ac4

Browse files
rodrigo-rocaclaude
andcommitted
feat(trace): add --span-id and --parent-id to trace span
Let callers assign an explicit span ID and nest a custom span under another custom span, instead of every span being flat under the job. --span-id defaults to a generated ID when omitted; --parent-id references another span's ID and attaches to the job/step as before when omitted. Both are validated as hexadecimal. This makes it possible to assemble arbitrary span trees (e.g. from an OpenTelemetry dump) via repeated `trace span` calls. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
1 parent 3bac124 commit e583ac4

4 files changed

Lines changed: 77 additions & 7 deletions

File tree

packages/base/src/commands/span/__tests__/span.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,48 @@ describe('span', () => {
120120
expect(context.stdout.toString()).toContain('"life":42')
121121
expect(context.stdout.toString()).toContain('"golden":1.618')
122122
})
123+
124+
test('span-id', async () => {
125+
const env = {GITLAB_CI: '1'}
126+
const {context, code} = await runCLI(
127+
['--name', 'mytestname', '--duration', '10000', '--span-id', '0a1b2c3d4e'],
128+
env
129+
)
130+
expect(code).toBe(0)
131+
expect(context.stdout.toString()).toContain('"span_id":"0a1b2c3d4e"')
132+
})
133+
134+
test('parent-id', async () => {
135+
const env = {GITLAB_CI: '1'}
136+
const {context, code} = await runCLI(
137+
['--name', 'mytestname', '--duration', '10000', '--span-id', '0a1b2c3d4e', '--parent-id', '5f6a7b8c9d'],
138+
env
139+
)
140+
expect(code).toBe(0)
141+
expect(context.stdout.toString()).toContain('"span_id":"0a1b2c3d4e"')
142+
expect(context.stdout.toString()).toContain('"parent_id":"5f6a7b8c9d"')
143+
})
144+
145+
test('no-parent-id-by-default', async () => {
146+
const env = {GITLAB_CI: '1'}
147+
const {context, code} = await runCLI(['--name', 'mytestname', '--duration', '10000'], env)
148+
expect(code).toBe(0)
149+
expect(context.stdout.toString()).not.toContain('"parent_id"')
150+
})
151+
152+
test('span-id-invalid', async () => {
153+
const env = {GITLAB_CI: '1'}
154+
const {context, code} = await runCLI(['--name', 'mytestname', '--duration', '10000', '--span-id', 'xyz'], env)
155+
expect(code).toBe(1)
156+
expect(context.stderr.toString()).toContain('The span ID must be a hexadecimal string.')
157+
})
158+
159+
test('parent-id-invalid', async () => {
160+
const env = {GITLAB_CI: '1'}
161+
const {context, code} = await runCLI(['--name', 'mytestname', '--duration', '10000', '--parent-id', 'xyz'], env)
162+
expect(code).toBe(1)
163+
expect(context.stderr.toString()).toContain('The parent ID must be a hexadecimal string.')
164+
})
123165
})
124166

125167
makeCIProviderTests(runCLI, ['--name', 'mytestname', '--duration', '10000'])

packages/base/src/commands/span/span.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export class SpanCommand extends CustomSpanCommand {
2727
'Create span with name "Get Dependencies" and duration of 10s and report to Datadog with tags and measures',
2828
'datadog-ci trace span --name "Get Dependencies" --duration 10000 --tags "dependency-set:notify" --measures "n-dependencies:42"',
2929
],
30+
[
31+
'Create a span nested under another custom span by referencing its span ID',
32+
'datadog-ci trace span --name "Plan" --duration 10000 --span-id 0a1b2c3d4e --parent-id 5f6a7b8c9d',
33+
],
3034
],
3135
})
3236

@@ -40,6 +44,9 @@ export class SpanCommand extends CustomSpanCommand {
4044
private endTimeInMs: number | undefined = Option.String('--end-time', {
4145
validator: validation.isInteger(),
4246
})
47+
// Optional explicit IDs so callers can assemble a span tree.
48+
private spanId = Option.String('--span-id')
49+
private parentId = Option.String('--parent-id')
4350

4451
public async execute() {
4552
this.tryEnableFips()
@@ -76,14 +83,32 @@ export class SpanCommand extends CustomSpanCommand {
7683
return 1
7784
}
7885

86+
const hexPattern = /^[0-9a-f]+$/i
87+
if (this.spanId !== undefined && !hexPattern.test(this.spanId)) {
88+
this.context.stderr.write(`The span ID must be a hexadecimal string.\n`)
89+
90+
return 1
91+
}
92+
if (this.parentId !== undefined && !hexPattern.test(this.parentId)) {
93+
this.context.stderr.write(`The parent ID must be a hexadecimal string.\n`)
94+
95+
return 1
96+
}
97+
7998
const endTime = this.endTimeInMs ? new Date(this.endTimeInMs) : new Date()
8099
const startTime = new Date(endTime.getTime() - this.durationInMs)
81100

82-
return this.executeReportCustomSpan(this.generateSpanId(), startTime, endTime, {
83-
name: this.name,
84-
error_message: '',
85-
exit_code: 0,
86-
command: 'datadog-ci trace span',
87-
})
101+
return this.executeReportCustomSpan(
102+
this.spanId ?? this.generateSpanId(),
103+
startTime,
104+
endTime,
105+
{
106+
name: this.name,
107+
error_message: '',
108+
exit_code: 0,
109+
command: 'datadog-ci trace span',
110+
},
111+
this.parentId
112+
)
88113
}
89114
}

packages/base/src/commands/trace/helper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export abstract class CustomSpanCommand extends BaseCommand {
5353
id: string,
5454
startTime: Date,
5555
endTime: Date,
56-
extraTags: Record<string, any>
56+
extraTags: Record<string, any>,
57+
parentId?: string
5758
): Promise<number> {
5859
const provider = getCIProvider()
5960
if (!SUPPORTED_PROVIDERS.includes(provider)) {
@@ -87,6 +88,7 @@ export abstract class CustomSpanCommand extends BaseCommand {
8788
end_time: endTime.toISOString(),
8889
ci_provider: provider,
8990
span_id: id,
91+
...(parentId ? {parent_id: parentId} : {}),
9092
tags: {...gitSpanTags, ...ciSpanTags, ...userGitSpanTags, ...cliTags, ...envVarTags},
9193
measures,
9294
command: extraTags.command,

packages/base/src/commands/trace/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type Provider = (typeof SUPPORTED_PROVIDERS)[number]
1616
export interface Payload {
1717
ci_provider: string
1818
span_id: string
19+
parent_id?: string
1920
command: string
2021
name: string
2122
start_time: string

0 commit comments

Comments
 (0)