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 }