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
37 changes: 37 additions & 0 deletions pkg/telemetry/providers/otlp/endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package otlp

import "strings"

// Default URL path suffixes for the OTLP/HTTP protocol, as defined in the
// OpenTelemetry specification:
// https://opentelemetry.io/docs/specs/otlp/#otlphttp-request
//
// The Go OTLP SDK normally appends these automatically. However, when the user
// provides a custom base path (e.g. "/api/public/otel" for Langfuse), we must
// call WithURLPath which replaces the entire path. In that case we concatenate
// the base path with the appropriate suffix ourselves (e.g.
// "/api/public/otel" + "/v1/traces").
const (
otlpTracesPath = "/v1/traces"
otlpMetricsPath = "/v1/metrics"
)

// splitEndpointPath separates an OTLP endpoint string into its host:port and
// path components. If no path is present, basePath is empty.
//
// The function defensively strips http:// and https:// prefixes so it works
// correctly even when the scheme has not been removed upstream (e.g. the CLI
// path, which does not call NormalizeTelemetryConfig).
func splitEndpointPath(endpoint string) (hostPort, basePath string) {
Comment thread
ChrisJBurns marked this conversation as resolved.
endpoint = strings.TrimPrefix(endpoint, "https://")
endpoint = strings.TrimPrefix(endpoint, "http://")

idx := strings.Index(endpoint, "/")
if idx < 0 {
return endpoint, ""
}
return endpoint[:idx], strings.TrimRight(endpoint[idx:], "/")
Comment thread
ChrisJBurns marked this conversation as resolved.
}
92 changes: 92 additions & 0 deletions pkg/telemetry/providers/otlp/endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package otlp

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSplitEndpointPath(t *testing.T) {
t.Parallel()

tests := []struct {
name string
endpoint string
wantHostPort string
wantBasePath string
}{
{
name: "host and port only",
endpoint: "localhost:4318",
wantHostPort: "localhost:4318",
wantBasePath: "",
},
{
name: "hostname without port",
endpoint: "otel-collector.local",
wantHostPort: "otel-collector.local",
wantBasePath: "",
},
{
name: "Langfuse endpoint with path",
endpoint: "cloud.langfuse.com/api/public/otel",
wantHostPort: "cloud.langfuse.com",
wantBasePath: "/api/public/otel",
},
{
name: "LangSmith endpoint with port and path",
endpoint: "smith.langchain.com:443/api/v1/otel",
wantHostPort: "smith.langchain.com:443",
wantBasePath: "/api/v1/otel",
},
{
name: "trailing slash stripped",
endpoint: "cloud.langfuse.com/api/public/otel/",
wantHostPort: "cloud.langfuse.com",
wantBasePath: "/api/public/otel",
},
{
name: "host:port with trailing slash only",
endpoint: "localhost:4318/",
wantHostPort: "localhost:4318",
wantBasePath: "",
},
{
name: "https scheme stripped before splitting",
endpoint: "https://cloud.langfuse.com/api/public/otel",
wantHostPort: "cloud.langfuse.com",
wantBasePath: "/api/public/otel",
},
{
name: "http scheme stripped before splitting",
endpoint: "http://localhost:4318",
wantHostPort: "localhost:4318",
wantBasePath: "",
},
{
name: "https scheme with host only",
endpoint: "https://api.honeycomb.io",
wantHostPort: "api.honeycomb.io",
wantBasePath: "",
},
{
name: "empty string",
endpoint: "",
wantHostPort: "",
wantBasePath: "",
},
Comment thread
ChrisJBurns marked this conversation as resolved.
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

hostPort, basePath := splitEndpointPath(tt.endpoint)
assert.Equal(t, tt.wantHostPort, hostPort)
assert.Equal(t, tt.wantBasePath, basePath)
})
}
}
7 changes: 6 additions & 1 deletion pkg/telemetry/providers/otlp/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ func NewMetricReader(ctx context.Context, config Config) (sdkmetric.Reader, erro
}

