Repository: go-openapi/runtime
Pull request #254 (2022-10-17 23:20:32)

Add support for tracing via OpenTelemetry
As OpenTracing (the predecessor to OpenTelemetry) has been deprecated, this change adds support for OpenTelemetry via the `WithOpenTelemetry` functional option.

A lot of the API and options align with the [`otelhttp`](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/instrumentation/net/http/otelhttp/transport.go) transport that can be used on the net/http client 

Addresses issue https://github.com/go-openapi/runtime/issues/249


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index cac5e4e..6d8db66 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -17,7 +17,7 @@ jobs:
     - name: Set up Go
       uses: actions/setup-go@v2
       with:
-        go-version: 1.17.1
+        go-version: 1.18.8
 
     - name: Setup gotestsum
       uses: autero1/action-gotestsum@v1.0.0
diff --git a/client/opentelemetry.go b/client/opentelemetry.go
new file mode 100644
index 0000000..3b7b839
--- /dev/null
+++ b/client/opentelemetry.go
@@ -0,0 +1,212 @@
+package client
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/codes"
+	"go.opentelemetry.io/otel/propagation"
+	semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
+	"go.opentelemetry.io/otel/trace"
+)
+
+const (
+	instrumentationVersion = "1.0.0"
+	tracerName             = "go-openapi"
+)
+
+type config struct {
+	Tracer            trace.Tracer
+	Propagator        propagation.TextMapPropagator
+	SpanStartOptions  []trace.SpanStartOption
+	SpanNameFormatter func(*runtime.ClientOperation) string
+	TracerProvider    trace.TracerProvider
+}
+
+type OpenTelemetryOpt interface {
+	apply(*config)
+}
+
+type optionFunc func(*config)
+
+func (o optionFunc) apply(c *config) {
+	o(c)
+}
+
+// WithTracerProvider specifies a tracer provider to use for creating a tracer.
+// If none is specified, the global provider is used.
+func WithTracerProvider(provider trace.TracerProvider) OpenTelemetryOpt {
+	return optionFunc(func(c *config) {
+		if provider != nil {
+			c.TracerProvider = provider
+		}
+	})
+}
+
+// WithPropagators configures specific propagators. If this
+// option isn't specified, then the global TextMapPropagator is used.
+func WithPropagators(ps propagation.TextMapPropagator) OpenTelemetryOpt {
+	return optionFunc(func(c *config) {
+		if ps != nil {
+			c.Propagator = ps
+		}
+	})
+}
+
+// WithSpanOptions configures an additional set of
+// trace.SpanOptions, which are applied to each new span.
+func WithSpanOptions(opts ...trace.SpanStartOption) OpenTelemetryOpt {
+	return optionFunc(func(c *config) {
+		c.SpanStartOptions = append(c.SpanStartOptions, opts...)
+	})
+}
+
+// WithSpanNameFormatter takes a function that will be called on every
+// request and the returned string will become the Span Name.
+func WithSpanNameFormatter(f func(op *runtime.ClientOperation) string) OpenTelemetryOpt {
+	return optionFunc(func(c *config) {
+		c.SpanNameFormatter = f
+	})
+}
+
+func defaultTransportFormatter(op *runtime.ClientOperation) string {
+	if op.ID != "" {
+		return op.ID
+	}
+
+	return fmt.Sprintf("%s_%s", strings.ToLower(op.Method), op.PathPattern)
+}
+
+type openTelemetryTransport struct {
+	transport runtime.ClientTransport
+	host      string
+	tracer    trace.Tracer
+	config    *config
+}
+
+func newOpenTelemetryTransport(transport runtime.ClientTransport, host string, opts []OpenTelemetryOpt) *openTelemetryTransport {
+	tr := &openTelemetryTransport{
+		transport: transport,
+		host:      host,
+	}
+
+	defaultOpts := []OpenTelemetryOpt{
+		WithSpanOptions(trace.WithSpanKind(trace.SpanKindClient)),
+		WithSpanNameFormatter(defaultTransportFormatter),
+		WithPropagators(otel.GetTextMapPropagator()),
+		WithTracerProvider(otel.GetTracerProvider()),
+	}
+
+	c := newConfig(append(defaultOpts, opts...)...)
+	tr.config = c
+
+	return tr
+}
+
+func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (interface{}, error) {
+	if op.Context == nil {
+		return t.transport.Submit(op)
+	}
+
+	params := op.Params
+	reader := op.Reader
+
+	var span trace.Span
+	defer func() {
+		if span != nil {
+			span.End()
+		}
+	}()
+
+	op.Params = runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error {
+		span = t.newOpenTelemetrySpan(op, req.GetHeaderParams())
+		return params.WriteToRequest(req, reg)
+	})
+
+	op.Reader = runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+		if span != nil {
+			statusCode := response.Code()
+			span.SetAttributes(attribute.Int(string(semconv.HTTPStatusCodeKey), statusCode))
+			span.SetStatus(semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(statusCode, trace.SpanKindClient))
+		}
+
+		return reader.ReadResponse(response, consumer)
+	})
+
+	submit, err := t.transport.Submit(op)
+	if err != nil && span != nil {
+		span.RecordError(err)
+		span.SetStatus(codes.Error, err.Error())
+	}
+
+	return submit, err
+}
+
+func (t *openTelemetryTransport) newOpenTelemetrySpan(op *runtime.ClientOperation, header http.Header) trace.Span {
+	ctx := op.Context
+
+	tracer := t.tracer
+	if tracer == nil {
+		if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
+			tracer = newTracer(span.TracerProvider())
+		} else {
+			tracer = newTracer(otel.GetTracerProvider())
+		}
+	}
+
+	ctx, span := tracer.Start(ctx, t.config.SpanNameFormatter(op), t.config.SpanStartOptions...)
+
+	var scheme string
+	if len(op.Schemes) > 0 {
+		scheme = op.Schemes[0]
+	}
+
+	span.SetAttributes(
+		attribute.String("net.peer.name", t.host),
+		attribute.String(string(semconv.HTTPRouteKey), op.PathPattern),
+		attribute.String(string(semconv.HTTPMethodKey), op.Method),
+		attribute.String("span.kind", trace.SpanKindClient.String()),
+		attribute.String("http.scheme", scheme),
+	)
+
+	carrier := propagation.HeaderCarrier(header)
+	t.config.Propagator.Inject(ctx, carrier)
+
+	return span
+}
+
+func newTracer(tp trace.TracerProvider) trace.Tracer {
+	return tp.Tracer(tracerName, trace.WithInstrumentationVersion(version()))
+}
+
+func newConfig(opts ...OpenTelemetryOpt) *config {
+	c := &config{
+		Propagator: otel.GetTextMapPropagator(),
+	}
+
+	for _, opt := range opts {
+		opt.apply(c)
+	}
+
+	// Tracer is only initialized if manually specified. Otherwise, can be passed with the tracing context.
+	if c.TracerProvider != nil {
+		c.Tracer = newTracer(c.TracerProvider)
+	}
+
+	return c
+}
+
+// Version is the current release version of the go-runtime instrumentation.
+func version() string {
+	return instrumentationVersion
+}
+
+// SemVersion is the semantic version to be supplied to tracer/meter creation.
+func semVersion() string {
+	return "semver:" + version()
+}
diff --git a/client/response.go b/client/response.go
index 0bbd388..b297a12 100644
--- a/client/response.go
+++ b/client/response.go
@@ -23,8 +23,6 @@ import (
 
 var _ runtime.ClientResponse = response{}
 
-func newResponse(resp *http.Response) runtime.ClientResponse { return response{resp: resp} }
-
 type response struct {
 	resp *http.Response
 }
diff --git a/client/runtime.go b/client/runtime.go
index 611925a..54e7958 100644
--- a/client/runtime.go
+++ b/client/runtime.go
@@ -31,13 +31,13 @@ import (
 	"sync"
 	"time"
 
-	"github.com/go-openapi/strfmt"
 	"github.com/opentracing/opentracing-go"
 
 	"github.com/go-openapi/runtime"
 	"github.com/go-openapi/runtime/logger"
 	"github.com/go-openapi/runtime/middleware"
 	"github.com/go-openapi/runtime/yamlpc"
+	"github.com/go-openapi/strfmt"
 )
 
 // TLSClientOptions to configure client authentication with mutual TLS
@@ -225,7 +225,7 @@ type Runtime struct {
 
 	Transport http.RoundTripper
 	Jar       http.CookieJar
-	// Spec      *spec.Document
+	//Spec      *spec.Document
 	Host     string
 	BasePath string
 	Formats  strfmt.Registry
@@ -237,7 +237,6 @@ type Runtime struct {
 	clientOnce *sync.Once
 	client     *http.Client
 	schemes    []string
-	response   ClientResponseFunc
 }
 
 // New creates a new default runtime for a swagger api runtime.Client
@@ -276,7 +275,6 @@ func New(host, basePath string, schemes []string) *Runtime {
 
 	rt.Debug = logger.DebugEnabled()
 	rt.logger = logger.StandardLogger{}
-	rt.response = newResponse
 
 	if len(schemes) > 0 {
 		rt.schemes = schemes
@@ -303,6 +301,14 @@ func (r *Runtime) WithOpenTracing(opts ...opentracing.StartSpanOption) runtime.C
 	return newOpenTracingTransport(r, r.Host, opts)
 }
 
+// WithOpenTelemetry adds opentelemetry support to the provided runtime.
+// A new client span is created for each request.
+// If the context of the client operation does not contain an active span, no span is created.
+// The provided opts are applied to each spans - for example to add global tags.
+func (r *Runtime) WithOpenTelemetry(opts ...OpenTelemetryOpt) runtime.ClientTransport {
+	return newOpenTelemetryTransport(r, r.Host, opts)
+}
+
 func (r *Runtime) pickScheme(schemes []string) string {
 	if v := r.selectScheme(r.schemes); v != "" {
 		return v
@@ -331,7 +337,6 @@ func (r *Runtime) selectScheme(schemes []string) string {
 	}
 	return scheme
 }
-
 func transportOrDefault(left, right http.RoundTripper) http.RoundTripper {
 	if left == nil {
 		return right
@@ -384,7 +389,7 @@ func (r *Runtime) createHttpRequest(operation *runtime.ClientOperation) (*reques
 			return r.DefaultAuthentication.AuthenticateRequest(req, reg)
 		})
 	}
-	// if auth != nil {
+	//if auth != nil {
 	//	if err := auth.AuthenticateRequest(request, r.Formats); err != nil {
 	//		return nil, err
 	//	}
@@ -503,7 +508,7 @@ func (r *Runtime) Submit(operation *runtime.ClientOperation) (interface{}, error
 			return nil, fmt.Errorf("no consumer: %q", ct)
 		}
 	}
-	return readResponse.ReadResponse(r.response(res), cons)
+	return readResponse.ReadResponse(response{res}, cons)
 }
 
 // SetDebug changes the debug flag.
@@ -519,13 +524,3 @@ func (r *Runtime) SetLogger(logger logger.Logger) {
 	r.logger = logger
 	middleware.Logger = logger
 }
-
-type ClientResponseFunc = func(*http.Response) runtime.ClientResponse
-
-// SetResponseReader changes the response reader implementation.
-func (r *Runtime) SetResponseReader(f ClientResponseFunc) {
-	if f == nil {
-		return
-	}
-	r.response = f
-}
diff --git a/go.mod b/go.mod
index e5db4c1..a94c638 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,6 @@
 module github.com/go-openapi/runtime
 
 require (
-	github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
 	github.com/docker/go-units v0.4.0
 	github.com/go-openapi/analysis v0.21.2
 	github.com/go-openapi/errors v0.20.2
@@ -10,14 +9,34 @@ require (
 	github.com/go-openapi/strfmt v0.21.2
 	github.com/go-openapi/swag v0.21.1
 	github.com/go-openapi/validate v0.21.0
+	github.com/opentracing/opentracing-go v1.2.0
+	github.com/stretchr/testify v1.8.0
+	go.opentelemetry.io/otel v1.11.1
+	go.opentelemetry.io/otel/sdk v1.11.1
+	go.opentelemetry.io/otel/trace v1.11.1
+	gopkg.in/yaml.v2 v2.4.0
+)
+
+require (
+	github.com/PuerkitoBio/purell v1.1.1 // indirect
+	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+	github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/go-logr/logr v1.2.3 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-openapi/jsonpointer v0.19.5 // indirect
+	github.com/go-openapi/jsonreference v0.19.6 // indirect
 	github.com/go-stack/stack v1.8.1 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mitchellh/mapstructure v1.4.3 // indirect
-	github.com/opentracing/opentracing-go v1.2.0
-	github.com/stretchr/testify v1.7.0
+	github.com/oklog/ulid v1.3.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	go.mongodb.org/mongo-driver v1.8.3 // indirect
 	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
-	gopkg.in/yaml.v2 v2.4.0
+	golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
+	golang.org/x/text v0.3.7 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
-go 1.15
+go 1.18
diff --git a/go.sum b/go.sum
index c821eb7..2e043cb 100644
--- a/go.sum
+++ b/go.sum
@@ -12,6 +12,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
+github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-openapi/analysis v0.21.2 h1:hXFrOYFHUAMQdu6zwAiKKJHJQ8kqZs1ux/ru1P1wLJU=
 github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY=
 github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
@@ -65,8 +70,8 @@ github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGt
 github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
 github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -117,11 +122,14 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@@ -132,6 +140,12 @@ go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R7
 go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
 go.mongodb.org/mongo-driver v1.8.3 h1:TDKlTkGDKm9kkJVUOAXDK5/fkqKHJVwYQSpoRfB43R4=
 go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
+go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
+go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
+go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZpKxs=
+go.opentelemetry.io/otel/sdk v1.11.1/go.mod h1:/l3FE4SupHJ12TduVjUkZtlfFqDCQJlOlithYrdktys=
+go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
+go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
@@ -156,11 +170,10 @@ golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
+golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -171,7 +184,6 @@ golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -185,5 +197,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/middleware/context.go b/middleware/context.go
index d21ae4e..0aa184c 100644
--- a/middleware/context.go
+++ b/middleware/context.go
@@ -195,17 +195,6 @@ func NewRoutableContext(spec *loads.Document, routableAPI RoutableAPI, routes Ro
 	if spec != nil {
 		an = analysis.New(spec.Spec())
 	}
-
-	return NewRoutableContextWithAnalyzedSpec(spec, an, routableAPI, routes)
-}
-
-// NewRoutableContextWithAnalyzedSpec is like NewRoutableContext but takes in input the analysed spec too
-func NewRoutableContextWithAnalyzedSpec(spec *loads.Document, an *analysis.Spec, routableAPI RoutableAPI, routes Router) *Context {
-	// Either there are no spec doc and analysis, or both of them.
-	if !((spec == nil && an == nil) || (spec != nil && an != nil)) {
-		panic(errors.New(http.StatusInternalServerError, "routable context requires either both spec doc and analysis, or none of them"))
-	}
-
 	ctx := &Context{spec: spec, api: routableAPI, analyzer: an, router: routes}
 	return ctx
 }
diff --git a/middleware/swaggerui.go b/middleware/swaggerui.go
index b4dea29..bab523c 100644
--- a/middleware/swaggerui.go
+++ b/middleware/swaggerui.go
@@ -16,8 +16,6 @@ type SwaggerUIOpts struct {
 	Path string
 	// SpecURL the url to find the spec for
 	SpecURL string
-	// OAuthCallbackURL the url called after OAuth2 login
-	OAuthCallbackURL string
 
 	// The three components needed to embed swagger-ui
 	SwaggerURL       string
@@ -42,9 +40,6 @@ func (r *SwaggerUIOpts) EnsureDefaults() {
 	if r.SpecURL == "" {
 		r.SpecURL = "/swagger.json"
 	}
-	if r.OAuthCallbackURL == "" {
-		r.OAuthCallbackURL = path.Join(r.BasePath, r.Path, "oauth2-callback")
-	}
 	if r.SwaggerURL == "" {
 		r.SwaggerURL = swaggerLatest
 	}
@@ -154,8 +149,7 @@ const (
         plugins: [
           SwaggerUIBundle.plugins.DownloadUrl
         ],
-        layout: "StandaloneLayout",
-		oauth2RedirectUrl: '{{ .OAuthCallbackURL }}'
+        layout: "StandaloneLayout"
       })
       // End Swagger UI call region
 
diff --git a/middleware/swaggerui_oauth2.go b/middleware/swaggerui_oauth2.go
deleted file mode 100644
index 576f600..0000000
--- a/middleware/swaggerui_oauth2.go
+++ /dev/null
@@ -1,122 +0,0 @@
-package middleware
-
-import (
-	"bytes"
-	"fmt"
-	"net/http"
-	"path"
-	"text/template"
-)
-
-func SwaggerUIOAuth2Callback(opts SwaggerUIOpts, next http.Handler) http.Handler {
-	opts.EnsureDefaults()
-
-	pth := opts.OAuthCallbackURL
-	tmpl := template.Must(template.New("swaggeroauth").Parse(swaggerOAuthTemplate))
-
-	buf := bytes.NewBuffer(nil)
-	_ = tmpl.Execute(buf, &opts)
-	b := buf.Bytes()
-
-	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
-		if path.Join(r.URL.Path) == pth {
-			rw.Header().Set("Content-Type", "text/html; charset=utf-8")
-			rw.WriteHeader(http.StatusOK)
-
-			_, _ = rw.Write(b)
-			return
-		}
-
-		if next == nil {
-			rw.Header().Set("Content-Type", "text/plain")
-			rw.WriteHeader(http.StatusNotFound)
-			_, _ = rw.Write([]byte(fmt.Sprintf("%q not found", pth)))
-			return
-		}
-		next.ServeHTTP(rw, r)
-	})
-}
-
-const (
-	swaggerOAuthTemplate = `
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <title>{{ .Title }}</title>
-</head>
-<body>
-<script>
-    'use strict';
-    function run () {
-        var oauth2 = window.opener.swaggerUIRedirectOauth2;
-        var sentState = oauth2.state;
-        var redirectUrl = oauth2.redirectUrl;
-        var isValid, qp, arr;
-
-        if (/code|token|error/.test(window.location.hash)) {
-            qp = window.location.hash.substring(1).replace('?', '&');
-        } else {
-            qp = location.search.substring(1);
-        }
-
-        arr = qp.split("&");
-        arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
-        qp = qp ? JSON.parse('{' + arr.join() + '}',
-                function (key, value) {
-                    return key === "" ? value : decodeURIComponent(value);
-                }
-        ) : {};
-
-        isValid = qp.state === sentState;
-
-        if ((
-          oauth2.auth.schema.get("flow") === "accessCode" ||
-          oauth2.auth.schema.get("flow") === "authorizationCode" ||
-          oauth2.auth.schema.get("flow") === "authorization_code"
-        ) && !oauth2.auth.code) {
-            if (!isValid) {
-                oauth2.errCb({
-                    authId: oauth2.auth.name,
-                    source: "auth",
-                    level: "warning",
-                    message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
-                });
-            }
-
-            if (qp.code) {
-                delete oauth2.state;
-                oauth2.auth.code = qp.code;
-                oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
-            } else {
-                let oauthErrorMsg;
-                if (qp.error) {
-                    oauthErrorMsg = "["+qp.error+"]: " +
-                        (qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
-                        (qp.error_uri ? "More info: "+qp.error_uri : "");
-                }
-
-                oauth2.errCb({
-                    authId: oauth2.auth.name,
-                    source: "auth",
-                    level: "error",
-                    message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
-                });
-            }
-        } else {
-            oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
-        }
-        window.close();
-    }
-
-    if (document.readyState !== 'loading') {
-        run();
-    } else {
-        document.addEventListener('DOMContentLoaded', function () {
-            run();
-        });
-    }
-</script>
-</body>
-</html>
-`
-)


Test patch:
diff --git a/client/opentelemetry_test.go b/client/opentelemetry_test.go
new file mode 100644
index 0000000..5026fd9
--- /dev/null
+++ b/client/opentelemetry_test.go
@@ -0,0 +1,123 @@
+package client
+
+import (
+	"context"
+	"testing"
+
+	"github.com/go-openapi/runtime"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/codes"
+	"go.opentelemetry.io/otel/propagation"
+	tracesdk "go.opentelemetry.io/otel/sdk/trace"
+	"go.opentelemetry.io/otel/sdk/trace/tracetest"
+	"go.opentelemetry.io/otel/trace"
+)
+
+func Test_OpenTelemetryRuntime_submit(t *testing.T) {
+	t.Parallel()
+
+	exporter := tracetest.NewInMemoryExporter()
+
+	tp := tracesdk.NewTracerProvider(
+		tracesdk.WithSampler(tracesdk.AlwaysSample()),
+		tracesdk.WithSyncer(exporter),
+	)
+
+	otel.SetTracerProvider(tp)
+
+	tracer := tp.Tracer("go-runtime")
+	ctx, _ := tracer.Start(context.Background(), "op")
+
+	assertOpenTelemetrySubmit(t, testOperation(ctx), exporter, 1)
+}
+
+func Test_OpenTelemetryRuntime_submit_nilAuthInfo(t *testing.T) {
+	t.Parallel()
+
+	exporter := tracetest.NewInMemoryExporter()
+
+	tp := tracesdk.NewTracerProvider(
+		tracesdk.WithSampler(tracesdk.AlwaysSample()),
+		tracesdk.WithSyncer(exporter),
+	)
+
+	otel.SetTracerProvider(tp)
+
+	tracer := tp.Tracer("go-runtime")
+	ctx, _ := tracer.Start(context.Background(), "op")
+
+	operation := testOperation(ctx)
+	operation.AuthInfo = nil
+	assertOpenTelemetrySubmit(t, operation, exporter, 1)
+}
+
+func Test_OpenTelemetryRuntime_submit_nilContext(t *testing.T) {
+	exporter := tracetest.NewInMemoryExporter()
+
+	tp := tracesdk.NewTracerProvider(
+		tracesdk.WithSampler(tracesdk.AlwaysSample()),
+		tracesdk.WithSyncer(exporter),
+	)
+
+	otel.SetTracerProvider(tp)
+
+	tracer := tp.Tracer("go-runtime")
+	ctx, _ := tracer.Start(context.Background(), "op")
+	operation := testOperation(ctx)
+	operation.Context = nil
+
+	assertOpenTelemetrySubmit(t, operation, exporter, 0) // just don't panic
+}
+
+func Test_injectOpenTelemetrySpanContext(t *testing.T) {
+	t.Parallel()
+
+	exporter := tracetest.NewInMemoryExporter()
+
+	tp := tracesdk.NewTracerProvider(
+		tracesdk.WithSampler(tracesdk.AlwaysSample()),
+		tracesdk.WithSyncer(exporter),
+	)
+
+	otel.SetTracerProvider(tp)
+
+	tracer := tp.Tracer("go-runtime")
+	ctx, _ := tracer.Start(context.Background(), "op")
+	operation := testOperation(ctx)
+
+	header := map[string][]string{}
+	tr := newOpenTelemetryTransport(&mockRuntime{runtime.TestClientRequest{Headers: header}}, "", nil)
+	tr.config.Propagator = propagation.TraceContext{}
+	tr.Submit(operation)
+
+	assert.Len(t, header, 1)
+}
+
+func assertOpenTelemetrySubmit(t *testing.T, operation *runtime.ClientOperation, exporter *tracetest.InMemoryExporter, expectedSpanCount int) {
+	header := map[string][]string{}
+	tr := newOpenTelemetryTransport(&mockRuntime{runtime.TestClientRequest{Headers: header}}, "remote_host", nil)
+
+	_, err := tr.Submit(operation)
+	require.NoError(t, err)
+
+	spans := exporter.GetSpans()
+	assert.Len(t, spans, expectedSpanCount)
+
+	if expectedSpanCount == 1 {
+		span := spans[0]
+		assert.Equal(t, "getCluster", span.Name)
+		assert.Equal(t, "go-openapi", span.InstrumentationLibrary.Name)
+		assert.Equal(t, span.Status.Code, codes.Error)
+		assert.Equal(t, []attribute.KeyValue{
+			attribute.String("net.peer.name", "remote_host"),
+			attribute.String("http.route", "/kubernetes-clusters/{cluster_id}"),
+			attribute.String("http.method", "GET"),
+			attribute.String("span.kind", trace.SpanKindClient.String()),
+			attribute.String("http.scheme", "https"),
+			attribute.Int("http.status_code", 490),
+		}, span.Attributes)
+	}
+}
diff --git a/middleware/swaggerui_oauth2_test.go b/middleware/swaggerui_oauth2_test.go
deleted file mode 100644
index eab1639..0000000
--- a/middleware/swaggerui_oauth2_test.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package middleware
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestSwaggerUIOAuth2CallbackMiddleware(t *testing.T) {
-	redoc := SwaggerUIOAuth2Callback(SwaggerUIOpts{}, nil)
-
-	req, _ := http.NewRequest("GET", "/docs/oauth2-callback", nil)
-	recorder := httptest.NewRecorder()
-	redoc.ServeHTTP(recorder, req)
-	assert.Equal(t, 200, recorder.Code)
-	assert.Equal(t, "text/html; charset=utf-8", recorder.Header().Get("Content-Type"))
-	assert.Contains(t, recorder.Body.String(), "<title>API documentation</title>")
-}