diff --git a/telemetry/trace/trace.go b/telemetry/trace/trace.go index fe277fb9ec..4b0ce4999d 100644 --- a/telemetry/trace/trace.go +++ b/telemetry/trace/trace.go @@ -30,6 +30,7 @@ import ( "go.opentelemetry.io/otel/trace/noop" itelemetry "trpc.group/trpc-go/trpc-agent-go/internal/telemetry" + "trpc.group/trpc-go/trpc-agent-go/platform" ) // TracerProvider is the global tracer TracerProvider for telemetry. @@ -248,13 +249,20 @@ func parseEndpointURL(endpointURL string) (endpoint, urlPath string, err error) u, err := url.Parse(endpointURL) if err != nil { - return "", "", fmt.Errorf("failed to parse URL %q: %w", originalURL, err) + return "", "", redactedTraceConfigError{ + msg: fmt.Sprintf( + "failed to parse URL %q: %s", + redactTraceConfigValue(originalURL), + redactTraceConfigValue(err.Error()), + ), + err: err, + } } // Extract host:port endpoint = u.Host if endpoint == "" { - return "", "", fmt.Errorf("no host found in URL %q", originalURL) + return "", "", fmt.Errorf("no host found in URL %q", redactTraceConfigValue(originalURL)) } // Extract path @@ -305,7 +313,7 @@ func initHTTPTracerProvider(ctx context.Context, res *resource.Resource, opts *o // Parse the full URL to extract host:port and path components endpoint, urlPath, err := parseEndpointURL(opts.tracesEndpointURL) if err != nil { - return nil, fmt.Errorf("failed to parse endpoint URL %q: %w", opts.tracesEndpointURL, err) + return nil, fmt.Errorf("failed to parse endpoint URL %q: %w", redactTraceConfigValue(opts.tracesEndpointURL), err) } otelOpts = append(otelOpts, otlptracehttp.WithEndpoint(endpoint), @@ -320,6 +328,50 @@ func initHTTPTracerProvider(ctx context.Context, res *resource.Resource, opts *o return setupTracerProvider(res, traceExporter), nil } +func redactTraceConfigValue(value string) string { + redactor, err := platform.NewRedactor() + if err != nil { + return "" + } + return redactTraceConfigURLUserinfo(redactor.Redact(value)) +} + +type redactedTraceConfigError struct { + msg string + err error +} + +func (e redactedTraceConfigError) Error() string { + return e.msg +} + +func (e redactedTraceConfigError) Unwrap() error { + return e.err +} + +func redactTraceConfigURLUserinfo(value string) string { + authorityStart := 0 + schemeIndex := strings.Index(value, "://") + if schemeIndex >= 0 { + authorityStart = schemeIndex + len("://") + } + if authorityStart >= len(value) { + return value + } + authorityEnd := len(value) + for _, delimiter := range []string{"/", "?", "#"} { + if index := strings.Index(value[authorityStart:], delimiter); index >= 0 { + authorityEnd = min(authorityEnd, authorityStart+index) + } + } + authority := value[authorityStart:authorityEnd] + at := strings.LastIndex(authority, "@") + if at < 0 { + return value + } + return value[:authorityStart] + "****" + value[authorityStart+at:] +} + // setupTracerProvider sets up the tracer provider with the given resource and exporter. func setupTracerProvider(res *resource.Resource, traceExporter sdktrace.SpanExporter) func(context.Context) error { // Register the trace exporter with a TracerProvider, using a batch diff --git a/telemetry/trace/trace_test.go b/telemetry/trace/trace_test.go index aace9d200a..7b2b7b3ddf 100644 --- a/telemetry/trace/trace_test.go +++ b/telemetry/trace/trace_test.go @@ -12,6 +12,7 @@ package trace import ( "context" "os" + "strings" "testing" "go.opentelemetry.io/otel/attribute" @@ -158,6 +159,39 @@ func TestStartHTTP_InvalidEndpointURL(t *testing.T) { } } +func TestStartHTTP_InvalidEndpointURLRedactsSensitiveConfig(t *testing.T) { + for _, endpointURL := range []string{ + "http://user:password@/bad?api_key=sk-1234567890abcdef", + "user:password@/bad?api_key=sk-1234567890abcdef", + "http://user:password@%zz/bad?api_key=sk-1234567890abcdef", + "user:password@%zz/bad?api_key=sk-1234567890abcdef", + } { + t.Run(endpointURL, func(t *testing.T) { + ctx := context.Background() + _, err := Start(ctx, + WithProtocol("http"), + WithEndpoint("localhost:4318"), + WithEndpointURL(endpointURL), + ) + if err == nil { + t.Fatalf("expected error from invalid endpoint URL") + } + for _, leaked := range []string{ + "user:password", + "password", + "sk-1234567890abcdef", + } { + if strings.Contains(err.Error(), leaked) { + t.Fatalf("trace config error leaked %q: %v", leaked, err) + } + } + if !strings.Contains(err.Error(), "failed to parse endpoint URL") { + t.Fatalf("expected endpoint context in error, got %v", err) + } + }) + } +} + func TestStartHTTP_DefaultNoEnv_NoEndpoint(t *testing.T) { // ensure env empty origTrace := os.Getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")