// Copyright 2017 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package promhttp import ( "errors" "net/http" "strconv" "strings" "time" dto "github.com/prometheus/client_model/go" "github.com/prometheus/client_golang/prometheus" ) // magicString is used for the hacky label test in checkLabels. Remove once fixed. const magicString = "zZgWfBxLqvG8kc8IMv3POi2Bb0tZI3vAnBx+gBaFi9FyPzB/CzKUer1yufDa" // InstrumentHandlerInFlight is a middleware that wraps the provided // http.Handler. It sets the provided prometheus.Gauge to the number of // requests currently handled by the wrapped http.Handler. // // See the example for InstrumentHandlerDuration for example usage. func InstrumentHandlerInFlight(g prometheus.Gauge, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { g.Inc() defer g.Dec() next.ServeHTTP(w, r) }) } // InstrumentHandlerDuration is a middleware that wraps the provided // http.Handler to observe the request duration with the provided ObserverVec. // The ObserverVec must have zero, one, or two non-const non-curried labels. For // those, the only allowed label names are "code" and "method". The function // panics otherwise. The Observe method of the Observer in the ObserverVec is // called with the request duration in seconds. Partitioning happens by HTTP // status code and/or HTTP method if the respective instance label names are // present in the ObserverVec. For unpartitioned observations, use an // ObserverVec with zero labels. Note that partitioning of Histograms is // expensive and should be used judiciously. // // If the wrapped Handler does not set a status code, a status code of 200 is assumed. // // If the wrapped Handler panics, no values are reported. // // Note that this method is only guaranteed to never observe negative durations // if used with Go1.9+. func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc { code, method := checkLabels(obs) if code { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { now := time.Now() d := newDelegator(w, nil) next.ServeHTTP(d, r) obs.With(labels(code, method, r.Method, d.Status())).Observe(time.Since(now).Seconds()) }) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { now := time.Now() next.ServeHTTP(w, r) obs.With(labels(code, method, r.Method, 0)).Observe(time.Since(now).Seconds()) }) } // InstrumentHandlerCounter is a middleware that wraps the provided http.Handler // to observe the request result with the provided CounterVec. The CounterVec // must have zero, one, or two non-const non-curried labels. For those, the only // allowed label names are "code" and "method". The function panics // otherwise. Partitioning of the CounterVec happens by HTTP status code and/or // HTTP method if the respective instance label names are present in the // CounterVec. For unpartitioned counting, use a CounterVec with zero labels. // // If the wrapped Handler does not set a status code, a status code of 200 is assumed. // // If the wrapped Handler panics, the Counter is not incremented. // // See the example for InstrumentHandlerDuration for example usage. func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler) http.HandlerFunc { code, method := checkLabels(counter) if code { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d := newDelegator(w, nil) next.ServeHTTP(d, r) counter.With(labels(code, method, r.Method, d.Status())).Inc() }) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) counter.With(labels(code, method, r.Method, 0)).Inc() }) } // InstrumentHandlerTimeToWriteHeader is a middleware that wraps the provided // http.Handler to observe with the provided ObserverVec the request duration // until the response headers are written. The ObserverVec must have zero, one, // or two non-const non-curried labels. For those, the only allowed label names // are "code" and "method". The function panics otherwise. The Observe method of // the Observer in the ObserverVec is called with the request duration in // seconds. Partitioning happens by HTTP status code and/or HTTP method if the // respective instance label names are present in the ObserverVec. For // unpartitioned observations, use an ObserverVec with zero labels. Note that // partitioning of Histograms is expensive and should be used judiciously. // // If the wrapped Handler panics before calling WriteHeader, no value is // reported. // // Note that this method is only guaranteed to never observe negative durations // if used with Go1.9+. // // See the example for InstrumentHandlerDuration for example usage. func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc { code, method := checkLabels(obs) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { now := time.Now() d := newDelegator(w, func(status int) { obs.With(labels(code, method, r.Method, status)).Observe(time.Since(now).Seconds()) }) next.ServeHTTP(d, r) }) } // InstrumentHandlerRequestSize is a middleware that wraps the provided // http.Handler to observe the request size with the provided ObserverVec. The // ObserverVec must have zero, one, or two non-const non-curried labels. For // those, the only allowed label names are "code" and "method". The function // panics otherwise. The Observe method of the Observer in the ObserverVec is // called with the request size in bytes. Partitioning happens by HTTP status // code and/or HTTP method if the respective instance label names are present in // the ObserverVec. For unpartitioned observations, use an ObserverVec with zero // labels. Note that partitioning of Histograms is expensive and should be used // judiciously. // // If the wrapped Handler does not set a status code, a status code of 200 is assumed. // // If the wrapped Handler panics, no values are reported. // // See the example for InstrumentHandlerDuration for example usage. func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler) http.HandlerFunc { code, method := checkLabels(obs) if code { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d := newDelegator(w, nil) next.ServeHTTP(d, r) size := computeApproximateRequestSize(r) obs.With(labels(code, method, r.Method, d.Status())).Observe(float64(size)) }) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) size := computeApproximateRequestSize(r) obs.With(labels(code, method, r.Method, 0)).Observe(float64(size)) }) } // InstrumentHandlerResponseSize is a middleware that wraps the provided // http.Handler to observe the response size with the provided ObserverVec. The // ObserverVec must have zero, one, or two non-const non-curried labels. For // those, the only allowed label names are "code" and "method". The function // panics otherwise. The Observe method of the Observer in the ObserverVec is // called with the response size in bytes. Partitioning happens by HTTP status // code and/or HTTP method if the respective instance label names are present in // the ObserverVec. For unpartitioned observations, use an ObserverVec with zero // labels. Note that partitioning of Histograms is expensive and should be used // judiciously. // // If the wrapped Handler does not set a status code, a status code of 200 is assumed. // // If the wrapped Handler panics, no values are reported. // // See the example for InstrumentHandlerDuration for example usage. func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler) http.Handler { code, method := checkLabels(obs) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d := newDelegator(w, nil) next.ServeHTTP(d, r) obs.With(labels(code, method, r.Method, d.Status())).Observe(float64(d.Written())) }) } func checkLabels(c prometheus.Collector) (code bool, method bool) { // TODO(beorn7): Remove this hacky way to check for instance labels // once Descriptors can have their dimensionality queried. var ( desc *prometheus.Desc m prometheus.Metric pm dto.Metric lvs []string ) // Get the Desc from the Collector. descc := make(chan *prometheus.Desc, 1) c.Describe(descc) select { case desc = <-descc: default: panic("no description provided by collector") } select { case <-descc: panic("more than one description provided by collector") default: } close(descc) // Create a ConstMetric with the Desc. Since we don't know how many // variable labels there are, try for as long as it needs. for err := errors.New("dummy"); err != nil; lvs = append(lvs, magicString) { m, err = prometheus.NewConstMetric(desc, prometheus.UntypedValue, 0, lvs...) } // Write out the metric into a proto message and look at the labels. // If the value is not the magicString, it is a constLabel, which doesn't interest us. // If the label is curried, it doesn't interest us. // In all other cases, only "code" or "method" is allowed. if err := m.Write(&pm); err != nil { panic("error checking metric for labels") } for _, label := range pm.Label { name, value := label.GetName(), label.GetValue() if value != magicString || isLabelCurried(c, name) { continue } switch name { case "code": code = true case "method": method = true default: panic("metric partitioned with non-supported labels") } } return } func isLabelCurried(c prometheus.Collector, label string) bool { // This is even hackier than the label test above. // We essentially try to curry again and see if it works. // But for that, we need to type-convert to the two // types we use here, ObserverVec or *CounterVec. switch v := c.(type) { case *prometheus.CounterVec: if _, err := v.CurryWith(prometheus.Labels{label: "dummy"}); err == nil { return false } case prometheus.ObserverVec: if _, err := v.CurryWith(prometheus.Labels{label: "dummy"}); err == nil { return false } default: panic("unsupported metric vec type") } return true } // emptyLabels is a one-time allocation for non-partitioned metrics to avoid // unnecessary allocations on each request. var emptyLabels = prometheus.Labels{} func labels(code, method bool, reqMethod string, status int) prometheus.Labels { if !(code || method) { return emptyLabels } labels := prometheus.Labels{} if code { labels["code"] = sanitizeCode(status) } if method { labels["method"] = sanitizeMethod(reqMethod) } return labels } func computeApproximateRequestSize(r *http.Request) int { s := 0 if r.URL != nil { s += len(r.URL.String()) } s += len(r.Method) s += len(r.Proto) for name, values := range r.Header { s += len(name) for _, value := range values { s += len(value) } } s += len(r.Host) // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. if r.ContentLength != -1 { s += int(r.ContentLength) } return s } func sanitizeMethod(m string) string { switch m { case "GET", "get": return "get" case "PUT", "put": return "put" case "HEAD", "head": return "head" case "POST", "post": return "post" case "DELETE", "delete": return "delete" case "CONNECT", "connect": return "connect" case "OPTIONS", "options": return "options" case "NOTIFY", "notify": return "notify" default: return strings.ToLower(m) } } // If the wrapped http.Handler has not set a status code, i.e. the value is // currently 0, santizeCode will return 200, for consistency with behavior in // the stdlib. func sanitizeCode(s int) string { switch s { case 100: return "100" case 101: return "101" case 200, 0: return "200" case 201: return "201" case 202: return "202" case 203: return "203" case 204: return "204" case 205: return "205" case 206: return "206" case 300: return "300" case 301: return "301" case 302: return "302" case 304: return "304" case 305: return "305" case 307: return "307" case 400: return "400" case 401: return "401" case 402: return "402" case 403: return "403" case 404: return "404" case 405: return "405" case 406: return "406" case 407: return "407" case 408: return "408" case 409: return "409" case 410: return "410" case 411: return "411" case 412: return "412" case 413: return "413" case 414: return "414" case 415: return "415" case 416: return "416" case 417: return "417" case 418: return "418" case 500: return "500" case 501: return "501" case 502: return "502" case 503: return "503" case 504: return "504" case 505: return "505" case 428: return "428" case 429: return "429" case 431: return "431" case 511: return "511" default: return strconv.Itoa(s) } }