From 861437a4dbca1dc049c6dbf3390e5103cb464f37 Mon Sep 17 00:00:00 2001 From: Richard Clark Date: Thu, 26 Feb 2026 12:33:42 +0000 Subject: [PATCH 1/2] fix: restore OTEL trace propagation in remote and remote_json authorizers The httpx.ResilientClientWithTracer option was removed from ory/x during the monorepo migration, but the remote and remote_json authorizers were not updated to compensate. Their HTTP clients now use a plain transport with no trace context propagation, breaking distributed tracing to downstream services. Restore propagation by wrapping the client transport with otelhttp.NewTransport after construction. This uses the global OTEL TracerProvider (already registered by otelx.SetupOTLP via otel.SetTracerProvider), consistent with how authenticator_cookie_session.go handles the same requirement. The fix applies to both the initial client in the constructor and the retry client rebuilt in Config() when timeout/max-wait settings are present. NOTE: Two stale entries in go.mod pruned by go mod tidy --- go.mod | 2 +- go.sum | 4 --- pipeline/authz/remote.go | 9 +++++-- pipeline/authz/remote_json.go | 9 +++++-- pipeline/authz/remote_json_test.go | 39 +++++++++++++++++++++++++++++ pipeline/authz/remote_test.go | 40 ++++++++++++++++++++++++++++++ 6 files changed, 94 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 9841736a4e..a4f4c23c98 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( github.com/urfave/negroni v1.0.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 gocloud.dev v0.20.0 golang.org/x/crypto v0.45.0 @@ -267,7 +268,6 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect go.opentelemetry.io/otel/exporters/zipkin v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 3b6f028d5b..0909d9abf2 100644 --- a/go.sum +++ b/go.sum @@ -164,8 +164,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= -github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= @@ -566,8 +564,6 @@ github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= -github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= -github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A= github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/pipeline/authz/remote.go b/pipeline/authz/remote.go index 593d1bf5bb..d06be27427 100644 --- a/pipeline/authz/remote.go +++ b/pipeline/authz/remote.go @@ -13,6 +13,7 @@ import ( "time" "github.com/pkg/errors" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "github.com/ory/x/httpx" "github.com/ory/x/otelx" @@ -50,9 +51,11 @@ type AuthorizerRemote struct { // NewAuthorizerRemote creates a new AuthorizerRemote. func NewAuthorizerRemote(c configuration.Provider, d interface{ Tracer() trace.Tracer }) *AuthorizerRemote { + client := httpx.NewResilientClient().StandardClient() + client.Transport = otelhttp.NewTransport(client.Transport) return &AuthorizerRemote{ c: c, - client: httpx.NewResilientClient().StandardClient(), + client: client, t: x.NewTemplate("remote"), tracer: d.Tracer(), } @@ -177,10 +180,12 @@ func (a *AuthorizerRemote) Config(config json.RawMessage) (*AuthorizerRemoteConf return nil, err } timeout := time.Millisecond * duration - a.client = httpx.NewResilientClient( + client := httpx.NewResilientClient( httpx.ResilientClientWithMaxRetryWait(maxWait), httpx.ResilientClientWithConnectionTimeout(timeout), ).StandardClient() + client.Transport = otelhttp.NewTransport(client.Transport) + a.client = client return &c, nil } diff --git a/pipeline/authz/remote_json.go b/pipeline/authz/remote_json.go index 8d3bf48bc4..b774af554b 100644 --- a/pipeline/authz/remote_json.go +++ b/pipeline/authz/remote_json.go @@ -13,6 +13,7 @@ import ( "time" "github.com/pkg/errors" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "github.com/ory/x/httpx" "github.com/ory/x/otelx" @@ -56,9 +57,11 @@ type AuthorizerRemoteJSON struct { // NewAuthorizerRemoteJSON creates a new AuthorizerRemoteJSON. func NewAuthorizerRemoteJSON(c configuration.Provider, d interface{ Tracer() trace.Tracer }) *AuthorizerRemoteJSON { + client := httpx.NewResilientClient().StandardClient() + client.Transport = otelhttp.NewTransport(client.Transport) return &AuthorizerRemoteJSON{ c: c, - client: httpx.NewResilientClient().StandardClient(), + client: client, t: x.NewTemplate("remote_json"), tracer: d.Tracer(), } @@ -187,10 +190,12 @@ func (a *AuthorizerRemoteJSON) Config(config json.RawMessage) (*AuthorizerRemote return nil, err } timeout := time.Millisecond * duration - a.client = httpx.NewResilientClient( + client := httpx.NewResilientClient( httpx.ResilientClientWithMaxRetryWait(maxWait), httpx.ResilientClientWithConnectionTimeout(timeout), ).StandardClient() + client.Transport = otelhttp.NewTransport(client.Transport) + a.client = client return &c, nil } diff --git a/pipeline/authz/remote_json_test.go b/pipeline/authz/remote_json_test.go index 04f0b72c5d..d33a40a33d 100644 --- a/pipeline/authz/remote_json_test.go +++ b/pipeline/authz/remote_json_test.go @@ -6,6 +6,7 @@ package authz_test import ( "context" "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" @@ -15,6 +16,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/sjson" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" "github.com/ory/x/configx" "github.com/ory/x/logrusx" @@ -360,3 +364,38 @@ func TestAuthorizerRemoteJSONConfig(t *testing.T) { }) } } + +// This test must NOT use t.Parallel() because it mutates global OTEL state. +func TestAuthorizerRemoteJSONTracePropagation(t *testing.T) { + // Set up a real tracer provider so otelhttp.NewTransport creates sampled spans. + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample())) + + prevTP := otel.GetTracerProvider() + prevProp := otel.GetTextMapPropagator() + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + t.Cleanup(func() { + otel.SetTracerProvider(prevTP) + otel.SetTextMapPropagator(prevProp) + _ = tp.Shutdown(context.Background()) + }) + + var gotTraceparent string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotTraceparent = r.Header.Get("Traceparent") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + config := json.RawMessage(fmt.Sprintf(`{"remote":%q,"payload":"{}"}`, server.URL)) + + p, err := configuration.NewKoanfProvider(context.Background(), nil, logrusx.New("", "")) + require.NoError(t, err) + + a := NewAuthorizerRemoteJSON(p, otelx.NewNoop()) + r, err := http.NewRequestWithContext(context.Background(), "", "", nil) + require.NoError(t, err) + err = a.Authorize(r, &authn.AuthenticationSession{}, config, &rule.Rule{}) + require.NoError(t, err) + assert.NotEmpty(t, gotTraceparent, "expected traceparent header to be propagated to remote_json authorizer endpoint") +} diff --git a/pipeline/authz/remote_test.go b/pipeline/authz/remote_test.go index ab0ca1a419..6831b1f794 100644 --- a/pipeline/authz/remote_test.go +++ b/pipeline/authz/remote_test.go @@ -6,6 +6,7 @@ package authz_test import ( "context" "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" @@ -15,6 +16,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/sjson" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" "github.com/ory/x/configx" "github.com/ory/x/logrusx" @@ -281,3 +285,39 @@ func TestAuthorizerRemoteValidate(t *testing.T) { }) } } + +// This test must NOT use t.Parallel() because it mutates global OTEL state. +func TestAuthorizerRemoteTracePropagation(t *testing.T) { + // Set up a real tracer provider so otelhttp.NewTransport creates sampled spans. + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample())) + + prevTP := otel.GetTracerProvider() + prevProp := otel.GetTextMapPropagator() + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + t.Cleanup(func() { + otel.SetTracerProvider(prevTP) + otel.SetTextMapPropagator(prevProp) + _ = tp.Shutdown(context.Background()) + }) + + var gotTraceparent string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotTraceparent = r.Header.Get("Traceparent") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + config := json.RawMessage(fmt.Sprintf(`{"remote":%q}`, server.URL)) + + p, err := configuration.NewKoanfProvider(context.Background(), nil, logrusx.New("", "")) + require.NoError(t, err) + + a := NewAuthorizerRemote(p, otelx.NewNoop()) + r, err := http.NewRequestWithContext(context.Background(), "POST", "", nil) + require.NoError(t, err) + r.Header.Set("Content-Type", "text/plain") + err = a.Authorize(r, &authn.AuthenticationSession{}, config, &rule.Rule{}) + require.NoError(t, err) + assert.NotEmpty(t, gotTraceparent, "expected traceparent header to be propagated to remote authorizer endpoint") +} From 40f0bd152030391980c6d36e3b8ecd6153bf2284 Mon Sep 17 00:00:00 2001 From: Richard Clark Date: Thu, 26 Feb 2026 13:11:36 +0000 Subject: [PATCH 2/2] fix: add missing checkout step in docs-cli CI job The docs-cli job was missing the ory/ci/checkout@master step before actions/setup-go, causing "go version file at: go.mod does not exist" failures because the repository was never checked out into the workspace. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e162327fe9..fd3efd9719 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,7 @@ jobs: needs: - test steps: + - uses: ory/ci/checkout@master - uses: ory/ci/docs/cli-next@master with: token: ${{ secrets.ORY_BOT_PAT }}