diff --git a/commons/opentelemetry/otel.go b/commons/opentelemetry/otel.go index 03717d9..fcb7980 100644 --- a/commons/opentelemetry/otel.go +++ b/commons/opentelemetry/otel.go @@ -8,6 +8,7 @@ import ( "fmt" "maps" "net/http" + "os" "reflect" "strconv" "strings" @@ -99,6 +100,7 @@ func NewTelemetry(cfg TelemetryConfig) (*Telemetry, error) { } normalizeEndpoint(&cfg) + normalizeEndpointEnvVars() if cfg.EnableTelemetry && strings.TrimSpace(cfg.CollectorExporterEndpoint) == "" { return handleEmptyEndpoint(cfg) @@ -143,6 +145,25 @@ func normalizeEndpoint(cfg *TelemetryConfig) { } } +// normalizeEndpointEnvVars ensures OTEL exporter endpoint environment variables +// contain a URL scheme. The OTEL SDK's envconfig reads these via url.Parse(), +// which fails on bare "host:port" values. Adding "http://" prevents noisy +// "parse url" errors from the SDK's internal logger. +func normalizeEndpointEnvVars() { + for _, key := range []string{ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + } { + v := strings.TrimSpace(os.Getenv(key)) + if v == "" || strings.HasPrefix(v, "http://") || strings.HasPrefix(v, "https://") { + continue + } + + _ = os.Setenv(key, "http://"+v) + } +} + // handleEmptyEndpoint handles the case where telemetry is enabled but the collector // endpoint is empty, returning noop providers installed as globals. func handleEmptyEndpoint(cfg TelemetryConfig) (*Telemetry, error) { diff --git a/commons/opentelemetry/otel_test.go b/commons/opentelemetry/otel_test.go index 3161dd0..ae970a2 100644 --- a/commons/opentelemetry/otel_test.go +++ b/commons/opentelemetry/otel_test.go @@ -4,6 +4,7 @@ package opentelemetry import ( "context" + "os" "strings" "testing" @@ -196,6 +197,89 @@ func TestNewTelemetry_EndpointNormalization(t *testing.T) { } } +// =========================================================================== +// 1c. Endpoint environment variable normalization +// =========================================================================== + +func TestNormalizeEndpointEnvVars(t *testing.T) { + envKeys := []string{ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + } + + tests := []struct { + name string + value string + set bool + expected string + }{ + { + name: "bare host:port gets http scheme", + value: "10.10.0.202:4317", + set: true, + expected: "http://10.10.0.202:4317", + }, + { + name: "hostname:port gets http scheme", + value: "otel-collector:4317", + set: true, + expected: "http://otel-collector:4317", + }, + { + name: "http scheme preserved", + value: "http://otel-collector:4317", + set: true, + expected: "http://otel-collector:4317", + }, + { + name: "https scheme preserved", + value: "https://otel-collector:4317", + set: true, + expected: "https://otel-collector:4317", + }, + { + name: "whitespace trimmed before adding scheme", + value: " 10.10.0.202:4317 ", + set: true, + expected: "http://10.10.0.202:4317", + }, + { + name: "empty value skipped", + value: "", + set: true, + expected: "", + }, + { + name: "whitespace-only value skipped", + value: " ", + set: true, + expected: " ", + }, + { + name: "unset env var skipped", + value: "", + set: false, + expected: "", + }, + } + + for _, tt := range tests { + for _, key := range envKeys { + t.Run(tt.name+"/"+key, func(t *testing.T) { + if tt.set { + t.Setenv(key, tt.value) + } + + normalizeEndpointEnvVars() + + got := os.Getenv(key) + assert.Equal(t, tt.expected, got) + }) + } + } +} + // =========================================================================== // 2. Telemetry methods on nil receiver // ===========================================================================