From a7939feb88d12d692a793922a30dce571cd5d246 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:19:02 +0000 Subject: [PATCH 1/3] Initial plan From 1a7de8756c9ae8f850c648ed597edddbeae359af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:33:44 +0000 Subject: [PATCH 2/3] Add OTLP resource attributes support Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schema_test.go | 24 ++++ pkg/parser/schemas/main_workflow_schema.json | 7 + .../compiler_orchestrator_workflow.go | 23 ++- .../compiler_orchestrator_workflow_test.go | 36 +++++ pkg/workflow/compiler_validators.go | 1 + pkg/workflow/frontmatter_types.go | 9 ++ pkg/workflow/observability_otlp.go | 123 +++++++++++++--- pkg/workflow/observability_otlp_test.go | 131 ++++++++++++++++++ 8 files changed, 333 insertions(+), 21 deletions(-) diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index a947eae3194..998f93d30a6 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -1170,6 +1170,30 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_OTLPGitHubAppImpli } } +func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_OTLPResourceAttributes(t *testing.T) { + frontmatter := map[string]any{ + "name": "OTLP resource attributes config", + "on": map[string]any{ + "issues": map[string]any{ + "types": []any{"opened"}, + }, + }, + "observability": map[string]any{ + "otlp": map[string]any{ + "resource-attributes": map[string]any{ + "my.target-repo": "${{ github.repository }}", + "my.event": "${{ github.event_name }}", + }, + }, + }, + } + + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/otlp-resource-attributes-schema-test.md") + if err != nil { + t.Fatalf("expected observability.otlp.resource-attributes to pass schema validation, got: %v", err) + } +} + func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_OTLPGitHubAppAudienceRejected(t *testing.T) { frontmatter := map[string]any{ "name": "OTLP github-app audience rejection", diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index b5040a2d38e..b82a1b63d48 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -10601,6 +10601,13 @@ "default": "error", "description": "How to handle missing OTLP endpoint/header values at runtime (for example from unset secrets). 'error' fails workflow startup (default), 'warn' logs a warning and skips MCP gateway OTLP configuration, and 'ignore' skips MCP gateway OTLP configuration without warning. This affects MCP gateway setup only; workflow-level OTEL_* environment variables are still injected." }, + "resource-attributes": { + "type": "object", + "description": "Additional OTEL_RESOURCE_ATTRIBUTES entries to append to the standard gh-aw/GitHub resource attributes. Values may be static strings or GitHub Actions expressions such as '${{ github.repository }}'. Do not use secrets.* or vars.* expressions here: resource attributes are exported to external tracing backends and are not treated as secret values.", + "additionalProperties": { + "type": "string" + } + }, "github-app": { "description": "Optional runtime authentication for OTLP export. Supports GitHub App credentials (client-id/app-id + private-key) for token minting, or implicit GitHub OIDC mode when the github-app object is present without credentials.", "type": "object", diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 3b6a5c5fc38..1582881098b 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -252,11 +252,23 @@ func (c *Compiler) mergeImportedObservability(workflowData *WorkflowData, merged extractOTLPCustomAttributesFromObsMap(mainObs), extractOTLPCustomAttributesFromObsMap(importedObs), ) + mergedResourceAttrs := mergeOTLPStringMaps( + extractOTLPResourceAttributesFromObsMap(mainObs), + extractOTLPResourceAttributesFromObsMap(importedObs), + ) githubApp := extractRawOTLPGitHubAppMap(mainObs) if githubApp == nil { githubApp = extractRawOTLPGitHubAppMap(importedObs) } - applyMergedRawObservability(workflowData.RawFrontmatter, mergedEndpoints, mergedAttrs, githubApp, mainCount, importAdded) + applyMergedRawObservability( + workflowData.RawFrontmatter, + mergedEndpoints, + mergedAttrs, + mergedResourceAttrs, + githubApp, + mainCount, + importAdded, + ) } func extractRawObservabilityMap(rawFrontmatter map[string]any) map[string]any { @@ -290,11 +302,12 @@ func applyMergedRawObservability( rawFrontmatter map[string]any, mergedEndpoints []any, mergedAttrs map[string]string, + mergedResourceAttrs map[string]string, githubApp map[string]any, mainCount int, importAdded int, ) { - if len(mergedEndpoints) == 0 && len(mergedAttrs) == 0 && githubApp == nil { + if len(mergedEndpoints) == 0 && len(mergedAttrs) == 0 && len(mergedResourceAttrs) == 0 && githubApp == nil { return } newOTLP := map[string]any{} @@ -304,6 +317,9 @@ func applyMergedRawObservability( if len(mergedAttrs) > 0 { newOTLP["attributes"] = mergedAttrs } + if len(mergedResourceAttrs) > 0 { + newOTLP["resource-attributes"] = mergedResourceAttrs + } if githubApp != nil { newOTLP["github-app"] = githubApp } @@ -312,6 +328,9 @@ func applyMergedRawObservability( if len(mergedAttrs) > 0 { orchestratorWorkflowLog.Printf("Merged %d custom OTLP attributes into RawFrontmatter", len(mergedAttrs)) } + if len(mergedResourceAttrs) > 0 { + orchestratorWorkflowLog.Printf("Merged %d OTLP resource attributes into RawFrontmatter", len(mergedResourceAttrs)) + } } func (c *Compiler) mergeWorkflowEnv(frontmatter map[string]any, workflowData *WorkflowData, importsResult *parser.ImportsResult) error { diff --git a/pkg/workflow/compiler_orchestrator_workflow_test.go b/pkg/workflow/compiler_orchestrator_workflow_test.go index 0209c9e4665..7f05a479726 100644 --- a/pkg/workflow/compiler_orchestrator_workflow_test.go +++ b/pkg/workflow/compiler_orchestrator_workflow_test.go @@ -3,6 +3,7 @@ package workflow import ( + "encoding/json" "os" "path/filepath" "strings" @@ -350,6 +351,41 @@ func TestMergeRawOTLPEndpoints_DedupesAndCountsSources(t *testing.T) { assert.Equal(t, "https://import.example/otlp", mergedEndpoints[2].(map[string]any)["url"]) } +func TestMergeImportedObservability_MergesResourceAttributesWithMainPrecedence(t *testing.T) { + importedObsJSON, err := json.Marshal(map[string]any{ + "otlp": map[string]any{ + "resource-attributes": map[string]any{ + "shared.key": "from-import", + "import.only.key": "import-value", + }, + }, + }) + require.NoError(t, err) + + workflowData := &WorkflowData{ + RawFrontmatter: map[string]any{ + "observability": map[string]any{ + "otlp": map[string]any{ + "resource-attributes": map[string]any{ + "shared.key": "from-main", + "main.only.key": "main-value", + }, + }, + }, + }, + } + + NewCompiler().mergeImportedObservability(workflowData, string(importedObsJSON)) + + obs := workflowData.RawFrontmatter["observability"].(map[string]any) + otlp := obs["otlp"].(map[string]any) + assert.Equal(t, map[string]string{ + "shared.key": "from-main", + "main.only.key": "main-value", + "import.only.key": "import-value", + }, otlp["resource-attributes"]) +} + func TestBuildMergedEnvSources_MainWorkflowWins(t *testing.T) { mergedEnv := map[string]any{ "MAIN_ONLY": "1", diff --git a/pkg/workflow/compiler_validators.go b/pkg/workflow/compiler_validators.go index 1acf78e73b1..ebf7748243c 100644 --- a/pkg/workflow/compiler_validators.go +++ b/pkg/workflow/compiler_validators.go @@ -162,6 +162,7 @@ func (c *Compiler) validateCoreToolConfiguration(workflowData *WorkflowData, mar {logMessage: "Validating network allowed domains", validateFn: func() error { return c.validateNetworkAllowedDomains(workflowData.NetworkPermissions) }}, {logMessage: "Validating network firewall configuration", validateFn: func() error { return validateNetworkFirewallConfig(workflowData.NetworkPermissions) }}, {logMessage: "Validating safe-outputs allow-workflows", validateFn: func() error { return validateSafeOutputsAllowWorkflows(workflowData.SafeOutputs) }}, + {logMessage: "Validating OTLP resource attributes", validateFn: func() error { return validateOTLPResourceAttributes(workflowData) }}, {logMessage: "Validating labels", validateFn: func() error { return validateLabels(workflowData) }}, {logMessage: "Validating workflow_dispatch input requirements for command triggers", validateFn: func() error { return validateCommandWorkflowDispatchInputs(workflowData) }}, {logMessage: "Validating max-daily-ai-credits frontmatter", validateFn: func() error { return validateMaxDailyAICFrontmatter(workflowData) }}, diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go index 5fb9d4e4d74..0527d350eb2 100644 --- a/pkg/workflow/frontmatter_types.go +++ b/pkg/workflow/frontmatter_types.go @@ -260,6 +260,15 @@ type OTLPConfig struct { // user.id: "{{ github.actor }}" Attributes map[string]string `json:"attributes,omitempty"` + // ResourceAttributes defines additional OTEL_RESOURCE_ATTRIBUTES entries to + // append to the standard gh-aw/GitHub resource attributes. + // + // Values may be static strings or GitHub Actions expressions such as + // ${{ github.repository }}. Do not use secrets.* or vars.* expressions here: + // resource attributes are exported to external tracing backends and are not + // treated as secret values. + ResourceAttributes map[string]string `json:"resource-attributes,omitempty"` + // GitHubApp configures runtime OTLP authentication via the `github-app` key. // Supported values: // github-app: diff --git a/pkg/workflow/observability_otlp.go b/pkg/workflow/observability_otlp.go index dee76bc80f2..96b75b9871e 100644 --- a/pkg/workflow/observability_otlp.go +++ b/pkg/workflow/observability_otlp.go @@ -16,6 +16,7 @@ import ( var otlpLog = logger.New("workflow:observability_otlp") var sentryEndpointExpressionPattern = regexp.MustCompile(`(?i)^\$\{\{\s*secrets\.` + regexp.QuoteMeta(constants.OTELSentryEndpointSecretName) + `\s*\}\}$`) +var otlpResourceAttributeSecretRefPattern = regexp.MustCompile(`\$\{\{\s*(secrets|vars)\.`) func normalizeOTLPHeadersForEndpoint(raw any, endpoint string) string { if raw == nil { @@ -324,6 +325,32 @@ func isOTLPAttributesPresent(data *WorkflowData) bool { return strings.Contains(data.Env, "GH_AW_OTLP_ATTRIBUTES") } +func getOTLPResourceAttributes(workflowData *WorkflowData) map[string]string { + if workflowData == nil { + return nil + } + resourceAttrs := collectOTLPResourceAttributes(workflowData.RawFrontmatter) + if len(resourceAttrs) == 0 && + workflowData.ParsedFrontmatter != nil && + workflowData.ParsedFrontmatter.Observability != nil && + workflowData.ParsedFrontmatter.Observability.OTLP != nil { + resourceAttrs = workflowData.ParsedFrontmatter.Observability.OTLP.ResourceAttributes + } + return resourceAttrs +} + +func validateOTLPResourceAttributes(workflowData *WorkflowData) error { + for key, value := range getOTLPResourceAttributes(workflowData) { + if otlpResourceAttributeSecretRefPattern.MatchString(value) { + return fmt.Errorf( + "observability.otlp.resource-attributes.%s must not reference secrets.* or vars.*; OTEL resource attributes are exported to tracing backends and are not treated as secret values", + key, + ) + } + } + return nil +} + // generateOTLPAttributesMaskStep returns a GitHub Actions step that runs // mask_otlp_attributes.sh to issue the ::add-mask:: workflow command for every // value in the GH_AW_OTLP_ATTRIBUTES JSON object. Masking the values prevents @@ -345,8 +372,8 @@ type otlpEndpointEntry struct { } // collectOTLPCustomAttributes reads the `observability.otlp.attributes` map from -// a raw frontmatter map and returns it as a map[string]string. Only string values -// are accepted; non-string values are silently ignored. Returns nil when the +// a raw frontmatter map and returns it as a map[string]string. Only string values +// are accepted; non-string values are silently ignored. Returns nil when the // field is absent or empty. func collectOTLPCustomAttributes(frontmatter map[string]any) map[string]string { if frontmatter == nil { @@ -360,15 +387,34 @@ func collectOTLPCustomAttributes(frontmatter map[string]any) map[string]string { if !ok { return nil } - return extractOTLPCustomAttributesFromObsMap(obsMap) + return extractOTLPStringMapFromObsMap(obsMap, "attributes") +} + +// collectOTLPResourceAttributes reads the +// `observability.otlp.resource-attributes` map from a raw frontmatter map and +// returns it as a map[string]string. Only string values are accepted; non-string +// values are silently ignored. Returns nil when the field is absent or empty. +func collectOTLPResourceAttributes(frontmatter map[string]any) map[string]string { + if frontmatter == nil { + return nil + } + obsAny, ok := frontmatter["observability"] + if !ok { + return nil + } + obsMap, ok := obsAny.(map[string]any) + if !ok { + return nil + } + return extractOTLPStringMapFromObsMap(obsMap, "resource-attributes") } -// extractOTLPCustomAttributesFromObsMap reads the `otlp.attributes` map from -// a raw observability section map (i.e. the value of the "observability" key in -// the frontmatter) and returns it as a map[string]string. Only string values are -// accepted; non-string values are silently ignored. Returns nil when the field is +// extractOTLPStringMapFromObsMap reads an `otlp.` string map from a +// raw observability section map (i.e. the value of the "observability" key in +// the frontmatter) and returns it as a map[string]string. Only string values are +// accepted; non-string values are silently ignored. Returns nil when the field is // absent or empty. -func extractOTLPCustomAttributesFromObsMap(obsMap map[string]any) map[string]string { +func extractOTLPStringMapFromObsMap(obsMap map[string]any, fieldName string) map[string]string { if obsMap == nil { return nil } @@ -380,7 +426,7 @@ func extractOTLPCustomAttributesFromObsMap(obsMap map[string]any) map[string]str if !ok { return nil } - attrsAny, ok := otlpMap["attributes"] + attrsAny, ok := otlpMap[fieldName] if !ok { return nil } @@ -400,6 +446,19 @@ func extractOTLPCustomAttributesFromObsMap(obsMap map[string]any) map[string]str return result } +// extractOTLPCustomAttributesFromObsMap reads the `otlp.attributes` map from a +// raw observability section map and returns it as a map[string]string. +func extractOTLPCustomAttributesFromObsMap(obsMap map[string]any) map[string]string { + return extractOTLPStringMapFromObsMap(obsMap, "attributes") +} + +// extractOTLPResourceAttributesFromObsMap reads the +// `otlp.resource-attributes` map from a raw observability section map and +// returns it as a map[string]string. +func extractOTLPResourceAttributesFromObsMap(obsMap map[string]any) map[string]string { + return extractOTLPStringMapFromObsMap(obsMap, "resource-attributes") +} + // encodeOTLPCustomAttributes serialises a map[string]string of custom OTLP span // attributes to a compact JSON string suitable for use as the GH_AW_OTLP_ATTRIBUTES // environment variable. Returns an empty string when the map is nil/empty or @@ -416,10 +475,10 @@ func encodeOTLPCustomAttributes(attrs map[string]string) string { return string(b) } -// mergeOTLPCustomAttributes merges two custom-attribute maps; values in base -// take precedence over values in override when the same key is present in both. -// Returns nil when both inputs are empty. -func mergeOTLPCustomAttributes(base, override map[string]string) map[string]string { +// mergeOTLPStringMaps merges two string maps; values in base take precedence over +// values in override when the same key is present in both. Returns nil when both +// inputs are empty. +func mergeOTLPStringMaps(base, override map[string]string) map[string]string { if len(base) == 0 && len(override) == 0 { return nil } @@ -430,6 +489,12 @@ func mergeOTLPCustomAttributes(base, override map[string]string) map[string]stri return merged } +// mergeOTLPCustomAttributes merges two custom-attribute maps; values in base +// take precedence over values in override when the same key is present in both. +func mergeOTLPCustomAttributes(base, override map[string]string) map[string]string { + return mergeOTLPStringMaps(base, override) +} + // collectAllOTLPEndpoints reads the `observability.otlp.endpoint` field from the raw // frontmatter and returns all configured endpoint entries. The `endpoint` field may be: // @@ -769,22 +834,42 @@ func encodeOTELResourceAttributeValue(value string) string { return strings.ReplaceAll(url.QueryEscape(value), "+", "%20") } +func formatOTELResourceAttribute(key, value string) string { + trimmedKey := strings.TrimSpace(key) + trimmedValue := strings.TrimSpace(value) + if strings.Contains(trimmedValue, "${{") { + return encodeOTELResourceAttributeValue(trimmedKey) + "=" + trimmedValue + } + return encodeOTELResourceAttributeValue(trimmedKey) + "=" + encodeOTELResourceAttributeValue(trimmedValue) +} + func otelResourceAttributes(workflowData *WorkflowData) string { workflowNameAttrValue := "unknown" if workflowData != nil { if workflowName := strings.TrimSpace(workflowData.Name); workflowName != "" { - workflowNameAttrValue = encodeOTELResourceAttributeValue(workflowName) + workflowNameAttrValue = workflowName } } attrs := []string{ - "gh-aw.workflow.name=" + workflowNameAttrValue, - "gh-aw.repository=${{ github.repository }}", - "gh-aw.run.id=${{ github.run_id }}", - "github.run_id=${{ github.run_id }}", + formatOTELResourceAttribute("gh-aw.workflow.name", workflowNameAttrValue), + formatOTELResourceAttribute("gh-aw.repository", "${{ github.repository }}"), + formatOTELResourceAttribute("gh-aw.run.id", "${{ github.run_id }}"), + formatOTELResourceAttribute("github.run_id", "${{ github.run_id }}"), } if engineID := ResolveEngineID(workflowData); engineID != "" { - attrs = append(attrs, "gh-aw.engine.id="+encodeOTELResourceAttributeValue(engineID)) + attrs = append(attrs, formatOTELResourceAttribute("gh-aw.engine.id", engineID)) + } + resourceAttrs := getOTLPResourceAttributes(workflowData) + if len(resourceAttrs) > 0 { + keys := make([]string, 0, len(resourceAttrs)) + for key := range resourceAttrs { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + attrs = append(attrs, formatOTELResourceAttribute(key, resourceAttrs[key])) + } } return strings.Join(attrs, ",") } diff --git a/pkg/workflow/observability_otlp_test.go b/pkg/workflow/observability_otlp_test.go index df2faa5ea80..30278ce55db 100644 --- a/pkg/workflow/observability_otlp_test.go +++ b/pkg/workflow/observability_otlp_test.go @@ -521,6 +521,32 @@ func TestInjectOTLPConfig(t *testing.T) { ) }) + t.Run("appends custom OTLP resource attributes", func(t *testing.T) { + c := newCompiler() + wd := &WorkflowData{ + AI: "copilot", + Name: "triage weekly", + ParsedFrontmatter: &FrontmatterConfig{ + Observability: &ObservabilityConfig{ + OTLP: &OTLPConfig{ + Endpoint: "https://traces.example.com:4317", + ResourceAttributes: map[string]string{ + "my.actor": "${{ github.actor }}", + "my.target repo": "owner/repo,weekly", + }, + }, + }, + }, + } + c.injectOTLPConfig(wd) + + assert.Contains( + t, + wd.Env, + "OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=triage%20weekly,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot,my.actor=${{ github.actor }},my.target%20repo=owner%2Frepo%2Cweekly'", + ) + }) + t.Run("appends domain to existing NetworkPermissions.Allowed", func(t *testing.T) { c := newCompiler() wd := &WorkflowData{ @@ -728,6 +754,27 @@ func TestObservabilityConfigParsing(t *testing.T) { } } +func TestObservabilityConfigParsing_OTLPResourceAttributes(t *testing.T) { + config, err := ParseFrontmatterConfig(map[string]any{ + "observability": map[string]any{ + "otlp": map[string]any{ + "resource-attributes": map[string]any{ + "my.target-repo": "${{ github.repository }}", + "my.event": "repository_dispatch", + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Observability) + require.NotNil(t, config.Observability.OTLP) + assert.Equal(t, map[string]string{ + "my.target-repo": "${{ github.repository }}", + "my.event": "repository_dispatch", + }, config.Observability.OTLP.ResourceAttributes) +} + // TestInjectOTLPConfig_RawFrontmatterFallback verifies that injectOTLPConfig works // when ParsedFrontmatter is nil (e.g. complex engine objects cause ParseFrontmatterConfig // to fail) but the raw frontmatter contains valid OTLP configuration. @@ -1915,6 +1962,90 @@ func TestCollectOTLPCustomAttributes(t *testing.T) { } } +func TestCollectOTLPResourceAttributes(t *testing.T) { + frontmatter := map[string]any{ + "observability": map[string]any{ + "otlp": map[string]any{ + "resource-attributes": map[string]any{ + "my.actor": "${{ github.actor }}", + "my.target-repo": "owner/repo", + "ignored.numeric": 42, + }, + }, + }, + } + + assert.Equal(t, map[string]string{ + "my.actor": "${{ github.actor }}", + "my.target-repo": "owner/repo", + }, collectOTLPResourceAttributes(frontmatter)) +} + +func TestValidateOTLPResourceAttributes(t *testing.T) { + tests := []struct { + name string + workflowData *WorkflowData + errorContains string + }{ + { + name: "allows safe expressions", + workflowData: &WorkflowData{ + ParsedFrontmatter: &FrontmatterConfig{ + Observability: &ObservabilityConfig{ + OTLP: &OTLPConfig{ + ResourceAttributes: map[string]string{ + "my.actor": "${{ github.actor }}", + }, + }, + }, + }, + }, + }, + { + name: "rejects secrets expressions", + workflowData: &WorkflowData{ + RawFrontmatter: map[string]any{ + "observability": map[string]any{ + "otlp": map[string]any{ + "resource-attributes": map[string]any{ + "api.key": "${{ secrets.OTLP_KEY }}", + }, + }, + }, + }, + }, + errorContains: "observability.otlp.resource-attributes.api.key must not reference secrets.* or vars.*", + }, + { + name: "rejects vars expressions", + workflowData: &WorkflowData{ + ParsedFrontmatter: &FrontmatterConfig{ + Observability: &ObservabilityConfig{ + OTLP: &OTLPConfig{ + ResourceAttributes: map[string]string{ + "tenant": "${{ vars.OTLP_TENANT }}", + }, + }, + }, + }, + }, + errorContains: "observability.otlp.resource-attributes.tenant must not reference secrets.* or vars.*", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateOTLPResourceAttributes(tt.workflowData) + if tt.errorContains == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorContains) + }) + } +} + // TestInjectOTLPConfig_CustomAttributes verifies that injectOTLPConfig injects the // GH_AW_OTLP_ATTRIBUTES env var when observability.otlp.attributes is configured. func TestInjectOTLPConfig_CustomAttributes(t *testing.T) { From c7f2d65d16f50da3f597694589dfbe097e50186b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:23:01 +0000 Subject: [PATCH 3/3] Remove redundant mergeOTLPCustomAttributes wrapper, use mergeOTLPStringMaps directly Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_orchestrator_workflow.go | 2 +- pkg/workflow/observability_otlp.go | 6 ------ pkg/workflow/observability_otlp_test.go | 12 ++++++------ 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 1582881098b..3a1c0701101 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -248,7 +248,7 @@ func (c *Compiler) mergeImportedObservability(workflowData *WorkflowData, merged } mainObs := extractRawObservabilityMap(workflowData.RawFrontmatter) mergedEndpoints, mainCount, importAdded := mergeRawOTLPEndpoints(mainObs, importedObs) - mergedAttrs := mergeOTLPCustomAttributes( + mergedAttrs := mergeOTLPStringMaps( extractOTLPCustomAttributesFromObsMap(mainObs), extractOTLPCustomAttributesFromObsMap(importedObs), ) diff --git a/pkg/workflow/observability_otlp.go b/pkg/workflow/observability_otlp.go index 96b75b9871e..f7c13c5f92c 100644 --- a/pkg/workflow/observability_otlp.go +++ b/pkg/workflow/observability_otlp.go @@ -489,12 +489,6 @@ func mergeOTLPStringMaps(base, override map[string]string) map[string]string { return merged } -// mergeOTLPCustomAttributes merges two custom-attribute maps; values in base -// take precedence over values in override when the same key is present in both. -func mergeOTLPCustomAttributes(base, override map[string]string) map[string]string { - return mergeOTLPStringMaps(base, override) -} - // collectAllOTLPEndpoints reads the `observability.otlp.endpoint` field from the raw // frontmatter and returns all configured endpoint entries. The `endpoint` field may be: // diff --git a/pkg/workflow/observability_otlp_test.go b/pkg/workflow/observability_otlp_test.go index 30278ce55db..3b91aa08c24 100644 --- a/pkg/workflow/observability_otlp_test.go +++ b/pkg/workflow/observability_otlp_test.go @@ -2106,29 +2106,29 @@ func TestInjectOTLPConfig_CustomAttributes(t *testing.T) { }) } -// TestMergeOTLPCustomAttributes verifies that mergeOTLPCustomAttributes correctly +// TestMergeOTLPStringMaps verifies that mergeOTLPStringMaps correctly // merges two attribute maps with base taking precedence. -func TestMergeOTLPCustomAttributes(t *testing.T) { +func TestMergeOTLPStringMaps(t *testing.T) { t.Run("nil inputs return nil", func(t *testing.T) { - assert.Nil(t, mergeOTLPCustomAttributes(nil, nil)) + assert.Nil(t, mergeOTLPStringMaps(nil, nil)) }) t.Run("base only is returned as-is", func(t *testing.T) { base := map[string]string{"a": "1"} - result := mergeOTLPCustomAttributes(base, nil) + result := mergeOTLPStringMaps(base, nil) assert.Equal(t, map[string]string{"a": "1"}, result) }) t.Run("override only is returned as-is", func(t *testing.T) { override := map[string]string{"b": "2"} - result := mergeOTLPCustomAttributes(nil, override) + result := mergeOTLPStringMaps(nil, override) assert.Equal(t, map[string]string{"b": "2"}, result) }) t.Run("base keys override the same key from override", func(t *testing.T) { base := map[string]string{"a": "base-value", "b": "base-b"} override := map[string]string{"a": "override-value", "c": "override-c"} - result := mergeOTLPCustomAttributes(base, override) + result := mergeOTLPStringMaps(base, override) require.NotNil(t, result) assert.Equal(t, "base-value", result["a"], "base should win for key 'a'") assert.Equal(t, "base-b", result["b"], "base-only key 'b' should be present")