{{ message }}
feat: OTLP multi-endpoint fan-out via GH_AW_OTLP_ENDPOINTS#7446
Merged
lpcox merged 4 commits intoJun 12, 2026
Conversation
Copilot
AI
changed the title
[WIP] Add support for OTLP multi-endpoint fan-out in MCP gateway
feat: OTLP multi-endpoint fan-out via GH_AW_OTLP_ENDPOINTS
Jun 12, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds OpenTelemetry OTLP multi-endpoint “fan-out” support so the gateway can export spans to every backend listed in GH_AW_OTLP_ENDPOINTS, rather than only the single OTEL_EXPORTER_OTLP_ENDPOINT.
Changes:
- Added
GH_AW_OTLP_ENDPOINTSparsing and endpoint normalization (signal path handling) to support multiple OTLP/HTTP destinations. - Introduced a
fanoutExporterSpanExporter to forward exports/shutdown across multiple underlying exporters. - Updated tracing initialization + startup logging to distinguish noop vs real SDK providers via
Provider.IsEnabled(), and added tests/docs.
Show a summary per file
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 8/8 changed files
- Comments generated: 6
Comment on lines
+339
to
+344
| displayEndpoint := endpoint | ||
| if displayEndpoint == "" { | ||
| displayEndpoint = os.Getenv("GH_AW_OTLP_ENDPOINTS") | ||
| } | ||
| logger.StartupInfo("OpenTelemetry tracing enabled: endpoint=%s, service=%s, sampleRate=%.2f", | ||
| endpoint, serviceName, sampleRate) | ||
| displayEndpoint, serviceName, sampleRate) |
Comment on lines
+166
to
+168
| if len(exporters) == 0 { | ||
| return nil, fmt.Errorf("failed to create any OTLP trace exporters") | ||
| } |
| assert.Equal(t, 1, ok.shutdowns, "healthy exporter should still be shut down") | ||
| } | ||
|
|
||
| // TestFanoutExporter_ExportSpans_Empty verifies no-op on empty span slice. |
Comment on lines
+27
to
+38
| // ExportSpans exports spans to each underlying exporter in order. Export | ||
| // continues to the next exporter even if the current one returns an error so | ||
| // that a single backend failure does not prevent delivery to the others. All | ||
| // errors are joined and returned. | ||
| func (f *fanoutExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error { | ||
| var errs []error | ||
| for _, exp := range f.exporters { | ||
| if err := exp.ExportSpans(ctx, spans); err != nil { | ||
| errs = append(errs, err) | ||
| } | ||
| } | ||
| return errors.Join(errs...) |
Comment on lines
+27
to
+33
| func TestInitProvider_IsEnabled_Noop(t *testing.T) { | ||
| ctx := context.Background() | ||
| provider, err := tracing.InitProvider(ctx, nil) | ||
| require.NoError(t, err) | ||
| defer provider.Shutdown(ctx) | ||
| assert.False(t, provider.IsEnabled(), "noop provider should report IsEnabled=false") | ||
| } |
Comment on lines
+36
to
+50
| func TestInitProvider_IsEnabled_SDK(t *testing.T) { | ||
| ctx := context.Background() | ||
| cfg := &config.TracingConfig{ | ||
| Endpoint: "http://localhost:14318", | ||
| ServiceName: "test", | ||
| } | ||
| provider, err := tracing.InitProvider(ctx, cfg) | ||
| require.NoError(t, err) | ||
| defer func() { | ||
| shutdownCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) | ||
| defer cancel() | ||
| _ = provider.Shutdown(shutdownCtx) | ||
| }() | ||
| assert.True(t, provider.IsEnabled(), "SDK provider should report IsEnabled=true") | ||
| } |
Collaborator
|
@copilot address review feedback |
Contributor
Author
This was referenced Jun 13, 2026
This was referenced Jun 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

The gateway only exported spans to a single OTLP endpoint (
OTEL_EXPORTER_OTLP_ENDPOINT), so when gh-aw configured multiple backends viaGH_AW_OTLP_ENDPOINTS, non-primary backends received lifecycle spans but zero gateway/tool-call spans.Changes
New
GH_AW_OTLP_ENDPOINTSenv var (comma-separated URLs)otlptracehttpexporter is created per URLOTEL_EXPORTER_OTLP_HEADERS/ configHeadersOTEL_EXPORTER_OTLP_ENDPOINTbehaviour when unsetinternal/tracing/fanout.go— newfanoutExporter(sdktrace.SpanExporter)ExportSpans/Shutdownto every underlying exporterinternal/tracing/config_resolver.go—resolveExtraEndpointsGH_AW_OTLP_ENDPOINTS, normalises each URL with the configured signal path (default/v1/traces)internal/tracing/provider.goInitProviderselects fan-out or single-endpoint based onGH_AW_OTLP_ENDPOINTSIsEnabled() boolto distinguish real SDK providers from noopinternal/cmd/root.goIsEnabled()and falls back toGH_AW_OTLP_ENDPOINTSas display value when primary endpoint is empty