Skip to content

[otel-advisor] OTel improvement: write JSONL mirror unconditionally, decoupled from OTLP endpoint #25708

@github-actions

Description

@github-actions

📡 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:

  1. 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.

  2. 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
  • In sendJobSetupSpan (send_otlp_span.cjs): move attribute collection + buildOTLPPayload call before the if (!endpoint) guard; call appendToOTLPJSONL(payload) immediately after building the payload
  • In sendJobConclusionSpan (send_otlp_span.cjs): same restructure
  • Add skipJSONL option to sendOTLPSpan to prevent double-writing when callers already mirrored the payload
  • Update send_otlp_span.test.cjs: add assertions that appendToOTLPJSONL is called (or that the JSONL file is populated) even when OTEL_EXPORTER_OTLP_ENDPOINT is absent
  • Run cd actions/setup/js && npx vitest run to confirm tests pass
  • Run make fmt to ensure formatting
  • Open a PR referencing this issue

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.cjssendJobSetupSpan (line 430), sendJobConclusionSpan (line 625), sendOTLPSpan (line 316), appendToOTLPJSONL (line 176)
  • actions/setup/js/send_otlp_span.test.cjsappendToOTLPJSONL 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 ·

  • expires on Apr 17, 2026, 9:25 PM UTC

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions