Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ func run(cmd *cobra.Command, args []string) error {
}
}()

// Apply W3C parent context from configured traceId/spanId (spec §4.1.3.6).
// This links the gateway process lifetime span into a pre-existing trace when provided.
ctx = tracing.ParentContext(ctx, tracingCfg)

if tracingProvider.Tracer() != nil {
// Log what InitProvider actually resolved (config already has env var defaults merged via CLI flags)
endpoint := ""
Expand Down
23 changes: 22 additions & 1 deletion internal/config/config_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,16 @@ type GatewayConfig struct {
// Example values: "copilot-swe-agent[bot]", "my-org-bot[bot]"
TrustedBots []string `toml:"trusted_bots" json:"trusted_bots,omitempty"`

// Tracing holds OpenTelemetry OTLP tracing configuration.
// Tracing holds OpenTelemetry OTLP tracing configuration (legacy TOML key).
// New configurations should use the opentelemetry key (spec §4.1.3.6).
// When Endpoint is set, traces are exported to the specified OTLP endpoint.
// When omitted or Endpoint is empty, a noop tracer is used (zero overhead).
Tracing *TracingConfig `toml:"tracing" json:"tracing,omitempty"`

// Opentelemetry holds OpenTelemetry OTLP tracing configuration per spec §4.1.3.6.
// This key takes precedence over the legacy tracing key when both are present.
// MUST use an HTTPS endpoint when configured.
Opentelemetry *TracingConfig `toml:"opentelemetry" json:"opentelemetry,omitempty"`
}

// HTTPKeepaliveInterval returns the keepalive interval as a time.Duration.
Expand Down Expand Up @@ -349,6 +355,21 @@ func LoadFromFile(path string) (*Config, error) {
return nil, err
}

// Merge opentelemetry key into tracing when present (spec §4.1.3.6).
// opentelemetry takes precedence over the legacy tracing key.
if cfg.Gateway.Opentelemetry != nil {
cfg.Gateway.Tracing = cfg.Gateway.Opentelemetry
cfg.Gateway.Opentelemetry = nil
// Expand ${VAR} expressions in tracing fields before validation.
if err := expandTracingVariables(cfg.Gateway.Tracing); err != nil {
return nil, err
}
// Validate HTTPS endpoint requirement for the opentelemetry section
if err := validateOpenTelemetryConfig(cfg.Gateway.Tracing, true); err != nil {
return nil, err
}
}

// Apply core gateway defaults
applyGatewayDefaults(cfg.Gateway)

Expand Down
35 changes: 27 additions & 8 deletions internal/config/config_stdin.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,33 @@ type StdinConfig struct {
// StdinGatewayConfig represents gateway configuration in stdin JSON format.
// Uses pointers for optional fields to distinguish between unset and zero values.
type StdinGatewayConfig struct {
Port *int `json:"port,omitempty"`
APIKey string `json:"apiKey,omitempty"`
Domain string `json:"domain,omitempty"`
StartupTimeout *int `json:"startupTimeout,omitempty"`
ToolTimeout *int `json:"toolTimeout,omitempty"`
KeepaliveInterval *int `json:"keepaliveInterval,omitempty"`
PayloadDir string `json:"payloadDir,omitempty"`
TrustedBots []string `json:"trustedBots,omitempty"`
Port *int `json:"port,omitempty"`
APIKey string `json:"apiKey,omitempty"`
Domain string `json:"domain,omitempty"`
StartupTimeout *int `json:"startupTimeout,omitempty"`
ToolTimeout *int `json:"toolTimeout,omitempty"`
KeepaliveInterval *int `json:"keepaliveInterval,omitempty"`
PayloadDir string `json:"payloadDir,omitempty"`
TrustedBots []string `json:"trustedBots,omitempty"`
OpenTelemetry *StdinOpenTelemetryConfig `json:"opentelemetry,omitempty"`
}

// StdinOpenTelemetryConfig represents the OpenTelemetry configuration in stdin JSON format (spec §4.1.3.6).
type StdinOpenTelemetryConfig struct {
// Endpoint is the OTLP/HTTP collector URL. MUST be HTTPS. Supports ${VAR} expansion.
Endpoint string `json:"endpoint"`

// Headers are HTTP headers for export requests (e.g. auth tokens). Values support ${VAR}.
Headers map[string]string `json:"headers,omitempty"`

// TraceID is the parent trace ID (32-char lowercase hex, W3C format). Supports ${VAR}.
TraceID string `json:"traceId,omitempty"`

// SpanID is the parent span ID (16-char lowercase hex, W3C format). Ignored without TraceID. Supports ${VAR}.
SpanID string `json:"spanId,omitempty"`

// ServiceName is the service.name resource attribute. Default: "mcp-gateway".
ServiceName string `json:"serviceName,omitempty"`
}

// StdinGuardConfig represents a guard configuration in stdin JSON format.
Expand Down
56 changes: 49 additions & 7 deletions internal/config/config_tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,48 @@ const DefaultTracingServiceName = "mcp-gateway"
// - OTEL_EXPORTER_OTLP_ENDPOINT — overrides Endpoint
// - OTEL_SERVICE_NAME — overrides ServiceName
//
// Example TOML:
// Example TOML (spec §4.1.3.6, using the opentelemetry section):
//
// [gateway.tracing]
// endpoint = "http://localhost:4318"
// [gateway.opentelemetry]
// endpoint = "https://otel-collector.example.com"
// service_name = "mcp-gateway"
// sample_rate = 1.0
// trace_id = "4bf92f3577b34da6a3ce929d0e0e4736"
// span_id = "00f067aa0ba902b7"
//
// [gateway.opentelemetry.headers]
// Authorization = "Bearer ${OTEL_TOKEN}"
type TracingConfig struct {
// Endpoint is the OTLP HTTP endpoint to export traces to.
// Example: "http://localhost:4318" (Jaeger, Grafana Tempo, Honeycomb, etc.)
// When using the opentelemetry section (spec §4.1.3.6), this MUST be an HTTPS URL.
// If empty, tracing is disabled and a noop tracer is used.
Endpoint string `toml:"endpoint" json:"endpoint,omitempty"`

// Headers are HTTP headers sent with every OTLP export request (e.g. auth tokens).
// Header values support ${VAR} variable expansion (expanded at config load time).
Headers map[string]string `toml:"headers" json:"headers,omitempty"`

// TraceID is an optional W3C trace ID (32-char lowercase hex) used to construct the
// parent traceparent header, linking gateway spans into a pre-existing trace.
// Supports ${VAR} variable expansion (expanded at config load time).
// Must be 32 lowercase hex characters and must not be all zeros.
TraceID string `toml:"trace_id" json:"traceId,omitempty"`

// SpanID is an optional W3C span ID (16-char lowercase hex) paired with TraceID
// to construct the parent traceparent header. Ignored when TraceID is absent.
// Supports ${VAR} variable expansion (expanded at config load time).
// Must be 16 lowercase hex characters and must not be all zeros.
SpanID string `toml:"span_id" json:"spanId,omitempty"`
Comment on lines +34 to +48
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments state tracing Headers/TraceID/SpanID “support ${VAR} variable expansion”, but TOML config loading doesn’t appear to perform ${VAR} expansion for these fields (stdin JSON does via ExpandRawJSONVariables). Consider clarifying the comment to match actual behavior, or implementing variable expansion for TOML-loaded tracing config so the documented behavior is true.

Copilot uses AI. Check for mistakes.

// ServiceName is the service name reported in traces.
// Defaults to "mcp-gateway".
ServiceName string `toml:"service_name" json:"service_name,omitempty"`
ServiceName string `toml:"service_name" json:"serviceName,omitempty"`

// SampleRate controls the fraction of traces that are sampled and exported.
// Valid range: 0.0 (no sampling) to 1.0 (sample everything).
// Defaults to 1.0 (100% sampling).
// Uses a pointer so that 0.0 can be distinguished from "unset".
SampleRate *float64 `toml:"sample_rate" json:"sample_rate,omitempty"`
// Note: SampleRate is a gateway extension field not present in spec §4.1.3.6.
SampleRate *float64 `toml:"sample_rate" json:"sampleRate,omitempty"`
}

// GetSampleRate returns the configured sample rate, defaulting to 1.0 if unset.
Expand All @@ -55,4 +76,25 @@ func init() {
}
}
})

// Register stdin converter for the opentelemetry gateway config field (spec §4.1.3.6).
RegisterStdinConverter(func(cfg *Config, stdinCfg *StdinConfig) {
if stdinCfg.Gateway == nil || stdinCfg.Gateway.OpenTelemetry == nil {
return
}
otel := stdinCfg.Gateway.OpenTelemetry
if cfg.Gateway == nil {
cfg.Gateway = &GatewayConfig{}
}
cfg.Gateway.Tracing = &TracingConfig{
Endpoint: otel.Endpoint,
Headers: otel.Headers,
TraceID: otel.TraceID,
SpanID: otel.SpanID,
ServiceName: otel.ServiceName,
}
if cfg.Gateway.Tracing.ServiceName == "" {
cfg.Gateway.Tracing.ServiceName = DefaultTracingServiceName
}
})
}
Loading
Loading