Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions