Skip to content

Commit c29bd2a

Browse files
authored
pass through optional job name and create job span id (#811)
1 parent ab0167c commit c29bd2a

10 files changed

Lines changed: 83 additions & 24 deletions

File tree

otel-export/action.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ inputs:
1717
collector-version:
1818
description: 'OpenTelemetry Collector version'
1919
required: false
20-
default: '0.145.0'
20+
default: '0.148.0'
2121

2222
otlp-protocol:
2323
description: 'OTLP exporter protocol (grpc, http/protobuf)'
@@ -39,6 +39,10 @@ inputs:
3939
required: false
4040
default: 'github.actions'
4141

42+
job-name:
43+
description: 'Job name for deterministic span ID (defaults to GITHUB_JOB). For matrix jobs, include the matrix key for unique per-shard traces (e.g., "build (images/go)").'
44+
required: false
45+
4246
fail-on-error:
4347
description: 'Fail the workflow if telemetry export fails'
4448
required: false

otel-export/dist/index.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33005,6 +33005,10 @@ function generateTraceId(runId, runAttempt) {
3300533005
function generateRootSpanId(runId, runAttempt) {
3300633006
return crypto$1.createHash('sha256').update(`${runId}${runAttempt}s`).digest('hex').substring(16, 32);
3300733007
}
33008+
/** sha256("{runId}{runAttempt}{jobName}")[16:32] — matches githubreceiver newJobSpanID */
33009+
function generateJobSpanId(runId, runAttempt, jobName) {
33010+
return crypto$1.createHash('sha256').update(`${runId}${runAttempt}${jobName}`).digest('hex').substring(16, 32);
33011+
}
3300833012

3300933013
async function run() {
3301033014
try {
@@ -33031,23 +33035,27 @@ async function run() {
3303133035
await waitForCollector();
3303233036
const runId = process.env.GITHUB_RUN_ID || '0';
3303333037
const runAttempt = process.env.GITHUB_RUN_ATTEMPT || '1';
33038+
const jobName = getInput('job-name') || process.env.GITHUB_JOB || 'unknown';
3303433039
const traceId = generateTraceId(runId, runAttempt);
33035-
const spanId = generateRootSpanId(runId, runAttempt);
33036-
const traceparent = `00-${traceId}-${spanId}-01`;
33040+
const rootSpanId = generateRootSpanId(runId, runAttempt);
33041+
const jobSpanId = generateJobSpanId(runId, runAttempt, jobName);
33042+
const traceparent = `00-${traceId}-${jobSpanId}-01`;
3303733043
const protocol = getInput('otlp-protocol');
3303833044
const collectorEndpoint = protocol === 'grpc' ? COLLECTOR_GRPC_ENDPOINT : COLLECTOR_HTTP_ENDPOINT;
3303933045
const envFile = process.env.GITHUB_ENV;
3304033046
if (envFile) {
3304133047
fs$1.appendFileSync(envFile, `TRACEPARENT=${traceparent}\n`);
3304233048
fs$1.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_PROTOCOL=${protocol}\n`);
3304333049
fs$1.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_ENDPOINT=http://${collectorEndpoint}\n`);
33044-
fs$1.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_GRPC_ENDPOINT=${COLLECTOR_GRPC_ENDPOINT}\n`);
33050+
fs$1.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_GRPC_ENDPOINT=http://${COLLECTOR_GRPC_ENDPOINT}\n`);
3304533051
fs$1.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_HTTP_ENDPOINT=http://${COLLECTOR_HTTP_ENDPOINT}\n`);
3304633052
}
33053+
saveState('job-name', jobName);
3304733054
setOutput('traceparent', traceparent);
3304833055
setOutput('trace-id', traceId);
33049-
setOutput('span-id', spanId);
33056+
setOutput('span-id', jobSpanId);
3305033057
info(`Trace ID: ${traceId}`);
33058+
info(`Job: ${jobName} (root=${rootSpanId}, job=${jobSpanId})`);
3305133059
info(`TRACEPARENT=${traceparent}`);
3305233060
info('OpenTelemetry collector running, telemetry will be exported after job completes');
3305333061
}

otel-export/dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

otel-export/dist/post/index.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39781,6 +39781,7 @@ async function collectMetrics(octokit, context) {
3978139781
sha: context.sha || process.env.GITHUB_SHA || '',
3978239782
ref: context.ref || process.env.GITHUB_REF || '',
3978339783
refName: process.env.GITHUB_REF_NAME || null,
39784+
headBranch: context.payload?.pull_request?.head?.ref || null,
3978439785
},
3978539786
event: {
3978639787
name: context.eventName || process.env.GITHUB_EVENT_NAME || '',
@@ -52607,6 +52608,10 @@ function generateTraceId(runId, runAttempt) {
5260752608
function generateRootSpanId(runId, runAttempt) {
5260852609
return crypto.createHash('sha256').update(`${runId}${runAttempt}s`).digest('hex').substring(16, 32);
5260952610
}
52611+
/** sha256("{runId}{runAttempt}{jobName}")[16:32] — matches githubreceiver newJobSpanID */
52612+
function generateJobSpanId(runId, runAttempt, jobName) {
52613+
return crypto.createHash('sha256').update(`${runId}${runAttempt}${jobName}`).digest('hex').substring(16, 32);
52614+
}
5261052615
function buildCicdAttributes(metrics, customAttributes = {}) {
5261152616
const attrs = {
5261252617
'cicd.pipeline.name': metrics.workflow,
@@ -52615,6 +52620,7 @@ function buildCicdAttributes(metrics, customAttributes = {}) {
5261552620
'vcs.repository.url.full': `https://github.com/${metrics.repository.fullName}`,
5261652621
'vcs.ref.head.name': metrics.git.refName || metrics.git.ref,
5261752622
'vcs.ref.head.revision': metrics.git.sha,
52623+
...(metrics.git.headBranch ? { 'vcs.ref.head.branch': metrics.git.headBranch } : {}),
5261852624
'github.run.number': metrics.run.number.toString(),
5261952625
'github.run.attempt': metrics.run.attempt,
5262052626
'github.event.name': metrics.event.name,
@@ -52771,27 +52777,36 @@ function createTracerProvider(config, idGenerator) {
5277152777
});
5277252778
return { tracerProvider, tracer: tracerProvider.getTracer(config.metricPrefix) };
5277352779
}
52774-
function recordTraces(tracer, metrics, customAttributes = {}) {
52780+
function recordTraces(tracer, metrics, parentSpanId, customAttributes = {}) {
5277552781
info('Recording traces');
5277652782
const baseAttributes = buildCicdAttributes(metrics, customAttributes);
5277752783
const jobResult = mapToOtelResult(metrics.job.conclusion);
5277852784
const runUrl = `https://github.com/${metrics.repository.fullName}/actions/runs/${metrics.run.id}`;
52785+
// Create a remote parent context pointing at the run-level root span
52786+
// (sha256("{runId}{runAttempt}s")). This makes the job span a child of
52787+
// the workflow run span, matching the githubreceiver hierarchy:
52788+
// workflow_run (root) → job → steps
52789+
const traceId = generateTraceId(metrics.run.id.toString(), metrics.run.attempt);
52790+
const parentContext = trace.setSpanContext(context.active(), {
52791+
traceId,
52792+
spanId: parentSpanId,
52793+
traceFlags: TraceFlags.SAMPLED,
52794+
isRemote: true,
52795+
});
5277952796
const jobSpan = tracer.startSpan(`RUN ${metrics.workflow}`, {
5278052797
kind: SpanKind.SERVER,
5278152798
startTime: metrics.job.startedAt,
5278252799
attributes: {
5278352800
...baseAttributes,
52784-
// Pipeline-level attributes (this is our root span)
5278552801
'cicd.pipeline.action.name': 'RUN',
5278652802
'cicd.pipeline.result': jobResult,
5278752803
'cicd.pipeline.run.url.full': runUrl,
52788-
// Task-level attributes
5278952804
'cicd.pipeline.task.name': metrics.job.name,
5279052805
'cicd.pipeline.task.run.id': metrics.job.id.toString(),
5279152806
'cicd.pipeline.task.run.result': jobResult,
5279252807
'cicd.pipeline.task.run.url.full': `${runUrl}/job/${metrics.job.id}`,
5279352808
},
52794-
});
52809+
}, parentContext);
5279552810
const jobContext = trace.setSpan(context.active(), jobSpan);
5279652811
for (const step of metrics.steps) {
5279752812
if (step.startedAt && step.completedAt) {
@@ -52855,8 +52870,10 @@ async function run() {
5285552870
const collectorLog = getState('collector-log');
5285652871
const runId = process.env.GITHUB_RUN_ID || '0';
5285752872
const runAttempt = process.env.GITHUB_RUN_ATTEMPT || '1';
52873+
const jobName = getState('job-name') || process.env.GITHUB_JOB || 'unknown';
5285852874
const traceId = generateTraceId(runId, runAttempt);
5285952875
const rootSpanId = generateRootSpanId(runId, runAttempt);
52876+
const jobSpanId = generateJobSpanId(runId, runAttempt, jobName);
5286052877
try {
5286152878
info('Starting OpenTelemetry export post-action');
5286252879
const token = getInput('github-token', { required: true });
@@ -52879,11 +52896,11 @@ async function run() {
5287952896
const metrics = await collectMetrics(octokit, context$1);
5288052897
const { meterProvider: mp, meter } = createMeterProvider(config);
5288152898
meterProvider = mp;
52882-
const idGenerator = new DeterministicIdGenerator(traceId, rootSpanId);
52899+
const idGenerator = new DeterministicIdGenerator(traceId, jobSpanId);
5288352900
const { tracerProvider: tp, tracer } = createTracerProvider(config, idGenerator);
5288452901
tracerProvider = tp;
5288552902
recordMetrics(meter, metrics, metricPrefix, customAttributes);
52886-
recordTraces(tracer, metrics, customAttributes);
52903+
recordTraces(tracer, metrics, rootSpanId, customAttributes);
5288752904
await shutdownMeterProvider(meterProvider);
5288852905
await shutdownTracerProvider(tracerProvider);
5288952906
if (traceId) {

otel-export/dist/post/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

otel-export/index.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as core from '@actions/core';
22
import * as fs from 'node:fs';
33
import * as path from 'node:path';
44
import { downloadCollector, startCollector, waitForCollector, COLLECTOR_HTTP_ENDPOINT, COLLECTOR_GRPC_ENDPOINT, type ResourceAttributes } from './lib/otelcol.js';
5-
import { generateTraceId, generateRootSpanId } from './lib/exporter.js';
5+
import { generateTraceId, generateRootSpanId, generateJobSpanId } from './lib/exporter.js';
66

77
async function run(): Promise<void> {
88
try {
@@ -38,9 +38,11 @@ async function run(): Promise<void> {
3838

3939
const runId = process.env.GITHUB_RUN_ID || '0';
4040
const runAttempt = process.env.GITHUB_RUN_ATTEMPT || '1';
41+
const jobName = core.getInput('job-name') || process.env.GITHUB_JOB || 'unknown';
4142
const traceId = generateTraceId(runId, runAttempt);
42-
const spanId = generateRootSpanId(runId, runAttempt);
43-
const traceparent = `00-${traceId}-${spanId}-01`;
43+
const rootSpanId = generateRootSpanId(runId, runAttempt);
44+
const jobSpanId = generateJobSpanId(runId, runAttempt, jobName);
45+
const traceparent = `00-${traceId}-${jobSpanId}-01`;
4446

4547
const protocol = core.getInput('otlp-protocol');
4648
const collectorEndpoint = protocol === 'grpc' ? COLLECTOR_GRPC_ENDPOINT : COLLECTOR_HTTP_ENDPOINT;
@@ -50,15 +52,18 @@ async function run(): Promise<void> {
5052
fs.appendFileSync(envFile, `TRACEPARENT=${traceparent}\n`);
5153
fs.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_PROTOCOL=${protocol}\n`);
5254
fs.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_ENDPOINT=http://${collectorEndpoint}\n`);
53-
fs.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_GRPC_ENDPOINT=${COLLECTOR_GRPC_ENDPOINT}\n`);
55+
fs.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_GRPC_ENDPOINT=http://${COLLECTOR_GRPC_ENDPOINT}\n`);
5456
fs.appendFileSync(envFile, `OTEL_EXPORTER_OTLP_HTTP_ENDPOINT=http://${COLLECTOR_HTTP_ENDPOINT}\n`);
5557
}
5658

59+
core.saveState('job-name', jobName);
60+
5761
core.setOutput('traceparent', traceparent);
5862
core.setOutput('trace-id', traceId);
59-
core.setOutput('span-id', spanId);
63+
core.setOutput('span-id', jobSpanId);
6064

6165
core.info(`Trace ID: ${traceId}`);
66+
core.info(`Job: ${jobName} (root=${rootSpanId}, job=${jobSpanId})`);
6267
core.info(`TRACEPARENT=${traceparent}`);
6368
core.info('OpenTelemetry collector running, telemetry will be exported after job completes');
6469
} catch (error) {

otel-export/lib/collector.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export async function collectMetrics(octokit: Octokit, context: Context): Promis
225225
sha: context.sha || process.env.GITHUB_SHA || '',
226226
ref: context.ref || process.env.GITHUB_REF || '',
227227
refName: process.env.GITHUB_REF_NAME || null,
228+
headBranch: (context.payload?.pull_request?.head?.ref as string) || null,
228229
},
229230
event: {
230231
name: context.eventName || process.env.GITHUB_EVENT_NAME || '',

otel-export/lib/exporter.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk
44
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
55
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
66
import { BasicTracerProvider, BatchSpanProcessor, RandomIdGenerator, type IdGenerator } from '@opentelemetry/sdk-trace-base';
7-
import { context, trace, SpanKind, SpanStatusCode, type Tracer, type Meter } from '@opentelemetry/api';
7+
import { context, trace, SpanKind, SpanStatusCode, TraceFlags, type Tracer, type Meter } from '@opentelemetry/api';
88
import { resourceFromAttributes } from '@opentelemetry/resources';
99
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
1010
import type { WorkflowMetrics, OtelConfig, CustomAttributes } from './types.js';
@@ -41,6 +41,11 @@ export function generateRootSpanId(runId: string, runAttempt: string): string {
4141
return crypto.createHash('sha256').update(`${runId}${runAttempt}s`).digest('hex').substring(16, 32);
4242
}
4343

44+
/** sha256("{runId}{runAttempt}{jobName}")[16:32] — matches githubreceiver newJobSpanID */
45+
export function generateJobSpanId(runId: string, runAttempt: string, jobName: string): string {
46+
return crypto.createHash('sha256').update(`${runId}${runAttempt}${jobName}`).digest('hex').substring(16, 32);
47+
}
48+
4449
function buildCicdAttributes(
4550
metrics: WorkflowMetrics,
4651
customAttributes: CustomAttributes = {},
@@ -53,6 +58,7 @@ function buildCicdAttributes(
5358
'vcs.repository.url.full': `https://github.com/${metrics.repository.fullName}`,
5459
'vcs.ref.head.name': metrics.git.refName || metrics.git.ref,
5560
'vcs.ref.head.revision': metrics.git.sha,
61+
...(metrics.git.headBranch ? { 'vcs.ref.head.branch': metrics.git.headBranch } : {}),
5662

5763
'github.run.number': metrics.run.number.toString(),
5864
'github.run.attempt': metrics.run.attempt,
@@ -260,6 +266,7 @@ export function createTracerProvider(
260266
export function recordTraces(
261267
tracer: Tracer,
262268
metrics: WorkflowMetrics,
269+
parentSpanId: string,
263270
customAttributes: CustomAttributes = {},
264271
): void {
265272
core.info('Recording traces');
@@ -269,22 +276,35 @@ export function recordTraces(
269276
const jobResult = mapToOtelResult(metrics.job.conclusion);
270277
const runUrl = `https://github.com/${metrics.repository.fullName}/actions/runs/${metrics.run.id}`;
271278

279+
// Create a remote parent context pointing at the run-level root span
280+
// (sha256("{runId}{runAttempt}s")). This makes the job span a child of
281+
// the workflow run span, matching the githubreceiver hierarchy:
282+
// workflow_run (root) → job → steps
283+
const traceId = generateTraceId(
284+
metrics.run.id.toString(),
285+
metrics.run.attempt,
286+
);
287+
const parentContext = trace.setSpanContext(context.active(), {
288+
traceId,
289+
spanId: parentSpanId,
290+
traceFlags: TraceFlags.SAMPLED,
291+
isRemote: true,
292+
});
293+
272294
const jobSpan = tracer.startSpan(`RUN ${metrics.workflow}`, {
273295
kind: SpanKind.SERVER,
274296
startTime: metrics.job.startedAt,
275297
attributes: {
276298
...baseAttributes,
277-
// Pipeline-level attributes (this is our root span)
278299
'cicd.pipeline.action.name': 'RUN',
279300
'cicd.pipeline.result': jobResult,
280301
'cicd.pipeline.run.url.full': runUrl,
281-
// Task-level attributes
282302
'cicd.pipeline.task.name': metrics.job.name,
283303
'cicd.pipeline.task.run.id': metrics.job.id.toString(),
284304
'cicd.pipeline.task.run.result': jobResult,
285305
'cicd.pipeline.task.run.url.full': `${runUrl}/job/${metrics.job.id}`,
286306
},
287-
});
307+
}, parentContext);
288308

289309
const jobContext = trace.setSpan(context.active(), jobSpan);
290310

otel-export/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface WorkflowMetrics {
3737
sha: string;
3838
ref: string;
3939
refName: string | null;
40+
headBranch: string | null;
4041
};
4142
event: {
4243
name: string;

otel-export/post.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { stopCollector, dumpCollectorLogs } from './lib/otelcol.js';
77
import {
88
generateTraceId,
99
generateRootSpanId,
10+
generateJobSpanId,
1011
DeterministicIdGenerator,
1112
createMeterProvider,
1213
recordMetrics,
@@ -49,8 +50,10 @@ async function run(): Promise<void> {
4950

5051
const runId = process.env.GITHUB_RUN_ID || '0';
5152
const runAttempt = process.env.GITHUB_RUN_ATTEMPT || '1';
53+
const jobName = core.getState('job-name') || process.env.GITHUB_JOB || 'unknown';
5254
const traceId = generateTraceId(runId, runAttempt);
5355
const rootSpanId = generateRootSpanId(runId, runAttempt);
56+
const jobSpanId = generateJobSpanId(runId, runAttempt, jobName);
5457

5558
try {
5659
core.info('Starting OpenTelemetry export post-action');
@@ -79,12 +82,12 @@ async function run(): Promise<void> {
7982
const { meterProvider: mp, meter } = createMeterProvider(config);
8083
meterProvider = mp;
8184

82-
const idGenerator = new DeterministicIdGenerator(traceId, rootSpanId);
85+
const idGenerator = new DeterministicIdGenerator(traceId, jobSpanId);
8386
const { tracerProvider: tp, tracer } = createTracerProvider(config, idGenerator);
8487
tracerProvider = tp;
8588

8689
recordMetrics(meter, metrics, metricPrefix, customAttributes);
87-
recordTraces(tracer, metrics, customAttributes);
90+
recordTraces(tracer, metrics, rootSpanId, customAttributes);
8891

8992
await shutdownMeterProvider(meterProvider);
9093
await shutdownTracerProvider(tracerProvider);

0 commit comments

Comments
 (0)