📡 OTel Instrumentation Improvement: JSONL mirror is silently skipped when OTLP endpoint is not configured
Analysis Date: 2026-04-10
Priority: Medium
Effort: Small (< 2h)
Problem
The local JSONL mirror at /tmp/gh-aw/otel.jsonl is never written when OTEL_EXPORTER_OTLP_ENDPOINT is not set. Both sendJobSetupSpan and sendJobConclusionSpan return early before building the payload or calling appendToOTLPJSONL, so the JSONL artifact remains empty for any workflow run without an OTLP backend configured.
This is a direct contradiction of the stated design intent in send_otlp_span.cjs:line 163:
"Every OTLP span payload is also appended here as a JSON line so that it can be inspected via GitHub Actions artifacts without needing a live collector."
In practice, the JSONL file only exists when a collector is configured.
Why This Matters (DevOps Perspective)
Two distinct failure modes exist:
-
No OTLP backend at all – Organizations that haven't configured a collector (or are evaluating gh-aw for the first time) get zero structured telemetry from their workflow runs. The JSONL artifact would have been their only way to inspect span payloads and validate attribute coverage without standing up a backend.
-
Transient collector outage – The current code handles this case correctly: appendToOTLPJSONL is called before the HTTP request inside sendOTLPSpan, so a network error still produces a local mirror. The gap is purely about the missing unconditional write.
Fixing this gives all gh-aw users access to structured span data as a GitHub Actions artifact — enabling:
- Faster bootstrap of new observability integrations (validate attribute shape before wiring a backend)
- Post-mortem debugging when an OTLP backend was offline during an incident
- Automated CI checks against expected span attributes without a live collector
Current Behavior
// send_otlp_span.cjs — sendJobSetupSpan (lines 466–469)
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "";
if (!endpoint) {
return { traceId, spanId }; // ← payload never built; JSONL never written
}
// ... attribute collection, buildOTLPPayload, sendOTLPSpan (which calls appendToOTLPJSONL) ...
// send_otlp_span.cjs — sendJobConclusionSpan (lines 626–629)
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "";
if (!endpoint) {
return; // ← same: JSONL never written
}
// send_otlp_span.cjs — sendOTLPSpan (line 318)
// appendToOTLPJSONL is only reached when sendOTLPSpan is called,
// which only happens when endpoint is set.
appendToOTLPJSONL(payload);
Proposed Change
Move the payload build + appendToOTLPJSONL call before the endpoint guard in both high-level functions, and pass a flag to sendOTLPSpan to skip the redundant second write:
// send_otlp_span.cjs — sendJobSetupSpan (proposed restructure)
// ... resolve traceId / spanId as before ...
// Build full payload unconditionally so it can always be mirrored locally.
const startMs = options.startMs ?? nowMs();
const endMs = nowMs();
// ... collect all env vars (serviceName, jobName, workflowName, etc.) ...
// ... build attributes[] and resourceAttributes[] ...
const payload = buildOTLPPayload({ traceId, spanId, ..., attributes, resourceAttributes });
// Always mirror to JSONL — the artifact is useful even without a live collector.
appendToOTLPJSONL(payload);
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "";
if (!endpoint) {
return { traceId, spanId };
}
// Pass skipJSONL: true so sendOTLPSpan doesn't double-write the mirror.
await sendOTLPSpan(endpoint, payload, { skipJSONL: true });
return { traceId, spanId };
// send_otlp_span.cjs — sendOTLPSpan (add skipJSONL option)
async function sendOTLPSpan(endpoint, payload, { maxRetries = 2, baseDelayMs = 100, skipJSONL = false } = {}) {
if (!skipJSONL) {
appendToOTLPJSONL(payload);
}
// ... rest unchanged ...
}
Apply the same restructure to sendJobConclusionSpan.
Expected Outcome
After this change:
- In the GitHub Actions artifact (
/tmp/gh-aw/otel.jsonl): every workflow run produces a JSONL file with all span payloads, regardless of OTLP backend configuration — enabling artifact-based debugging for all users.
- In Grafana / Honeycomb / Datadog: no change; behaviour for configured backends is identical.
- For on-call engineers: during an OTLP backend outage, re-running the failed job (or downloading artifacts) still produces local telemetry. This reduces the "dark period" during collector downtime.
Implementation Steps
Evidence from Live Sentry Data
No Sentry MCP was available in this environment. Analysis is based on static code review. The code path was confirmed by tracing sendJobSetupSpan (line 466–469) and sendJobConclusionSpan (line 626–629): both reach an unconditional return before buildOTLPPayload or appendToOTLPJSONL are ever called when OTEL_EXPORTER_OTLP_ENDPOINT is unset.
The contradiction between the design comment ("inspected via GitHub Actions artifacts without needing a live collector") and the implementation (JSONL never written when endpoint is absent) is self-documenting.
Related Files
actions/setup/js/send_otlp_span.cjs — sendJobSetupSpan (line 430), sendJobConclusionSpan (line 625), sendOTLPSpan (line 316), appendToOTLPJSONL (line 176)
actions/setup/js/send_otlp_span.test.cjs — appendToOTLPJSONL tests (line 578+)
actions/setup/js/action_setup_otlp.cjs — calls sendJobSetupSpan
actions/setup/js/action_conclusion_otlp.cjs — calls sendJobConclusionSpan
Generated by the Daily OTel Instrumentation Advisor workflow
Generated by Daily OTel Instrumentation Advisor · ● 192.3K · ◷
📡 OTel Instrumentation Improvement: JSONL mirror is silently skipped when OTLP endpoint is not configured
Analysis Date: 2026-04-10
Priority: Medium
Effort: Small (< 2h)
Problem
The local JSONL mirror at
/tmp/gh-aw/otel.jsonlis never written whenOTEL_EXPORTER_OTLP_ENDPOINTis not set. BothsendJobSetupSpanandsendJobConclusionSpanreturn early before building the payload or callingappendToOTLPJSONL, so the JSONL artifact remains empty for any workflow run without an OTLP backend configured.This is a direct contradiction of the stated design intent in
send_otlp_span.cjs:line 163:In practice, the JSONL file only exists when a collector is configured.
Why This Matters (DevOps Perspective)
Two distinct failure modes exist:
No OTLP backend at all – Organizations that haven't configured a collector (or are evaluating
gh-awfor the first time) get zero structured telemetry from their workflow runs. The JSONL artifact would have been their only way to inspect span payloads and validate attribute coverage without standing up a backend.Transient collector outage – The current code handles this case correctly:
appendToOTLPJSONLis called before the HTTP request insidesendOTLPSpan, so a network error still produces a local mirror. The gap is purely about the missing unconditional write.Fixing this gives all
gh-awusers access to structured span data as a GitHub Actions artifact — enabling:Current Behavior
Proposed Change
Move the payload build +
appendToOTLPJSONLcall before the endpoint guard in both high-level functions, and pass a flag tosendOTLPSpanto skip the redundant second write:Apply the same restructure to
sendJobConclusionSpan.Expected Outcome
After this change:
/tmp/gh-aw/otel.jsonl): every workflow run produces a JSONL file with all span payloads, regardless of OTLP backend configuration — enabling artifact-based debugging for all users.Implementation Steps
sendJobSetupSpan(send_otlp_span.cjs): move attribute collection +buildOTLPPayloadcall before theif (!endpoint)guard; callappendToOTLPJSONL(payload)immediately after building the payloadsendJobConclusionSpan(send_otlp_span.cjs): same restructureskipJSONLoption tosendOTLPSpanto prevent double-writing when callers already mirrored the payloadsend_otlp_span.test.cjs: add assertions thatappendToOTLPJSONLis called (or that the JSONL file is populated) even whenOTEL_EXPORTER_OTLP_ENDPOINTis absentcd actions/setup/js && npx vitest runto confirm tests passmake fmtto ensure formattingEvidence from Live Sentry Data
No Sentry MCP was available in this environment. Analysis is based on static code review. The code path was confirmed by tracing
sendJobSetupSpan(line 466–469) andsendJobConclusionSpan(line 626–629): both reach an unconditionalreturnbeforebuildOTLPPayloadorappendToOTLPJSONLare ever called whenOTEL_EXPORTER_OTLP_ENDPOINTis unset.The contradiction between the design comment ("inspected via GitHub Actions artifacts without needing a live collector") and the implementation (JSONL never written when endpoint is absent) is self-documenting.
Related Files
actions/setup/js/send_otlp_span.cjs—sendJobSetupSpan(line 430),sendJobConclusionSpan(line 625),sendOTLPSpan(line 316),appendToOTLPJSONL(line 176)actions/setup/js/send_otlp_span.test.cjs—appendToOTLPJSONLtests (line 578+)actions/setup/js/action_setup_otlp.cjs— callssendJobSetupSpanactions/setup/js/action_conclusion_otlp.cjs— callssendJobConclusionSpanGenerated by the Daily OTel Instrumentation Advisor workflow