github.com/thanos-io/thanos@v0.32.5/pkg/extprom/http/instrument_server.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package http
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/opentracing/opentracing-go"
    13  	"github.com/prometheus/client_golang/prometheus"
    14  	"github.com/prometheus/client_golang/prometheus/promhttp"
    15  	"github.com/uber/jaeger-client-go"
    16  	"go.opentelemetry.io/otel/trace"
    17  )
    18  
    19  // InstrumentationMiddleware holds necessary metrics to instrument an http.Server
    20  // and provides necessary behaviors.
    21  type InstrumentationMiddleware interface {
    22  	// NewHandler wraps the given HTTP handler for instrumentation.
    23  	NewHandler(handlerName string, handler http.Handler) http.HandlerFunc
    24  }
    25  
    26  type nopInstrumentationMiddleware struct{}
    27  
    28  func (ins nopInstrumentationMiddleware) NewHandler(handlerName string, handler http.Handler) http.HandlerFunc {
    29  	return func(w http.ResponseWriter, r *http.Request) {
    30  		handler.ServeHTTP(w, r)
    31  	}
    32  }
    33  
    34  // NewNopInstrumentationMiddleware provides a InstrumentationMiddleware which does nothing.
    35  func NewNopInstrumentationMiddleware() InstrumentationMiddleware {
    36  	return nopInstrumentationMiddleware{}
    37  }
    38  
    39  type defaultInstrumentationMiddleware struct {
    40  	metrics *defaultMetrics
    41  }
    42  
    43  // NewInstrumentationMiddleware provides default InstrumentationMiddleware.
    44  // Passing nil as buckets uses the default buckets.
    45  func NewInstrumentationMiddleware(reg prometheus.Registerer, buckets []float64) InstrumentationMiddleware {
    46  	return &defaultInstrumentationMiddleware{
    47  		metrics: newDefaultMetrics(reg, buckets, []string{}),
    48  	}
    49  }
    50  
    51  // NewHandler wraps the given HTTP handler for instrumentation. It
    52  // registers four metric collectors (if not already done) and reports HTTP
    53  // metrics to the (newly or already) registered collectors: http_requests_total
    54  // (CounterVec), http_request_duration_seconds (Histogram),
    55  // http_request_size_bytes (Summary), http_response_size_bytes (Summary).
    56  // Each has a constant label named "handler" with the provided handlerName as value.
    57  func (ins *defaultInstrumentationMiddleware) NewHandler(handlerName string, handler http.Handler) http.HandlerFunc {
    58  	baseLabels := prometheus.Labels{"handler": handlerName}
    59  	return httpInstrumentationHandler(baseLabels, ins.metrics, handler)
    60  }
    61  
    62  func httpInstrumentationHandler(baseLabels prometheus.Labels, metrics *defaultMetrics, next http.Handler) http.HandlerFunc {
    63  	return promhttp.InstrumentHandlerRequestSize(
    64  		metrics.requestSize.MustCurryWith(baseLabels),
    65  		instrumentHandlerInFlight(
    66  			metrics.inflightHTTPRequests.MustCurryWith(baseLabels),
    67  			promhttp.InstrumentHandlerCounter(
    68  				metrics.requestsTotal.MustCurryWith(baseLabels),
    69  				promhttp.InstrumentHandlerResponseSize(
    70  					metrics.responseSize.MustCurryWith(baseLabels),
    71  					http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    72  						now := time.Now()
    73  
    74  						wd := &responseWriterDelegator{w: w}
    75  						next.ServeHTTP(wd, r)
    76  
    77  						requestLabels := prometheus.Labels{"code": wd.Status(), "method": strings.ToLower(r.Method)}
    78  						observer := metrics.requestDuration.MustCurryWith(baseLabels).With(requestLabels)
    79  						requestDuration := time.Since(now).Seconds()
    80  
    81  						// If we find a tracingID we'll expose it as Exemplar.
    82  						var (
    83  							traceID string
    84  							OTfound bool
    85  						)
    86  
    87  						span := opentracing.SpanFromContext(r.Context())
    88  						if span != nil {
    89  							spanCtx, ok := span.Context().(jaeger.SpanContext)
    90  							if ok && spanCtx.IsSampled() {
    91  								traceID = spanCtx.TraceID().String()
    92  							}
    93  
    94  							OTfound = ok
    95  						}
    96  
    97  						// If OpenTracing span not found, try OTEL.
    98  						if !OTfound {
    99  							span := trace.SpanFromContext(r.Context())
   100  							if span != nil && span.SpanContext().IsSampled() {
   101  								traceID = span.SpanContext().TraceID().String()
   102  							}
   103  						}
   104  
   105  						if traceID != "" {
   106  							observer.(prometheus.ExemplarObserver).ObserveWithExemplar(
   107  								requestDuration,
   108  								prometheus.Labels{
   109  									"traceID": traceID,
   110  								},
   111  							)
   112  						} else {
   113  							observer.Observe(requestDuration)
   114  						}
   115  					}),
   116  				),
   117  			),
   118  		),
   119  	)
   120  }
   121  
   122  // responseWriterDelegator implements http.ResponseWriter and extracts the statusCode.
   123  type responseWriterDelegator struct {
   124  	w          http.ResponseWriter
   125  	written    bool
   126  	statusCode int
   127  }
   128  
   129  func (wd *responseWriterDelegator) Header() http.Header {
   130  	return wd.w.Header()
   131  }
   132  
   133  func (wd *responseWriterDelegator) Write(bytes []byte) (int, error) {
   134  	return wd.w.Write(bytes)
   135  }
   136  
   137  func (wd *responseWriterDelegator) WriteHeader(statusCode int) {
   138  	wd.written = true
   139  	wd.statusCode = statusCode
   140  	wd.w.WriteHeader(statusCode)
   141  }
   142  
   143  func (wd *responseWriterDelegator) StatusCode() int {
   144  	if !wd.written {
   145  		return http.StatusOK
   146  	}
   147  	return wd.statusCode
   148  }
   149  
   150  func (wd *responseWriterDelegator) Status() string {
   151  	return fmt.Sprintf("%d", wd.StatusCode())
   152  }
   153  
   154  // instrumentHandlerInFlight is responsible for counting the amount of
   155  // in-flight HTTP requests (requests being processed by the handler) at a given
   156  // moment in time.
   157  // This is used instead of prometheus/client_golang/promhttp.InstrumentHandlerInFlight
   158  // to be able to have the HTTP method as a label.
   159  func instrumentHandlerInFlight(vec *prometheus.GaugeVec, next http.Handler) http.Handler {
   160  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   161  		gauge := vec.With(prometheus.Labels{"method": r.Method})
   162  		gauge.Inc()
   163  		defer gauge.Dec()
   164  		next.ServeHTTP(w, r)
   165  	})
   166  }