From fb319bfbd32418f7cbfd5900bc90f80320cb09b4 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Mon, 24 Aug 2026 15:56:30 +0000 Subject: [PATCH 1/2] otlptracegrpc: fix env endpoint URL parsing Signed-off-by: Mridankan Mandal --- CHANGELOG.md | 1 + .../internal/otlpconfig/envconfig.go | 12 +++++++- .../internal/otlpconfig/options_test.go | 28 ++++++++++++++++++- .../internal/otlpconfig/envconfig.go | 12 +++++++- .../internal/otlpconfig/options_test.go | 28 ++++++++++++++++++- .../otlptrace/otlpconfig/envconfig.go.tmpl | 12 +++++++- .../otlptrace/otlpconfig/options_test.go.tmpl | 28 ++++++++++++++++++- 7 files changed, 115 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e36c319674e..7a944a39c8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The next release will require at least [Go 1.26]. - Name span events created from OpenTracing logs after the `event` log field, falling back to `log`, instead of always using an empty name in `go.opentelemetry.io/otel/bridge/opentracing`. (#8648) - Count exception attributes omitted due to the attribute count limit as dropped in `go.opentelemetry.io/otel/sdk/log`. (#8796) - Encode `NaN` and infinite double attribute values as strings in OTLP/HTTP JSON requests from `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#8775) +- Ignore HTTP(S) paths when deriving gRPC trace exporter endpoints from `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. - Prevent log record and instrumentation scope attributes with empty keys from reaching processors and exporters in `go.opentelemetry.io/otel/sdk/log`. (#8797) - Fix a data race when span attributes are read concurrently in `go.opentelemetry.io/otel/sdk/trace`. (#8706) - Prevent a panic in `(*Set).Filter` when called on a nil receiver in `go.opentelemetry.io/otel/attribute`. (#8792) diff --git a/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/envconfig.go b/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/envconfig.go index 3bfc2a6f3fc..ce41a22ebfe 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/envconfig.go +++ b/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/envconfig.go @@ -119,10 +119,20 @@ func withEndpointScheme(u *url.URL) GenericOption { } func withEndpointForGRPC(u *url.URL) func(cfg Config) Config { + target := u.String() + switch strings.ToLower(u.Scheme) { + case "http", "https": + target = u.Host + case "": + if u.Host != "" { + target = path.Join(u.Host, u.Path) + } + } + return func(cfg Config) Config { // For OTLP/gRPC endpoints, this is the target to which the // exporter is going to send telemetry. - cfg.Traces.Endpoint = path.Join(u.Host, u.Path) + cfg.Traces.Endpoint = target return cfg } } diff --git a/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/options_test.go b/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/options_test.go index ec31c20fe65..18f51c2571e 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/options_test.go +++ b/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/options_test.go @@ -179,13 +179,27 @@ func TestConfigs(t *testing.T) { asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance assert.False(t, c.Traces.Insecure) if grpcOption { - assert.Equal(t, "env.endpoint/prefix", c.Traces.Endpoint) + assert.Equal(t, "env.endpoint", c.Traces.Endpoint) } else { assert.Equal(t, "env.endpoint", c.Traces.Endpoint) assert.Equal(t, "/prefix/v1/traces", c.Traces.URLPath) } }, }, + { + name: "Test Environment Signal Specific Endpoint With Path", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env.traces.endpoint/prefix", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance + assert.True(t, c.Traces.Insecure) + assert.Equal(t, "env.traces.endpoint", c.Traces.Endpoint) + if !grpcOption { + assert.Equal(t, "/prefix", c.Traces.URLPath) + } + }, + }, { name: "Test Environment Signal Specific Endpoint", env: map[string]string{ @@ -200,6 +214,18 @@ func TestConfigs(t *testing.T) { } }, }, + { + name: "Test Environment Unix Endpoint", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "unix:///tmp/otel.sock", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance + assert.True(t, c.Traces.Insecure) + if grpcOption { + assert.Equal(t, "unix:///tmp/otel.sock", c.Traces.Endpoint) + } + }, + }, { name: "Test Mixed Environment and With Endpoint", opts: []GenericOption{ diff --git a/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/envconfig.go b/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/envconfig.go index 96f857f9787..e129bd7d772 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/envconfig.go +++ b/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/envconfig.go @@ -119,10 +119,20 @@ func withEndpointScheme(u *url.URL) GenericOption { } func withEndpointForGRPC(u *url.URL) func(cfg Config) Config { + target := u.String() + switch strings.ToLower(u.Scheme) { + case "http", "https": + target = u.Host + case "": + if u.Host != "" { + target = path.Join(u.Host, u.Path) + } + } + return func(cfg Config) Config { // For OTLP/gRPC endpoints, this is the target to which the // exporter is going to send telemetry. - cfg.Traces.Endpoint = path.Join(u.Host, u.Path) + cfg.Traces.Endpoint = target return cfg } } diff --git a/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/options_test.go b/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/options_test.go index e0f67523483..aacf8d87814 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/options_test.go +++ b/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/options_test.go @@ -179,13 +179,27 @@ func TestConfigs(t *testing.T) { asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance assert.False(t, c.Traces.Insecure) if grpcOption { - assert.Equal(t, "env.endpoint/prefix", c.Traces.Endpoint) + assert.Equal(t, "env.endpoint", c.Traces.Endpoint) } else { assert.Equal(t, "env.endpoint", c.Traces.Endpoint) assert.Equal(t, "/prefix/v1/traces", c.Traces.URLPath) } }, }, + { + name: "Test Environment Signal Specific Endpoint With Path", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env.traces.endpoint/prefix", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance + assert.True(t, c.Traces.Insecure) + assert.Equal(t, "env.traces.endpoint", c.Traces.Endpoint) + if !grpcOption { + assert.Equal(t, "/prefix", c.Traces.URLPath) + } + }, + }, { name: "Test Environment Signal Specific Endpoint", env: map[string]string{ @@ -200,6 +214,18 @@ func TestConfigs(t *testing.T) { } }, }, + { + name: "Test Environment Unix Endpoint", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "unix:///tmp/otel.sock", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance + assert.True(t, c.Traces.Insecure) + if grpcOption { + assert.Equal(t, "unix:///tmp/otel.sock", c.Traces.Endpoint) + } + }, + }, { name: "Test Mixed Environment and With Endpoint", opts: []GenericOption{ diff --git a/internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl b/internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl index 50fdac66ee7..20d8045cb56 100644 --- a/internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl +++ b/internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl @@ -119,10 +119,20 @@ func withEndpointScheme(u *url.URL) GenericOption { } func withEndpointForGRPC(u *url.URL) func(cfg Config) Config { + target := u.String() + switch strings.ToLower(u.Scheme) { + case "http", "https": + target = u.Host + case "": + if u.Host != "" { + target = path.Join(u.Host, u.Path) + } + } + return func(cfg Config) Config { // For OTLP/gRPC endpoints, this is the target to which the // exporter is going to send telemetry. - cfg.Traces.Endpoint = path.Join(u.Host, u.Path) + cfg.Traces.Endpoint = target return cfg } } diff --git a/internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl b/internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl index c22da8592c3..b6cc71e0ade 100644 --- a/internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl +++ b/internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl @@ -179,13 +179,27 @@ func TestConfigs(t *testing.T) { asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance assert.False(t, c.Traces.Insecure) if grpcOption { - assert.Equal(t, "env.endpoint/prefix", c.Traces.Endpoint) + assert.Equal(t, "env.endpoint", c.Traces.Endpoint) } else { assert.Equal(t, "env.endpoint", c.Traces.Endpoint) assert.Equal(t, "/prefix/v1/traces", c.Traces.URLPath) } }, }, + { + name: "Test Environment Signal Specific Endpoint With Path", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env.traces.endpoint/prefix", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance + assert.True(t, c.Traces.Insecure) + assert.Equal(t, "env.traces.endpoint", c.Traces.Endpoint) + if !grpcOption { + assert.Equal(t, "/prefix", c.Traces.URLPath) + } + }, + }, { name: "Test Environment Signal Specific Endpoint", env: map[string]string{ @@ -200,6 +214,18 @@ func TestConfigs(t *testing.T) { } }, }, + { + name: "Test Environment Unix Endpoint", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "unix:///tmp/otel.sock", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance + assert.True(t, c.Traces.Insecure) + if grpcOption { + assert.Equal(t, "unix:///tmp/otel.sock", c.Traces.Endpoint) + } + }, + }, { name: "Test Mixed Environment and With Endpoint", opts: []GenericOption{ From cb4a9cac4ad8dd2df82aa4ca171f4d4ecf70be1d Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Mon, 24 Aug 2026 20:38:56 +0000 Subject: [PATCH 2/2] otlptracegrpc: pin env URL parsing Signed-off-by: Mridankan Mandal --- CHANGELOG.md | 2 +- exporters/otlp/otlptrace/otlptracegrpc/doc.go | 5 +++-- .../internal/otlpconfig/options_test.go | 15 +++++++++++++++ exporters/otlp/otlptrace/otlptracegrpc/options.go | 9 +++++---- .../internal/otlpconfig/options_test.go | 15 +++++++++++++++ .../otlptrace/otlpconfig/options_test.go.tmpl | 15 +++++++++++++++ 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6efdc2c6a..2841838257b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ The next release will require at least [Go 1.26]. - Name span events created from OpenTracing logs after the `event` log field, falling back to `log`, instead of always using an empty name in `go.opentelemetry.io/otel/bridge/opentracing`. (#8648) - Count exception attributes omitted due to the attribute count limit as dropped in `go.opentelemetry.io/otel/sdk/log`. (#8796) - Encode `NaN` and infinite double attribute values as strings in OTLP/HTTP JSON requests from `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#8775) -- Ignore HTTP(S) paths when deriving gRPC trace exporter endpoints from `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. +- Ignore HTTP(S) paths when deriving gRPC trace exporter endpoints from `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#8852) - Prevent log record and instrumentation scope attributes with empty keys from reaching processors and exporters in `go.opentelemetry.io/otel/sdk/log`. (#8797) - Fix a data race when span attributes are read concurrently in `go.opentelemetry.io/otel/sdk/trace`. (#8706) - Prevent a panic in `(*Set).Filter` when called on a nil receiver in `go.opentelemetry.io/otel/attribute`. (#8792) diff --git a/exporters/otlp/otlptrace/otlptracegrpc/doc.go b/exporters/otlp/otlptrace/otlptracegrpc/doc.go index b01358fc83c..3dbc8427544 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/doc.go +++ b/exporters/otlp/otlptrace/otlptracegrpc/doc.go @@ -12,8 +12,9 @@ The environment variables described below can be used for configuration. OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (default: "https://localhost:4317") - target to which the exporter sends telemetry. The target syntax is defined in https://github.com/grpc/grpc/blob/master/doc/naming.md. -The value must contain a scheme ("http" or "https") and host. -The value may additionally contain a port, and a path. +If the value uses the "http" or "https" scheme, the host and optional port are +used as the gRPC target and any path is ignored. +Values with other schemes are passed to gRPC as-is. The value should not contain a query string or fragment. OTEL_EXPORTER_OTLP_TRACES_ENDPOINT takes precedence over OTEL_EXPORTER_OTLP_ENDPOINT. The configuration can be overridden by [WithEndpoint], [WithEndpointURL], [WithInsecure], and [WithGRPCConn] options. diff --git a/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/options_test.go b/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/options_test.go index 18f51c2571e..828a8cc98dd 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/options_test.go +++ b/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig/options_test.go @@ -186,6 +186,21 @@ func TestConfigs(t *testing.T) { } }, }, + { + name: "Test Environment Endpoint Without Scheme With Path", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "//env.endpoint:4317/prefix", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance + assert.False(t, c.Traces.Insecure) + if grpcOption { + assert.Equal(t, "env.endpoint:4317/prefix", c.Traces.Endpoint) + } else { + assert.Equal(t, "env.endpoint:4317", c.Traces.Endpoint) + assert.Equal(t, "/prefix/v1/traces", c.Traces.URLPath) + } + }, + }, { name: "Test Environment Signal Specific Endpoint With Path", env: map[string]string{ diff --git a/exporters/otlp/otlptrace/otlptracegrpc/options.go b/exporters/otlp/otlptrace/otlptracegrpc/options.go index ea20363c05f..516113b786c 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/options.go +++ b/exporters/otlp/otlptrace/otlptracegrpc/options.go @@ -74,9 +74,10 @@ func WithEndpoint(endpoint string) Option { return wrappedOption{otlpconfig.WithEndpoint(endpoint)} } -// WithEndpointURL sets the target endpoint URL (scheme, host, port, path) -// the Exporter will connect to. The provided endpoint URL should resemble -// "https://example.com:4318/v1/traces". +// WithEndpointURL sets the target endpoint URL (scheme, host, port) the +// Exporter will connect to. The provided endpoint URL should resemble +// "https://example.com:4317". If the URL contains a path, it is ignored by +// the gRPC client. // // If the OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT // environment variable is set, and this option is not passed, that variable @@ -90,7 +91,7 @@ func WithEndpoint(endpoint string) Option { // If an invalid URL is provided, the default value will be kept. // // By default, if an environment variable is not set, and this option is not -// passed, "https://localhost:4317/v1/traces" will be used. +// passed, "https://localhost:4317" will be used. // // This option has no effect if WithGRPCConn is used. func WithEndpointURL(u string) Option { diff --git a/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/options_test.go b/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/options_test.go index aacf8d87814..523f201d0f5 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/options_test.go +++ b/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig/options_test.go @@ -186,6 +186,21 @@ func TestConfigs(t *testing.T) { } }, }, + { + name: "Test Environment Endpoint Without Scheme With Path", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "//env.endpoint:4317/prefix", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance + assert.False(t, c.Traces.Insecure) + if grpcOption { + assert.Equal(t, "env.endpoint:4317/prefix", c.Traces.Endpoint) + } else { + assert.Equal(t, "env.endpoint:4317", c.Traces.Endpoint) + assert.Equal(t, "/prefix/v1/traces", c.Traces.URLPath) + } + }, + }, { name: "Test Environment Signal Specific Endpoint With Path", env: map[string]string{ diff --git a/internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl b/internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl index b6cc71e0ade..ad0c016e397 100644 --- a/internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl +++ b/internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl @@ -186,6 +186,21 @@ func TestConfigs(t *testing.T) { } }, }, + { + name: "Test Environment Endpoint Without Scheme With Path", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "//env.endpoint:4317/prefix", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { //nolint:revive // interface compliance + assert.False(t, c.Traces.Insecure) + if grpcOption { + assert.Equal(t, "env.endpoint:4317/prefix", c.Traces.Endpoint) + } else { + assert.Equal(t, "env.endpoint:4317", c.Traces.Endpoint) + assert.Equal(t, "/prefix/v1/traces", c.Traces.URLPath) + } + }, + }, { name: "Test Environment Signal Specific Endpoint With Path", env: map[string]string{