func createMetricExporter(ctx context.Context, config Config) (sdkmetric.Exporter, error) {
host, basePath := splitEndpointPath(config.Endpoint)
opts := []otlpmetrichttp.Option{
otlpmetrichttp.WithEndpoint(config.Endpoint),
otlpmetrichttp.WithEndpoint(host),
}

if basePath != "" {
opts = append(opts, otlpmetrichttp.WithURLPath(basePath+otlpMetricsPath))
}

if len(config.Headers) > 0 {
Expand Down
35 changes: 23 additions & 12 deletions pkg/telemetry/providers/otlp/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func TestCreateMetricExporter(t *testing.T) {
tests := []struct {
name string
config Config
ctx func() context.Context
wantErr bool
errMsg string
}{
Expand All @@ -26,6 +27,7 @@ func TestCreateMetricExporter(t *testing.T) {
Headers: map[string]string{"x-api-key": "secret"},
Insecure: true,
},
ctx: func() context.Context { return context.Background() },
wantErr: false,
},
{
Expand All @@ -34,24 +36,37 @@ func TestCreateMetricExporter(t *testing.T) {
Endpoint: "localhost:4318",
Insecure: false,
},
ctx: func() context.Context { return context.Background() },
wantErr: false,
},
{
name: "error creating metrics exporter due to malformed endpoint",
name: "endpoint with custom path",
Comment thread
ChrisJBurns marked this conversation as resolved.
config: Config{
Endpoint: "malformed//:4318",
Endpoint: "cloud.langfuse.com/api/public/otel",
Headers: map[string]string{"Authorization": "Basic abc123"},
Insecure: false,
},
ctx: func() context.Context { return context.Background() },
wantErr: false,
},
{
name: "error creating metrics exporter due to invalid CA cert",
config: Config{
Endpoint: "localhost:4318",
Insecure: false,
CACertPath: "/nonexistent/ca.crt",
},
ctx: func() context.Context { return context.Background() },
wantErr: true,
errMsg: "invalid URL escape",
errMsg: "failed to configure TLS for metric exporter",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

ctx := context.Background()
ctx := tt.ctx()
exporter, err := createMetricExporter(ctx, tt.config)

if tt.wantErr {
Expand Down Expand Up @@ -109,17 +124,13 @@ func TestNewMetricReader(t *testing.T) {
wantErr: false,
},
{
name: "expect error creating metrics exporter due to malformed endpoint",
name: "endpoint with custom path",
config: Config{
Endpoint: "malformed//:4318",
Headers: map[string]string{
"x-api-key": "secret",
"x-env": "production",
},
Endpoint: "cloud.langfuse.com/api/public/otel",
Headers: map[string]string{"Authorization": "Basic abc123"},
Insecure: false,
},
wantErr: true,
errMsg: "invalid URL escape",
wantErr: false,
},
}

Expand Down
7 changes: 6 additions & 1 deletion pkg/telemetry/providers/otlp/tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ import (
)

func createTraceExporter(ctx context.Context, config Config) (sdktrace.SpanExporter, error) {
host, basePath := splitEndpointPath(config.Endpoint)
opts := []otlptracehttp.Option{
otlptracehttp.WithEndpoint(config.Endpoint),
otlptracehttp.WithEndpoint(host),
}

if basePath != "" {
opts = append(opts, otlptracehttp.WithURLPath(basePath+otlpTracesPath))
}

if len(config.Headers) > 0 {
Expand Down
10 changes: 10 additions & 0 deletions pkg/telemetry/providers/otlp/tracing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ func TestCreateTraceExporter(t *testing.T) {
ctx: func() context.Context { return context.Background() },
wantErr: false,
},
{
name: "endpoint with custom path",
config: Config{
Endpoint: "cloud.langfuse.com/api/public/otel",
Headers: map[string]string{"Authorization": "Basic abc123"},
Insecure: false,
},
ctx: func() context.Context { return context.Background() },
wantErr: false,
},
{
name: "error creating sdk-span-exporter due to error (cancelled context)",
config: Config{
Expand Down
Loading