// 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 ( "io" "log" "net/http" "net/http/httptest" "testing" "github.com/prometheus/client_golang/prometheus" ) func TestLabelCheck(t *testing.T) { scenarios := map[string]struct { varLabels []string constLabels []string curriedLabels []string ok bool }{ "empty": { varLabels: []string{}, constLabels: []string{}, curriedLabels: []string{}, ok: true, }, "code as single var label": { varLabels: []string{"code"}, constLabels: []string{}, curriedLabels: []string{}, ok: true, }, "method as single var label": { varLabels: []string{"method"}, constLabels: []string{}, curriedLabels: []string{}, ok: true, }, "cade and method as var labels": { varLabels: []string{"method", "code"}, constLabels: []string{}, curriedLabels: []string{}, ok: true, }, "valid case with all labels used": { varLabels: []string{"code", "method"}, constLabels: []string{"foo", "bar"}, curriedLabels: []string{"dings", "bums"}, ok: true, }, "unsupported var label": { varLabels: []string{"foo"}, constLabels: []string{}, curriedLabels: []string{}, ok: false, }, "mixed var labels": { varLabels: []string{"method", "foo", "code"}, constLabels: []string{}, curriedLabels: []string{}, ok: false, }, "unsupported var label but curried": { varLabels: []string{}, constLabels: []string{}, curriedLabels: []string{"foo"}, ok: true, }, "mixed var labels but unsupported curried": { varLabels: []string{"code", "method"}, constLabels: []string{}, curriedLabels: []string{"foo"}, ok: true, }, "supported label as const and curry": { varLabels: []string{}, constLabels: []string{"code"}, curriedLabels: []string{"method"}, ok: true, }, "supported label as const and curry with unsupported as var": { varLabels: []string{"foo"}, constLabels: []string{"code"}, curriedLabels: []string{"method"}, ok: false, }, } for name, sc := range scenarios { t.Run(name, func(t *testing.T) { constLabels := prometheus.Labels{} for _, l := range sc.constLabels { constLabels[l] = "dummy" } c := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "c", Help: "c help", ConstLabels: constLabels, }, append(sc.varLabels, sc.curriedLabels...), ) o := prometheus.ObserverVec(prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "c", Help: "c help", ConstLabels: constLabels, }, append(sc.varLabels, sc.curriedLabels...), )) for _, l := range sc.curriedLabels { c = c.MustCurryWith(prometheus.Labels{l: "dummy"}) o = o.MustCurryWith(prometheus.Labels{l: "dummy"}) } func() { defer func() { if err := recover(); err != nil { if sc.ok { t.Error("unexpected panic:", err) } } else if !sc.ok { t.Error("expected panic") } }() InstrumentHandlerCounter(c, nil) }() func() { defer func() { if err := recover(); err != nil { if sc.ok { t.Error("unexpected panic:", err) } } else if !sc.ok { t.Error("expected panic") } }() InstrumentHandlerDuration(o, nil) }() if sc.ok { // Test if wantCode and wantMethod were detected correctly. var wantCode, wantMethod bool for _, l := range sc.varLabels { if l == "code" { wantCode = true } if l == "method" { wantMethod = true } } gotCode, gotMethod := checkLabels(c) if gotCode != wantCode { t.Errorf("wanted code=%t for counter, got code=%t", wantCode, gotCode) } if gotMethod != wantMethod { t.Errorf("wanted method=%t for counter, got method=%t", wantMethod, gotMethod) } gotCode, gotMethod = checkLabels(o) if gotCode != wantCode { t.Errorf("wanted code=%t for observer, got code=%t", wantCode, gotCode) } if gotMethod != wantMethod { t.Errorf("wanted method=%t for observer, got method=%t", wantMethod, gotMethod) } } }) } } func TestMiddlewareAPI(t *testing.T) { reg := prometheus.NewRegistry() inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "in_flight_requests", Help: "A gauge of requests currently being served by the wrapped handler.", }) counter := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "api_requests_total", Help: "A counter for requests to the wrapped handler.", }, []string{"code", "method"}, ) histVec := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "response_duration_seconds", Help: "A histogram of request latencies.", Buckets: prometheus.DefBuckets, ConstLabels: prometheus.Labels{"handler": "api"}, }, []string{"method"}, ) writeHeaderVec := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "write_header_duration_seconds", Help: "A histogram of time to first write latencies.", Buckets: prometheus.DefBuckets, ConstLabels: prometheus.Labels{"handler": "api"}, }, []string{}, ) responseSize := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "push_request_size_bytes", Help: "A histogram of request sizes for requests.", Buckets: []float64{200, 500, 900, 1500}, }, []string{}, ) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }) reg.MustRegister(inFlightGauge, counter, histVec, responseSize, writeHeaderVec) chain := InstrumentHandlerInFlight(inFlightGauge, InstrumentHandlerCounter(counter, InstrumentHandlerDuration(histVec, InstrumentHandlerTimeToWriteHeader(writeHeaderVec, InstrumentHandlerResponseSize(responseSize, handler), ), ), ), ) r, _ := http.NewRequest("GET", "www.example.com", nil) w := httptest.NewRecorder() chain.ServeHTTP(w, r) } func TestInstrumentTimeToFirstWrite(t *testing.T) { var i int dobs := &responseWriterDelegator{ ResponseWriter: httptest.NewRecorder(), observeWriteHeader: func(status int) { i = status }, } d := newDelegator(dobs, nil) d.WriteHeader(http.StatusOK) if i != http.StatusOK { t.Fatalf("failed to execute observeWriteHeader") } } // testResponseWriter is an http.ResponseWriter that also implements // http.CloseNotifier, http.Flusher, and io.ReaderFrom. type testResponseWriter struct { closeNotifyCalled, flushCalled, readFromCalled bool } func (t *testResponseWriter) Header() http.Header { return nil } func (t *testResponseWriter) Write([]byte) (int, error) { return 0, nil } func (t *testResponseWriter) WriteHeader(int) {} func (t *testResponseWriter) CloseNotify() <-chan bool { t.closeNotifyCalled = true return nil } func (t *testResponseWriter) Flush() { t.flushCalled = true } func (t *testResponseWriter) ReadFrom(io.Reader) (int64, error) { t.readFromCalled = true return 0, nil } func TestInterfaceUpgrade(t *testing.T) { w := &testResponseWriter{} d := newDelegator(w, nil) d.(http.CloseNotifier).CloseNotify() if !w.closeNotifyCalled { t.Error("CloseNotify not called") } d.(http.Flusher).Flush() if !w.flushCalled { t.Error("Flush not called") } d.(io.ReaderFrom).ReadFrom(nil) if !w.readFromCalled { t.Error("ReadFrom not called") } if _, ok := d.(http.Hijacker); ok { t.Error("delegator unexpectedly implements http.Hijacker") } } func ExampleInstrumentHandlerDuration() { inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "in_flight_requests", Help: "A gauge of requests currently being served by the wrapped handler.", }) counter := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "api_requests_total", Help: "A counter for requests to the wrapped handler.", }, []string{"code", "method"}, ) // duration is partitioned by the HTTP method and handler. It uses custom // buckets based on the expected request duration. duration := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "request_duration_seconds", Help: "A histogram of latencies for requests.", Buckets: []float64{.25, .5, 1, 2.5, 5, 10}, }, []string{"handler", "method"}, ) // responseSize has no labels, making it a zero-dimensional // ObserverVec. responseSize := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "response_size_bytes", Help: "A histogram of response sizes for requests.", Buckets: []float64{200, 500, 900, 1500}, }, []string{}, ) // Create the handlers that will be wrapped by the middleware. pushHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Push")) }) pullHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Pull")) }) // Register all of the metrics in the standard registry. prometheus.MustRegister(inFlightGauge, counter, duration, responseSize) // Instrument the handlers with all the metrics, injecting the "handler" // label by currying. pushChain := InstrumentHandlerInFlight(inFlightGauge, InstrumentHandlerDuration(duration.MustCurryWith(prometheus.Labels{"handler": "push"}), InstrumentHandlerCounter(counter, InstrumentHandlerResponseSize(responseSize, pushHandler), ), ), ) pullChain := InstrumentHandlerInFlight(inFlightGauge, InstrumentHandlerDuration(duration.MustCurryWith(prometheus.Labels{"handler": "pull"}), InstrumentHandlerCounter(counter, InstrumentHandlerResponseSize(responseSize, pullHandler), ), ), ) http.Handle("/metrics", Handler()) http.Handle("/push", pushChain) http.Handle("/pull", pullChain) if err := http.ListenAndServe(":3000", nil); err != nil { log.Fatal(err) } }