Skip to content

OTEL_RESOURCE_ATTRIBUTES: un-encoded workflow names (spaces) make strict OTel consumers discard the entire resource #39595

@yskopets

Description

@yskopets

🤖 This issue has been generated by Claude Code.

Summary

gh-aw injects OTEL_RESOURCE_ATTRIBUTES into the workflow env with un-encoded values. In particular, a workflow whose name: contains a space (very common, e.g. name: My Workflow) produces:

OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=My Workflow,gh-aw.repository=...,gh-aw.run.id=...,gh-aw.engine.id=claude'

The raw space in gh-aw.workflow.name=My Workflow makes the value non-compliant with the OpenTelemetry env-var format. Strict OTel SDK consumers that share this env var (e.g. the Claude Code engine's JS OTel SDK) then discard the entire OTEL_RESOURCE_ATTRIBUTES variable — so none of the resource attributes (not even gh-aw's own gh-aw.*) appear on that engine's spans.

gh-aw's own (Go) OTel SDK is lenient and accepts the raw space, so gh-aw's own setup/conclusion spans look correct — which masks the bug. It only manifests on downstream consumers that read the same variable.

Version observed: v0.79.1.

Root cause

pkg/workflow/observability_otlp.go:

// OTEL_RESOURCE_ATTRIBUTES values must escape backslash (`\`), comma (`,`), and
// equals (`=`) per the OpenTelemetry env-var resource attribute grammar.
var otelResourceValueEscaper = strings.NewReplacer(`\`, `\\`, ",", `\,`, "=", `\=`)

func escapeOTELResourceAttributeValue(value string) string {
	return otelResourceValueEscaper.Replace(value)
}

func otelResourceAttributes(workflowData *WorkflowData) string {
	// ...
	workflowNameAttrValue = escapeOTELResourceAttributeValue(workflowName) // "My Workflow" -> "My Workflow" (space untouched)
	attrs := []string{ "gh-aw.workflow.name=" + workflowNameAttrValue, ... }
	return strings.Join(attrs, ",")
}

Two problems with the escaper:

  1. Spaces (and other non-unreserved characters) are not encoded at all. The workflow name: flows in verbatim, so any space survives into the emitted value.
  2. , and = are backslash-escaped (\,, \=) rather than percent-encoded. The OTel spec requires percent-encoding, not backslash escaping, so the current approach is also non-conformant (it would break a workflow name containing , or = on a strict consumer).

Why this violates the spec

Per the OpenTelemetry Resource SDK spec, OTEL_RESOURCE_ATTRIBUTES is a list of key=value pairs whose values are percent-encoded (RFC 3986 §2.1):

  • , and = MUST be percent-encoded; other characters MAY be.
  • "On any decoding failure, the entire environment variable value SHOULD be discarded and an error SHOULD be reported."

That last clause is exactly the observed symptom: one malformed value (the space) causes a conformant consumer to drop the whole variable, not just the offending attribute. (The format descends from the W3C Baggage baggage-octet grammar, which explicitly disallows space, comma, semicolon, backslash, and DQUOTE in values.)

Reproduction

  1. A workflow with a space in its name and OTLP observability enabled:
    ---
    name: My Workflow
    engine: claude
    observability:
      otlp:
        endpoint: ${{ secrets.OTLP_ENDPOINT }}
    ---
  2. gh aw compile → the generated lock has OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=My Workflow,...' (raw space) at workflow level.
  3. Run it and inspect the engine's exported spans (Claude Code CLI). The spans' resource carries none of the OTEL_RESOURCE_ATTRIBUTES keys (only the engine's built-ins such as service.name/service.version), because the engine's OTel SDK discarded the malformed variable.

For contrast, setting the name to My_Workflow (no space) makes all the attributes appear — confirming the space is the trigger.

Suggested fix

Percent-encode resource-attribute values per RFC 3986 instead of backslash-escaping. At minimum encode space, ,, and = (%20, %2C, %3D); ideally percent-encode everything outside the unreserved set. Compliant consumers (including gh-aw's own Go SDK) percent-decode on read, so this stays correct everywhere. Example:

// Replace the strings.NewReplacer backslash-escaper with RFC 3986 percent-encoding
// of every byte outside the unreserved set (ALPHA / DIGIT / "-" / "." / "_" / "~").
func escapeOTELResourceAttributeValue(value string) string {
    var b strings.Builder
    for i := 0; i < len(value); i++ {
        c := value[i]
        if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
            c == '-' || c == '.' || c == '_' || c == '~' {
            b.WriteByte(c)
        } else {
            fmt.Fprintf(&b, "%%%02X", c)
        }
    }
    return b.String()
}

(Note: GitHub Actions expression placeholders like ${{ github.repository }} are not run through the escaper today; if any expanded value can contain spaces/reserved chars, those would need encoding at runtime too.)

Happy to send a PR.

Metadata

Metadata

Labels

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