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
34 changes: 28 additions & 6 deletions pkg/utils/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ func SetupOTelSDKWithConfig(ctx context.Context, cfg OTelConfig) (shutdown func(
err = errors.Join(inErr, shutdown(ctx))
}

// Normalize endpoint to include a URL scheme so WithEndpointURL can
// parse it. Bare IP:port values like "127.0.0.1:4317" cause url.Parse
// to fail with "first path segment in URL cannot contain colon".
if cfg.Endpoint != "" {
cfg.Endpoint = endpointURL(cfg.Endpoint, cfg.Insecure)
}

// Create resource with service information.
res, err := newResource(cfg)
if err != nil {
Expand Down Expand Up @@ -281,6 +288,21 @@ func newPropagator(cfg OTelConfig) propagation.TextMapPropagator {
return propagation.NewCompositeTextMapPropagator(propagators...)
}

// endpointURL ensures the endpoint has a URL scheme. The OTel SDK internally
// reads OTEL_EXPORTER_OTLP_ENDPOINT and parses it with url.Parse, which fails
// for bare IP:port values like "127.0.0.1:4317" with "first path segment in
// URL cannot contain colon". Prepending a scheme based on the insecure flag
// produces a valid URL the SDK can parse.
func endpointURL(raw string, insecure bool) string {
if strings.Contains(raw, "://") {
return raw
}
if insecure {
return "http://" + raw
}
return "https://" + raw
}

// newTraceProvider creates a TracerProvider with an OTLP exporter configured based on the protocol setting.
func newTraceProvider(ctx context.Context, cfg OTelConfig, res *resource.Resource) (*trace.TracerProvider, error) {
var exporter trace.SpanExporter
Expand All @@ -289,7 +311,7 @@ func newTraceProvider(ctx context.Context, cfg OTelConfig, res *resource.Resourc
if cfg.Protocol == OTelProtocolHTTP {
var opts []otlptracehttp.Option
if cfg.Endpoint != "" {
opts = append(opts, otlptracehttp.WithEndpoint(cfg.Endpoint))
opts = append(opts, otlptracehttp.WithEndpointURL(cfg.Endpoint))
}
if cfg.Insecure {
opts = append(opts, otlptracehttp.WithInsecure())
Expand All @@ -298,7 +320,7 @@ func newTraceProvider(ctx context.Context, cfg OTelConfig, res *resource.Resourc
} else {
var opts []otlptracegrpc.Option
if cfg.Endpoint != "" {
opts = append(opts, otlptracegrpc.WithEndpoint(cfg.Endpoint))
opts = append(opts, otlptracegrpc.WithEndpointURL(cfg.Endpoint))
}
if cfg.Insecure {
opts = append(opts, otlptracegrpc.WithInsecure())
Expand Down Expand Up @@ -328,7 +350,7 @@ func newMetricsProvider(ctx context.Context, cfg OTelConfig, res *resource.Resou
if cfg.Protocol == OTelProtocolHTTP {
var opts []otlpmetrichttp.Option
if cfg.Endpoint != "" {
opts = append(opts, otlpmetrichttp.WithEndpoint(cfg.Endpoint))
opts = append(opts, otlpmetrichttp.WithEndpointURL(cfg.Endpoint))
}
if cfg.Insecure {
opts = append(opts, otlpmetrichttp.WithInsecure())
Expand All @@ -337,7 +359,7 @@ func newMetricsProvider(ctx context.Context, cfg OTelConfig, res *resource.Resou
} else {
var opts []otlpmetricgrpc.Option
if cfg.Endpoint != "" {
opts = append(opts, otlpmetricgrpc.WithEndpoint(cfg.Endpoint))
opts = append(opts, otlpmetricgrpc.WithEndpointURL(cfg.Endpoint))
}
if cfg.Insecure {
opts = append(opts, otlpmetricgrpc.WithInsecure())
Expand Down Expand Up @@ -366,7 +388,7 @@ func newLoggerProvider(ctx context.Context, cfg OTelConfig, res *resource.Resour
if cfg.Protocol == OTelProtocolHTTP {
var opts []otlploghttp.Option
if cfg.Endpoint != "" {
opts = append(opts, otlploghttp.WithEndpoint(cfg.Endpoint))
opts = append(opts, otlploghttp.WithEndpointURL(cfg.Endpoint))
}
if cfg.Insecure {
opts = append(opts, otlploghttp.WithInsecure())
Expand All @@ -375,7 +397,7 @@ func newLoggerProvider(ctx context.Context, cfg OTelConfig, res *resource.Resour
} else {
var opts []otlploggrpc.Option
if cfg.Endpoint != "" {
opts = append(opts, otlploggrpc.WithEndpoint(cfg.Endpoint))
opts = append(opts, otlploggrpc.WithEndpointURL(cfg.Endpoint))
}
if cfg.Insecure {
opts = append(opts, otlploggrpc.WithInsecure())
Expand Down
96 changes: 96 additions & 0 deletions pkg/utils/otel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package utils

import (
"context"

Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Remove the blank line between standard library imports. Both "context" and "testing" are standard library packages and should be grouped together without a blank line separator. Go convention typically uses blank lines only to separate standard library imports from third-party imports.

Suggested change

Copilot uses AI. Check for mistakes.
"testing"
)

Expand Down Expand Up @@ -423,3 +424,98 @@ func TestOTelConfig_MinimalConfig(t *testing.T) {
t.Errorf("shutdown returned unexpected error: %v", err)
}
}

// TestEndpointURL verifies that endpointURL prepends the correct scheme
// when missing and preserves existing schemes.
func TestEndpointURL(t *testing.T) {
tests := []struct {
name string
raw string
insecure bool
want string
}{
{
name: "IP:port insecure",
raw: "127.0.0.1:4317",
insecure: true,
want: "http://127.0.0.1:4317",
},
{
name: "IP:port secure",
raw: "127.0.0.1:4317",
insecure: false,
want: "https://127.0.0.1:4317",
},
{
name: "localhost:port insecure",
raw: "localhost:4317",
insecure: true,
want: "http://localhost:4317",
},
{
name: "hostname without port",
raw: "collector",
insecure: true,
want: "http://collector",
},
{
name: "http URL preserved",
raw: "http://collector.example.com:4318",
insecure: false,
want: "http://collector.example.com:4318",
},
{
name: "https URL preserved",
raw: "https://collector.example.com:4318",
insecure: true,
want: "https://collector.example.com:4318",
},
{
name: "https URL with path preserved",
raw: "https://collector.example.com:4318/v1/traces",
insecure: false,
want: "https://collector.example.com:4318/v1/traces",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := endpointURL(tt.raw, tt.insecure)
if got != tt.want {
t.Errorf("endpointURL(%q, %t) = %q, want %q", tt.raw, tt.insecure, got, tt.want)
}
})
}
}

// TestSetupOTelSDKWithConfig_IPEndpoint verifies that SetupOTelSDKWithConfig
// normalizes a bare IP:port endpoint to include a scheme, preventing the
// "first path segment in URL cannot contain colon" error from the SDK.
func TestSetupOTelSDKWithConfig_IPEndpoint(t *testing.T) {
t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "127.0.0.1:4317")

cfg := OTelConfig{
ServiceName: "test-service",
ServiceVersion: "1.0.0",
Protocol: OTelProtocolGRPC,
Endpoint: "127.0.0.1:4317",
Insecure: true,
TracesExporter: OTelExporterOTLP,
TracesSampleRatio: 1.0,
MetricsExporter: OTelExporterNone,
LogsExporter: OTelExporterNone,
Propagators: "tracecontext,baggage",
}

ctx := context.Background()
shutdown, err := SetupOTelSDKWithConfig(ctx, cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if shutdown == nil {
t.Fatal("expected non-nil shutdown function")
}

_ = shutdown(ctx)
}
Loading