Skip to content

Commit 295a503

Browse files
authored
Merge pull request #12 from linuxfoundation/bramwelt/otel-endpoint-fix
Add endpointURL for bare IP:port values
2 parents 85a57e5 + 581f33e commit 295a503

File tree

2 files changed

+124
-6
lines changed

2 files changed

+124
-6
lines changed

pkg/utils/otel.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ func SetupOTelSDKWithConfig(ctx context.Context, cfg OTelConfig) (shutdown func(
188188
err = errors.Join(inErr, shutdown(ctx))
189189
}
190190

191+
// Normalize endpoint to include a URL scheme so WithEndpointURL can
192+
// parse it. Bare IP:port values like "127.0.0.1:4317" cause url.Parse
193+
// to fail with "first path segment in URL cannot contain colon".
194+
if cfg.Endpoint != "" {
195+
cfg.Endpoint = endpointURL(cfg.Endpoint, cfg.Insecure)
196+
}
197+
191198
// Create resource with service information.
192199
res, err := newResource(cfg)
193200
if err != nil {
@@ -276,6 +283,21 @@ func newPropagator(cfg OTelConfig) propagation.TextMapPropagator {
276283
return propagation.NewCompositeTextMapPropagator(propagators...)
277284
}
278285

286+
// endpointURL ensures the endpoint has a URL scheme. The OTel SDK internally
287+
// reads OTEL_EXPORTER_OTLP_ENDPOINT and parses it with url.Parse, which fails
288+
// for bare IP:port values like "127.0.0.1:4317" with "first path segment in
289+
// URL cannot contain colon". Prepending a scheme based on the insecure flag
290+
// produces a valid URL the SDK can parse.
291+
func endpointURL(raw string, insecure bool) string {
292+
if strings.Contains(raw, "://") {
293+
return raw
294+
}
295+
if insecure {
296+
return "http://" + raw
297+
}
298+
return "https://" + raw
299+
}
300+
279301
// newTraceProvider creates a TracerProvider with an OTLP exporter configured based on the protocol setting.
280302
func newTraceProvider(ctx context.Context, cfg OTelConfig, res *resource.Resource) (*sdktrace.TracerProvider, error) {
281303
var exporter sdktrace.SpanExporter
@@ -284,7 +306,7 @@ func newTraceProvider(ctx context.Context, cfg OTelConfig, res *resource.Resourc
284306
if cfg.Protocol == OTelProtocolHTTP {
285307
opts := []otlptracehttp.Option{}
286308
if cfg.Endpoint != "" {
287-
opts = append(opts, otlptracehttp.WithEndpoint(cfg.Endpoint))
309+
opts = append(opts, otlptracehttp.WithEndpointURL(cfg.Endpoint))
288310
}
289311
if cfg.Insecure {
290312
opts = append(opts, otlptracehttp.WithInsecure())
@@ -293,7 +315,7 @@ func newTraceProvider(ctx context.Context, cfg OTelConfig, res *resource.Resourc
293315
} else {
294316
opts := []otlptracegrpc.Option{}
295317
if cfg.Endpoint != "" {
296-
opts = append(opts, otlptracegrpc.WithEndpoint(cfg.Endpoint))
318+
opts = append(opts, otlptracegrpc.WithEndpointURL(cfg.Endpoint))
297319
}
298320
if cfg.Insecure {
299321
opts = append(opts, otlptracegrpc.WithInsecure())
@@ -323,7 +345,7 @@ func newMetricsProvider(ctx context.Context, cfg OTelConfig, res *resource.Resou
323345
if cfg.Protocol == OTelProtocolHTTP {
324346
opts := []otlpmetrichttp.Option{}
325347
if cfg.Endpoint != "" {
326-
opts = append(opts, otlpmetrichttp.WithEndpoint(cfg.Endpoint))
348+
opts = append(opts, otlpmetrichttp.WithEndpointURL(cfg.Endpoint))
327349
}
328350
if cfg.Insecure {
329351
opts = append(opts, otlpmetrichttp.WithInsecure())
@@ -332,7 +354,7 @@ func newMetricsProvider(ctx context.Context, cfg OTelConfig, res *resource.Resou
332354
} else {
333355
opts := []otlpmetricgrpc.Option{}
334356
if cfg.Endpoint != "" {
335-
opts = append(opts, otlpmetricgrpc.WithEndpoint(cfg.Endpoint))
357+
opts = append(opts, otlpmetricgrpc.WithEndpointURL(cfg.Endpoint))
336358
}
337359
if cfg.Insecure {
338360
opts = append(opts, otlpmetricgrpc.WithInsecure())
@@ -361,7 +383,7 @@ func newLoggerProvider(ctx context.Context, cfg OTelConfig, res *resource.Resour
361383
if cfg.Protocol == OTelProtocolHTTP {
362384
opts := []otlploghttp.Option{}
363385
if cfg.Endpoint != "" {
364-
opts = append(opts, otlploghttp.WithEndpoint(cfg.Endpoint))
386+
opts = append(opts, otlploghttp.WithEndpointURL(cfg.Endpoint))
365387
}
366388
if cfg.Insecure {
367389
opts = append(opts, otlploghttp.WithInsecure())
@@ -370,7 +392,7 @@ func newLoggerProvider(ctx context.Context, cfg OTelConfig, res *resource.Resour
370392
} else {
371393
opts := []otlploggrpc.Option{}
372394
if cfg.Endpoint != "" {
373-
opts = append(opts, otlploggrpc.WithEndpoint(cfg.Endpoint))
395+
opts = append(opts, otlploggrpc.WithEndpointURL(cfg.Endpoint))
374396
}
375397
if cfg.Insecure {
376398
opts = append(opts, otlploggrpc.WithInsecure())

pkg/utils/otel_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package utils
55

66
import (
77
"context"
8+
89
"testing"
910
)
1011

@@ -225,3 +226,98 @@ func TestOTelConstants(t *testing.T) {
225226
t.Errorf("expected OTelExporterNone to be 'none', got %q", OTelExporterNone)
226227
}
227228
}
229+
230+
// TestEndpointURL verifies that endpointURL prepends the correct scheme
231+
// when missing and preserves existing schemes.
232+
func TestEndpointURL(t *testing.T) {
233+
tests := []struct {
234+
name string
235+
raw string
236+
insecure bool
237+
want string
238+
}{
239+
{
240+
name: "IP:port insecure",
241+
raw: "127.0.0.1:4317",
242+
insecure: true,
243+
want: "http://127.0.0.1:4317",
244+
},
245+
{
246+
name: "IP:port secure",
247+
raw: "127.0.0.1:4317",
248+
insecure: false,
249+
want: "https://127.0.0.1:4317",
250+
},
251+
{
252+
name: "localhost:port insecure",
253+
raw: "localhost:4317",
254+
insecure: true,
255+
want: "http://localhost:4317",
256+
},
257+
{
258+
name: "hostname without port",
259+
raw: "collector",
260+
insecure: true,
261+
want: "http://collector",
262+
},
263+
{
264+
name: "http URL preserved",
265+
raw: "http://collector.example.com:4318",
266+
insecure: false,
267+
want: "http://collector.example.com:4318",
268+
},
269+
{
270+
name: "https URL preserved",
271+
raw: "https://collector.example.com:4318",
272+
insecure: true,
273+
want: "https://collector.example.com:4318",
274+
},
275+
{
276+
name: "https URL with path preserved",
277+
raw: "https://collector.example.com:4318/v1/traces",
278+
insecure: false,
279+
want: "https://collector.example.com:4318/v1/traces",
280+
},
281+
}
282+
283+
for _, tt := range tests {
284+
t.Run(tt.name, func(t *testing.T) {
285+
got := endpointURL(tt.raw, tt.insecure)
286+
if got != tt.want {
287+
t.Errorf("endpointURL(%q, %t) = %q, want %q", tt.raw, tt.insecure, got, tt.want)
288+
}
289+
})
290+
}
291+
}
292+
293+
// TestSetupOTelSDKWithConfig_IPEndpoint verifies that SetupOTelSDKWithConfig
294+
// normalizes a bare IP:port endpoint to include a scheme, preventing the
295+
// "first path segment in URL cannot contain colon" error from the SDK.
296+
func TestSetupOTelSDKWithConfig_IPEndpoint(t *testing.T) {
297+
t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "127.0.0.1:4317")
298+
299+
cfg := OTelConfig{
300+
ServiceName: "test-service",
301+
ServiceVersion: "1.0.0",
302+
Protocol: OTelProtocolGRPC,
303+
Endpoint: "127.0.0.1:4317",
304+
Insecure: true,
305+
TracesExporter: OTelExporterOTLP,
306+
TracesSampleRatio: 1.0,
307+
MetricsExporter: OTelExporterNone,
308+
LogsExporter: OTelExporterNone,
309+
Propagators: "tracecontext,baggage",
310+
}
311+
312+
ctx := context.Background()
313+
shutdown, err := SetupOTelSDKWithConfig(ctx, cfg)
314+
if err != nil {
315+
t.Fatalf("unexpected error: %v", err)
316+
}
317+
318+
if shutdown == nil {
319+
t.Fatal("expected non-nil shutdown function")
320+
}
321+
322+
_ = shutdown(ctx)
323+
}

0 commit comments

Comments
 (0)