...

Source file src/github.com/go-openapi/runtime/client/opentelemetry.go

Documentation: github.com/go-openapi/runtime/client

     1  package client
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"strings"
     7  
     8  	"github.com/go-openapi/runtime"
     9  	"github.com/go-openapi/strfmt"
    10  	"go.opentelemetry.io/otel"
    11  	"go.opentelemetry.io/otel/attribute"
    12  	"go.opentelemetry.io/otel/codes"
    13  	"go.opentelemetry.io/otel/propagation"
    14  	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
    15  	"go.opentelemetry.io/otel/semconv/v1.17.0/httpconv"
    16  	"go.opentelemetry.io/otel/trace"
    17  )
    18  
    19  const (
    20  	instrumentationVersion = "1.0.0"
    21  	tracerName             = "go-openapi"
    22  )
    23  
    24  type config struct {
    25  	Tracer            trace.Tracer
    26  	Propagator        propagation.TextMapPropagator
    27  	SpanStartOptions  []trace.SpanStartOption
    28  	SpanNameFormatter func(*runtime.ClientOperation) string
    29  	TracerProvider    trace.TracerProvider
    30  }
    31  
    32  type OpenTelemetryOpt interface {
    33  	apply(*config)
    34  }
    35  
    36  type optionFunc func(*config)
    37  
    38  func (o optionFunc) apply(c *config) {
    39  	o(c)
    40  }
    41  
    42  // WithTracerProvider specifies a tracer provider to use for creating a tracer.
    43  // If none is specified, the global provider is used.
    44  func WithTracerProvider(provider trace.TracerProvider) OpenTelemetryOpt {
    45  	return optionFunc(func(c *config) {
    46  		if provider != nil {
    47  			c.TracerProvider = provider
    48  		}
    49  	})
    50  }
    51  
    52  // WithPropagators configures specific propagators. If this
    53  // option isn't specified, then the global TextMapPropagator is used.
    54  func WithPropagators(ps propagation.TextMapPropagator) OpenTelemetryOpt {
    55  	return optionFunc(func(c *config) {
    56  		if ps != nil {
    57  			c.Propagator = ps
    58  		}
    59  	})
    60  }
    61  
    62  // WithSpanOptions configures an additional set of
    63  // trace.SpanOptions, which are applied to each new span.
    64  func WithSpanOptions(opts ...trace.SpanStartOption) OpenTelemetryOpt {
    65  	return optionFunc(func(c *config) {
    66  		c.SpanStartOptions = append(c.SpanStartOptions, opts...)
    67  	})
    68  }
    69  
    70  // WithSpanNameFormatter takes a function that will be called on every
    71  // request and the returned string will become the Span Name.
    72  func WithSpanNameFormatter(f func(op *runtime.ClientOperation) string) OpenTelemetryOpt {
    73  	return optionFunc(func(c *config) {
    74  		c.SpanNameFormatter = f
    75  	})
    76  }
    77  
    78  func defaultTransportFormatter(op *runtime.ClientOperation) string {
    79  	if op.ID != "" {
    80  		return op.ID
    81  	}
    82  
    83  	return fmt.Sprintf("%s_%s", strings.ToLower(op.Method), op.PathPattern)
    84  }
    85  
    86  type openTelemetryTransport struct {
    87  	transport runtime.ClientTransport
    88  	host      string
    89  	tracer    trace.Tracer
    90  	config    *config
    91  }
    92  
    93  func newOpenTelemetryTransport(transport runtime.ClientTransport, host string, opts []OpenTelemetryOpt) *openTelemetryTransport {
    94  	tr := &openTelemetryTransport{
    95  		transport: transport,
    96  		host:      host,
    97  	}
    98  
    99  	defaultOpts := []OpenTelemetryOpt{
   100  		WithSpanOptions(trace.WithSpanKind(trace.SpanKindClient)),
   101  		WithSpanNameFormatter(defaultTransportFormatter),
   102  		WithPropagators(otel.GetTextMapPropagator()),
   103  		WithTracerProvider(otel.GetTracerProvider()),
   104  	}
   105  
   106  	c := newConfig(append(defaultOpts, opts...)...)
   107  	tr.config = c
   108  
   109  	return tr
   110  }
   111  
   112  func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (interface{}, error) {
   113  	if op.Context == nil {
   114  		return t.transport.Submit(op)
   115  	}
   116  
   117  	params := op.Params
   118  	reader := op.Reader
   119  
   120  	var span trace.Span
   121  	defer func() {
   122  		if span != nil {
   123  			span.End()
   124  		}
   125  	}()
   126  
   127  	op.Params = runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error {
   128  		span = t.newOpenTelemetrySpan(op, req.GetHeaderParams())
   129  		return params.WriteToRequest(req, reg)
   130  	})
   131  
   132  	op.Reader = runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
   133  		if span != nil {
   134  			statusCode := response.Code()
   135  			// NOTE: this is replaced by semconv.HTTPResponseStatusCode in semconv v1.21
   136  			span.SetAttributes(semconv.HTTPStatusCode(statusCode))
   137  			// NOTE: the conversion from HTTP status code to trace code is no longer available with
   138  			// semconv v1.21
   139  			span.SetStatus(httpconv.ServerStatus(statusCode))
   140  		}
   141  
   142  		return reader.ReadResponse(response, consumer)
   143  	})
   144  
   145  	submit, err := t.transport.Submit(op)
   146  	if err != nil && span != nil {
   147  		span.RecordError(err)
   148  		span.SetStatus(codes.Error, err.Error())
   149  	}
   150  
   151  	return submit, err
   152  }
   153  
   154  func (t *openTelemetryTransport) newOpenTelemetrySpan(op *runtime.ClientOperation, header http.Header) trace.Span {
   155  	ctx := op.Context
   156  
   157  	tracer := t.tracer
   158  	if tracer == nil {
   159  		if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
   160  			tracer = newTracer(span.TracerProvider())
   161  		} else {
   162  			tracer = newTracer(otel.GetTracerProvider())
   163  		}
   164  	}
   165  
   166  	ctx, span := tracer.Start(ctx, t.config.SpanNameFormatter(op), t.config.SpanStartOptions...)
   167  
   168  	var scheme string
   169  	if len(op.Schemes) > 0 {
   170  		scheme = op.Schemes[0]
   171  	}
   172  
   173  	span.SetAttributes(
   174  		attribute.String("net.peer.name", t.host),
   175  		attribute.String(string(semconv.HTTPRouteKey), op.PathPattern),
   176  		attribute.String(string(semconv.HTTPMethodKey), op.Method),
   177  		attribute.String("span.kind", trace.SpanKindClient.String()),
   178  		attribute.String("http.scheme", scheme),
   179  	)
   180  
   181  	carrier := propagation.HeaderCarrier(header)
   182  	t.config.Propagator.Inject(ctx, carrier)
   183  
   184  	return span
   185  }
   186  
   187  func newTracer(tp trace.TracerProvider) trace.Tracer {
   188  	return tp.Tracer(tracerName, trace.WithInstrumentationVersion(version()))
   189  }
   190  
   191  func newConfig(opts ...OpenTelemetryOpt) *config {
   192  	c := &config{
   193  		Propagator: otel.GetTextMapPropagator(),
   194  	}
   195  
   196  	for _, opt := range opts {
   197  		opt.apply(c)
   198  	}
   199  
   200  	// Tracer is only initialized if manually specified. Otherwise, can be passed with the tracing context.
   201  	if c.TracerProvider != nil {
   202  		c.Tracer = newTracer(c.TracerProvider)
   203  	}
   204  
   205  	return c
   206  }
   207  
   208  // Version is the current release version of the go-runtime instrumentation.
   209  func version() string {
   210  	return instrumentationVersion
   211  }
   212  

View as plain text