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 }} 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") +}