-
Notifications
You must be signed in to change notification settings - Fork 116
fix(otelx): accept OTLP server_url with URL path #883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // Copyright © 2023 Ory Corp | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package otelx | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "fmt" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/ory/jsonschema/v3" | ||
| ) | ||
|
|
||
| func TestOTLPServerURLSchema(t *testing.T) { | ||
| c := jsonschema.NewCompiler() | ||
| require.NoError(t, AddConfigSchema(c)) | ||
|
|
||
| schema, err := c.Compile(context.Background(), ConfigSchemaID) | ||
| require.NoError(t, err) | ||
|
|
||
| for _, tc := range []struct { | ||
| name string | ||
| url string | ||
| wantErr bool | ||
| }{ | ||
| {name: "host and port", url: "localhost:4318"}, | ||
| {name: "ipv4 and port", url: "127.0.0.1:4318"}, | ||
| {name: "grafana cloud url", url: "https://otlp-gateway-prod-gb-south-0.grafana.net/otlp"}, | ||
| {name: "http url with path", url: "http://collector.example.com:4318/otlp"}, | ||
| {name: "https url without path", url: "https://collector.example.com:4318"}, | ||
| {name: "bare hostname", url: "localhost", wantErr: true}, | ||
| {name: "path without scheme", url: "collector.example.com/otlp", wantErr: true}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| doc := fmt.Sprintf(`{"provider":"otel","providers":{"otlp":{"server_url":%q}}}`, tc.url) | ||
| err := schema.Validate(bytes.NewBufferString(doc)) | ||
| if tc.wantErr { | ||
| assert.Error(t, err) | ||
| return | ||
| } | ||
| assert.NoError(t, err) | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ package otelx | |
|
|
||
| import ( | ||
| "context" | ||
| "net/url" | ||
| "strings" | ||
|
|
||
| "go.opentelemetry.io/contrib/propagators/b3" | ||
| jaegerPropagator "go.opentelemetry.io/contrib/propagators/jaeger" | ||
|
|
@@ -18,22 +20,53 @@ import ( | |
| "go.opentelemetry.io/otel/trace" | ||
| ) | ||
|
|
||
| func SetupOTLP(t *Tracer, tracerName string, c *Config) (trace.Tracer, error) { | ||
| ctx := context.Background() | ||
| const otlpTracesPath = "/v1/traces" | ||
|
|
||
| clientOpts := []otlptracehttp.Option{ | ||
| otlptracehttp.WithEndpoint(c.Providers.OTLP.ServerURL), | ||
| func otlpHTTPOptions(c *OTLPConfig) []otlptracehttp.Option { | ||
| opts := make([]otlptracehttp.Option, 0, 4) | ||
| insecure := c.Insecure | ||
|
|
||
| if u, err := url.Parse(c.ServerURL); err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" { | ||
| opts = append(opts, otlptracehttp.WithEndpoint(u.Host)) | ||
| if p := otlpTracesURLPath(u.Path); p != "" { | ||
| opts = append(opts, otlptracehttp.WithURLPath(p)) | ||
| } | ||
| insecure = u.Scheme == "http" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: sed -n '1,115p' otelx/otlp.goRepository: ory/x Length of output: 3015 Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information Exploitability: Moderate Reject authenticated HTTP exporters. When 🤖 Prompt for AI Agents |
||
| } else { | ||
| opts = append(opts, otlptracehttp.WithEndpoint(c.ServerURL)) | ||
| } | ||
|
|
||
| if c.Providers.OTLP.Insecure { | ||
| clientOpts = append(clientOpts, otlptracehttp.WithInsecure()) | ||
| if insecure { | ||
| opts = append(opts, otlptracehttp.WithInsecure()) | ||
| } | ||
|
|
||
| if c.Providers.OTLP.AuthorizationHeader != "" { | ||
| clientOpts = append(clientOpts, | ||
| otlptracehttp.WithHeaders(map[string]string{"Authorization": c.Providers.OTLP.AuthorizationHeader}), | ||
| ) | ||
| if c.AuthorizationHeader != "" { | ||
| opts = append(opts, otlptracehttp.WithHeaders(map[string]string{"Authorization": c.AuthorizationHeader})) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: sed -n '1,125p' otelx/otlp.go
printf '\n--- module dependencies ---\n'
rg -n 'opentelemetry|otlptracehttp|go ' go.mod go.sum
printf '\n--- relevant local references ---\n'
rg -n 'AuthorizationHeader|WithHeaders|otlpHTTPOptions|ServerURL' otelx --glob '*.go'Repository: ory/x Length of output: 10474 🏁 Script executed: set -eu
base='https://raw.githubusercontent.com/open-telemetry/opentelemetry-go/v1.44.0/exporters/otlp/otlptrace/otlptracehttp'
for f in client.go internal/http_client.go; do
echo "--- $f ---"
curl -fsSL "$base/$f" | nl -ba | sed -n '1,260p'
done
echo '--- redirect behavior in Go net/http source ---'
go env GOROOT 2>/dev/null || true
if [ -n "${GOROOT:-}" ] && [ -f "$GOROOT/src/net/http/client.go" ]; then
rg -n -A18 -B8 'Authorization|shouldCopyHeaderOnRedirect|redirect' "$GOROOT/src/net/http/client.go" | sed -n '1,220p'
else
echo 'Go source tree unavailable'
fiRepository: ory/x Length of output: 9784 🏁 Script executed: set -eu
url='https://raw.githubusercontent.com/open-telemetry/opentelemetry-go/v1.44.0/exporters/otlp/otlptrace/otlptracehttp/client.go'
curl -fsSL "$url" | sed -n '1,320p'
echo '--- Go redirect header logic ---'
sed -n '700,820p' /usr/local/go/src/net/http/client.go
echo '--- redirect header helper references ---'
grep -n -A35 -B10 'shouldCopyHeaderOnRedirect\|sensitiveHeaders' /usr/local/go/src/net/http/client.goRepository: ory/x Length of output: 16775 Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information Reachability: External · Exploitability: Difficult Reject HTTPS-to-HTTP redirects before sending trace credentials.
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return opts | ||
| } | ||
|
|
||
| // otlpTracesURLPath maps a URL path to the OTLP HTTP traces path. | ||
| // Empty or "/" leaves the exporter default (/v1/traces). Other paths are treated | ||
| // as a base prefix (OTEL_EXPORTER_OTLP_ENDPOINT semantics) unless they already | ||
| // end with /v1/traces. | ||
| func otlpTracesURLPath(p string) string { | ||
| p = strings.TrimSpace(p) | ||
| if p == "" || p == "/" { | ||
| return "" | ||
| } | ||
| p = strings.TrimRight(p, "/") | ||
| if strings.HasSuffix(p, otlpTracesPath) { | ||
| return p | ||
| } | ||
| return p + otlpTracesPath | ||
| } | ||
|
|
||
| func SetupOTLP(t *Tracer, tracerName string, c *Config) (trace.Tracer, error) { | ||
| ctx := context.Background() | ||
|
|
||
| clientOpts := otlpHTTPOptions(&c.Providers.OTLP) | ||
|
|
||
| exp, err := otlptrace.New( | ||
| ctx, otlptracehttp.NewClient(clientOpts...), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| // Copyright © 2023 Ory Corp | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package otelx | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "net/url" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| "go.opentelemetry.io/otel" | ||
| "go.opentelemetry.io/otel/attribute" | ||
| sdktrace "go.opentelemetry.io/otel/sdk/trace" | ||
| ) | ||
|
|
||
| func TestSetupOTLPRequestPath(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| name string | ||
| serverURL func(base, host string) string | ||
| wantPath string | ||
| }{ | ||
| { | ||
| name: "host and port", | ||
| serverURL: func(_, host string) string { | ||
| return host | ||
| }, | ||
| wantPath: "/v1/traces", | ||
| }, | ||
| { | ||
| name: "grafana cloud style url", | ||
| serverURL: func(base, _ string) string { | ||
| return base + "/otlp" | ||
| }, | ||
| wantPath: "/otlp/v1/traces", | ||
| }, | ||
| { | ||
| name: "full traces url", | ||
| serverURL: func(base, _ string) string { | ||
| return base + "/v1/traces" | ||
| }, | ||
| wantPath: "/v1/traces", | ||
| }, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| prevTP := otel.GetTracerProvider() | ||
| prevProp := otel.GetTextMapPropagator() | ||
| t.Cleanup(func() { | ||
| otel.SetTracerProvider(prevTP) | ||
| otel.SetTextMapPropagator(prevProp) | ||
| }) | ||
|
|
||
| got := make(chan string, 1) | ||
| ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| select { | ||
| case got <- r.URL.Path: | ||
| default: | ||
| } | ||
| })) | ||
| t.Cleanup(ts.Close) | ||
|
|
||
| tsu, err := url.Parse(ts.URL) | ||
| require.NoError(t, err) | ||
|
|
||
| _, err = SetupOTLP(nil, "test", &Config{ | ||
| ServiceName: "ORY X", | ||
| Provider: "otel", | ||
| Providers: ProvidersConfig{ | ||
| OTLP: OTLPConfig{ | ||
| ServerURL: tc.serverURL(ts.URL, tsu.Host), | ||
| Insecure: true, | ||
| Sampling: OTLPSampling{ | ||
| SamplingRatio: 1, | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| require.NoError(t, err) | ||
|
|
||
| tp, ok := otel.GetTracerProvider().(*sdktrace.TracerProvider) | ||
| require.True(t, ok) | ||
| t.Cleanup(func() { | ||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| _ = tp.Shutdown(ctx) | ||
| }) | ||
|
|
||
| tracer := tp.Tracer("test") | ||
| _, span := tracer.Start(context.Background(), "testSpan") | ||
| span.SetAttributes(attribute.Bool("testAttribute", true)) | ||
| span.End() | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| require.NoError(t, tp.ForceFlush(ctx)) | ||
|
|
||
| select { | ||
| case path := <-got: | ||
| assert.Equal(t, tc.wantPath, path) | ||
| case <-ctx.Done(): | ||
| t.Fatalf("collector did not receive a span; want path %s", tc.wantPath) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Require an authority in HTTP(S) URLs.
The pattern accepts
https:///otlp.url.Parsethen returns an empty host, andSetupOTLPfalls back to the host-and-port endpoint path. Configuration validation succeeds, but the exporter cannot use the configured URL.Reject URL values without an authority. Add
https:///otlpas a rejected schema case.Proposed validation change
📝 Committable suggestion
🤖 Prompt for AI Agents