From ea73a83cab39f3719a648158647b05b153c707a1 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 25 Nov 2016 17:54:17 +0100 Subject: [PATCH 01/17] Wip Lots to implement, but the middleware concept seems to be working. --- prometheus/promhttp/client.go | 126 +++++++++++++++++++++++++++++ prometheus/promhttp/client_test.go | 84 +++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 prometheus/promhttp/client.go create mode 100644 prometheus/promhttp/client_test.go diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go new file mode 100644 index 000000000..e5897b6f2 --- /dev/null +++ b/prometheus/promhttp/client.go @@ -0,0 +1,126 @@ +// Copyright 2016 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. + +// Copyright (c) 2013, The Prometheus Authors +// All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package promhttp + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" +) + +type Client struct { + // Do we want to allow users to modify the underlying client after creating the wrapping client? + // c defaults to the http DefaultClient. We use a pointer to support + // the user relying on modifying this behavior globally. + c *http.Client + + observers []prometheus.Observer + + middleware Middleware +} + +type ConfigFunc func(*Client) error + +type Middleware func(req *http.Request) (*http.Response, error) + +type middlewareFunc func(req *http.Request, next Middleware) (*http.Response, error) + +func (c *Client) Head(url string) (*http.Response, error) { + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return nil, err + } + return c.middleware(req) +} + +func (c *Client) Get(url string) (resp *http.Response, err error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + return c.middleware(req) +} + +func (c *Client) do(r *http.Request, _ middlewareFunc) (*http.Response, error) { + return c.c.Do(r) +} + +// SetObservers sets the observers on Client. Each Observer has Observe called +// at the conclusion of an HTTP roundtrip. +func SetObservers(obs []prometheus.Observer) ConfigFunc { + return func(c *Client) error { + c.observers = obs + return nil + } +} + +func SetMiddleware(middlewares ...middlewareFunc) ConfigFunc { + return func(c *Client) error { + c.middleware = build(c, middlewares...) + return nil + } +} + +func build(c *Client, middlewares ...middlewareFunc) Middleware { + if len(middlewares) == 0 { + return endMiddleware(c) + } + + next := build(c, middlewares[1:]...) + + return wrap(middlewares[0], next) +} + +func wrap(fn middlewareFunc, next Middleware) Middleware { + return Middleware(func(r *http.Request) (*http.Response, error) { + return fn(r, next) + }) +} + +func endMiddleware(c *Client) Middleware { + return Middleware(func(r *http.Request) (*http.Response, error) { + return c.do(r, nil) + }) +} + +// SetClient sets the http client on Client. It accepts a value as opposed to a +// pointer to discourage untraced modification of a custom http client after it +// has been set. +func SetClient(client http.Client) ConfigFunc { + return func(c *Client) error { + c.c = &client + return nil + } +} + +func NewClient(fns ...ConfigFunc) (*Client, error) { + c := &Client{ + c: http.DefaultClient, + } + c.middleware = build(c) + + for _, fn := range fns { + if err := fn(c); err != nil { + return nil, err + } + } + + return c, nil +} diff --git a/prometheus/promhttp/client_test.go b/prometheus/promhttp/client_test.go new file mode 100644 index 000000000..cd8ba028e --- /dev/null +++ b/prometheus/promhttp/client_test.go @@ -0,0 +1,84 @@ +// Copyright 2016 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. + +// Copyright (c) 2013, The Prometheus Authors +// All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package promhttp + +import ( + "log" + "net/http" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestMiddlewareAPI(t *testing.T) { + var ( + his = prometheus.NewHistogram(prometheus.HistogramOpts{Name: "test_histogram"}) + sum = prometheus.NewSummary(prometheus.SummaryOpts{Name: "test_summary"}) + gauge = prometheus.NewGauge(prometheus.GaugeOpts{Name: "test_gauge"}) + ) + + obs := []prometheus.Observer{ + his, + sum, + prometheus.ObserverFunc(gauge.Set), + } + + client := *http.DefaultClient + client.Timeout = 300 * time.Millisecond + + inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "inFlight"}) + inFlight := func(r *http.Request, next Middleware) (*http.Response, error) { + log.Println("1st") + inFlightGauge.Inc() + + resp, err := next(r) + + inFlightGauge.Dec() + + log.Println("last") + return resp, err + } + + counter := prometheus.NewCounter(prometheus.CounterOpts{Name: "test_counter"}) + bytesSent := func(req *http.Request, next Middleware) (*http.Response, error) { + counter.Add(42) + log.Println("2nd") + // counter.Add(float64(req.ContentLength)) + + return next(req) + } + + logging := func(req *http.Request, next Middleware) (*http.Response, error) { + log.Println("3rd") + return next(req) + } + + promclient, err := NewClient(SetClient(client), SetObservers(obs), SetMiddleware(inFlight, bytesSent, logging)) + if err != nil { + t.Fatalf("%v", err) + } + + resp, err := promclient.Get("http://google.com") + if err != nil { + t.Fatalf("%v", err) + } + defer resp.Body.Close() +} From 2a468161931fc73a8f3b3b2314e2ae1b1d7d88f4 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Mon, 28 Nov 2016 14:40:57 +0100 Subject: [PATCH 02/17] Add test for checking middleware wrapping --- prometheus/promhttp/client_test.go | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/prometheus/promhttp/client_test.go b/prometheus/promhttp/client_test.go index cd8ba028e..671f26680 100644 --- a/prometheus/promhttp/client_test.go +++ b/prometheus/promhttp/client_test.go @@ -28,6 +28,46 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +func TestMiddlewareWrapsFirstToLast(t *testing.T) { + order := []int{} + first := func(r *http.Request, next Middleware) (*http.Response, error) { + order = append(order, 0) + + resp, err := next(r) + + order = append(order, 3) + return resp, err + } + + second := func(req *http.Request, next Middleware) (*http.Response, error) { + order = append(order, 1) + + return next(req) + } + + third := func(req *http.Request, next Middleware) (*http.Response, error) { + order = append(order, 2) + return next(req) + } + + promclient, err := NewClient(SetMiddleware(first, second, third)) + if err != nil { + t.Fatalf("%v", err) + } + + resp, err := promclient.Get("http://google.com") + if err != nil { + t.Fatalf("%v", err) + } + defer resp.Body.Close() + + for want, got := range order { + if want != got { + t.Fatalf("wanted %d, got %d", want, got) + } + } +} + func TestMiddlewareAPI(t *testing.T) { var ( his = prometheus.NewHistogram(prometheus.HistogramOpts{Name: "test_histogram"}) From e3d3aa8e58994b247245de8dd13ab6ff83b7acf6 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Wed, 30 Nov 2016 11:28:46 +0100 Subject: [PATCH 03/17] Add WIP middlewares --- prometheus/promhttp/middleware.go | 117 +++++++++++++++++++++++++ prometheus/promhttp/middleware_test.go | 76 ++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 prometheus/promhttp/middleware.go create mode 100644 prometheus/promhttp/middleware_test.go diff --git a/prometheus/promhttp/middleware.go b/prometheus/promhttp/middleware.go new file mode 100644 index 000000000..1e685c7dd --- /dev/null +++ b/prometheus/promhttp/middleware.go @@ -0,0 +1,117 @@ +// Copyright 2016 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. + +// Copyright (c) 2013, The Prometheus Authors +// All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package promhttp + +import ( + "context" + "net/http" + "net/http/httptrace" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +// ClientTrace adds middleware providing a histogram of outgoing request +// latencies, partitioned by http client, request host and httptrace event. +func ClientTrace(httpClientName string) middlewareFunc { + hist := prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "outgoing_httptrace", + Name: "duration_seconds", + ConstLabels: prometheus.Labels{"client": httpClientName}, + Help: "Histogram of outgoing request latencies.", + Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, + }, + []string{"host", "event"}, + ) + + if err := prometheus.Register(hist); err != nil { + if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + hist = are.ExistingCollector.(*prometheus.HistogramVec) + } else { + panic(err) + } + } + + return func(req *http.Request, next Middleware) (*http.Response, error) { + var ( + host = req.URL.Host + start = time.Now() + ) + + trace := &httptrace.ClientTrace{ + DNSStart: func(_ httptrace.DNSStartInfo) { + hist.WithLabelValues(host, "DNSStart").Observe(time.Since(start).Seconds()) + }, + DNSDone: func(_ httptrace.DNSDoneInfo) { + hist.WithLabelValues(host, "DNSDone").Observe(time.Since(start).Seconds()) + }, + ConnectStart: func(_, _ string) { + hist.WithLabelValues(host, "ConnectStart").Observe(time.Since(start).Seconds()) + }, + ConnectDone: func(net, addr string, err error) { + if err != nil { + return + } + hist.WithLabelValues(host, "ConnectDone").Observe(time.Since(start).Seconds()) + }, + GotConn: func(_ httptrace.GotConnInfo) { + hist.WithLabelValues(host, "GotConn").Observe(time.Since(start).Seconds()) + }, + GotFirstResponseByte: func() { + hist.WithLabelValues(host, "GotFirstResponseByte").Observe(time.Since(start).Seconds()) + }, + } + req = req.WithContext(httptrace.WithClientTrace(context.Background(), trace)) + + return next(req) + } +} + +// InFlight is middleware that instruments number of open requests partitioned +// by http client and request host. +var InFlight = func(httpClientName string) middlewareFunc { + gauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "outgoing", + Name: "open_requests", + ConstLabels: prometheus.Labels{"client": httpClientName}, + Help: "Gauge of open outgoing requests.", + }, + []string{"host"}, + ) + + if err := prometheus.Register(gauge); err != nil { + if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + gauge = are.ExistingCollector.(*prometheus.GaugeVec) + } else { + panic(err) + } + } + + return func(req *http.Request, next Middleware) (*http.Response, error) { + host := req.URL.Host + gauge.WithLabelValues(host).Inc() + + resp, err := next(req) + + gauge.WithLabelValues(host).Dec() + + return resp, err + } +} diff --git a/prometheus/promhttp/middleware_test.go b/prometheus/promhttp/middleware_test.go new file mode 100644 index 000000000..2a1871272 --- /dev/null +++ b/prometheus/promhttp/middleware_test.go @@ -0,0 +1,76 @@ +// Copyright 2016 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. + +// Copyright (c) 2013, The Prometheus Authors +// All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package promhttp + +import ( + "log" + "net/http" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestClientTraceMiddleware(t *testing.T) {} + +func ExampleMiddleware() { + t := time.NewTicker(time.Second) + defer t.Stop() + + client := *http.DefaultClient + client.Timeout = 500 * time.Millisecond + + counter := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "request_length_counter", + Help: "Counter of request length.", + }) + prometheus.MustRegister(counter) + customMiddleware := func(req *http.Request, next Middleware) (*http.Response, error) { + counter.Add(float64(req.ContentLength)) + + return next(req) + } + + promclient, err := NewClient( + SetClient(client), + SetMiddleware(ClientTrace("example_client"), customMiddleware, InFlight("example_client")), + ) + if err != nil { + log.Fatalln(err) + } + + go func() { + if err := http.ListenAndServe(":3000", prometheus.Handler()); err != nil { + log.Fatalln(err) + } + }() + + for { + select { + case <-t.C: + log.Println("sending GET") + resp, err := promclient.Get("http://example.com") + if err != nil { + log.Fatalln(err) + } + resp.Body.Close() + } + } +} From 7ea8059ef86fe8820bae1b55f62d6faa7ad67d1c Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Wed, 30 Nov 2016 11:29:06 +0100 Subject: [PATCH 04/17] Remove observers from client --- prometheus/promhttp/client.go | 17 +---------------- prometheus/promhttp/client_test.go | 22 +++------------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go index e5897b6f2..bbbdfd5f7 100644 --- a/prometheus/promhttp/client.go +++ b/prometheus/promhttp/client.go @@ -19,11 +19,7 @@ package promhttp -import ( - "net/http" - - "github.com/prometheus/client_golang/prometheus" -) +import "net/http" type Client struct { // Do we want to allow users to modify the underlying client after creating the wrapping client? @@ -31,8 +27,6 @@ type Client struct { // the user relying on modifying this behavior globally. c *http.Client - observers []prometheus.Observer - middleware Middleware } @@ -62,15 +56,6 @@ func (c *Client) do(r *http.Request, _ middlewareFunc) (*http.Response, error) { return c.c.Do(r) } -// SetObservers sets the observers on Client. Each Observer has Observe called -// at the conclusion of an HTTP roundtrip. -func SetObservers(obs []prometheus.Observer) ConfigFunc { - return func(c *Client) error { - c.observers = obs - return nil - } -} - func SetMiddleware(middlewares ...middlewareFunc) ConfigFunc { return func(c *Client) error { c.middleware = build(c, middlewares...) diff --git a/prometheus/promhttp/client_test.go b/prometheus/promhttp/client_test.go index 671f26680..9039d1bd2 100644 --- a/prometheus/promhttp/client_test.go +++ b/prometheus/promhttp/client_test.go @@ -69,49 +69,33 @@ func TestMiddlewareWrapsFirstToLast(t *testing.T) { } func TestMiddlewareAPI(t *testing.T) { - var ( - his = prometheus.NewHistogram(prometheus.HistogramOpts{Name: "test_histogram"}) - sum = prometheus.NewSummary(prometheus.SummaryOpts{Name: "test_summary"}) - gauge = prometheus.NewGauge(prometheus.GaugeOpts{Name: "test_gauge"}) - ) - - obs := []prometheus.Observer{ - his, - sum, - prometheus.ObserverFunc(gauge.Set), - } - client := *http.DefaultClient client.Timeout = 300 * time.Millisecond inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "inFlight"}) inFlight := func(r *http.Request, next Middleware) (*http.Response, error) { - log.Println("1st") inFlightGauge.Inc() resp, err := next(r) inFlightGauge.Dec() - log.Println("last") return resp, err } counter := prometheus.NewCounter(prometheus.CounterOpts{Name: "test_counter"}) - bytesSent := func(req *http.Request, next Middleware) (*http.Response, error) { + addFortyTwo := func(req *http.Request, next Middleware) (*http.Response, error) { counter.Add(42) - log.Println("2nd") - // counter.Add(float64(req.ContentLength)) return next(req) } logging := func(req *http.Request, next Middleware) (*http.Response, error) { - log.Println("3rd") + log.Println("log something interesting") return next(req) } - promclient, err := NewClient(SetClient(client), SetObservers(obs), SetMiddleware(inFlight, bytesSent, logging)) + promclient, err := NewClient(SetClient(client), SetMiddleware(inFlight, addFortyTwo, logging)) if err != nil { t.Fatalf("%v", err) } From 5402fe3f96ce3ad659507b700125dbe84c019c74 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 31 Mar 2017 12:02:47 +0200 Subject: [PATCH 05/17] Spike out rework with new Handler middleware --- prometheus/promhttp/client.go | 102 ++++++++++-------------------- prometheus/promhttp/middleware.go | 79 ++++++++--------------- 2 files changed, 60 insertions(+), 121 deletions(-) diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go index bbbdfd5f7..d26585106 100644 --- a/prometheus/promhttp/client.go +++ b/prometheus/promhttp/client.go @@ -19,29 +19,30 @@ package promhttp -import "net/http" - -type Client struct { - // Do we want to allow users to modify the underlying client after creating the wrapping client? - // c defaults to the http DefaultClient. We use a pointer to support - // the user relying on modifying this behavior globally. - c *http.Client - - middleware Middleware +import ( + "io" + "net/http" + "net/url" + "strings" +) + +type httpClient interface { + Do(*http.Request) (*http.Response, error) + Get(string) (*http.Response, error) + Head(string) (*http.Response, error) + Post(string, string, io.Reader) (*http.Response, error) + PostForm(string, url.Values) (*http.Response, error) } -type ConfigFunc func(*Client) error - type Middleware func(req *http.Request) (*http.Response, error) -type middlewareFunc func(req *http.Request, next Middleware) (*http.Response, error) +type Client struct { + c httpClient + middleware Middleware +} -func (c *Client) Head(url string) (*http.Response, error) { - req, err := http.NewRequest("HEAD", url, nil) - if err != nil { - return nil, err - } - return c.middleware(req) +func (c *Client) Do(r *http.Request) (*http.Response, error) { + return c.middleware(r) } func (c *Client) Get(url string) (resp *http.Response, err error) { @@ -49,63 +50,26 @@ func (c *Client) Get(url string) (resp *http.Response, err error) { if err != nil { return nil, err } - return c.middleware(req) + return c.Do(req) } -func (c *Client) do(r *http.Request, _ middlewareFunc) (*http.Response, error) { - return c.c.Do(r) -} - -func SetMiddleware(middlewares ...middlewareFunc) ConfigFunc { - return func(c *Client) error { - c.middleware = build(c, middlewares...) - return nil - } -} - -func build(c *Client, middlewares ...middlewareFunc) Middleware { - if len(middlewares) == 0 { - return endMiddleware(c) +func (c *Client) Head(url string) (*http.Response, error) { + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return nil, err } - - next := build(c, middlewares[1:]...) - - return wrap(middlewares[0], next) -} - -func wrap(fn middlewareFunc, next Middleware) Middleware { - return Middleware(func(r *http.Request) (*http.Response, error) { - return fn(r, next) - }) -} - -func endMiddleware(c *Client) Middleware { - return Middleware(func(r *http.Request) (*http.Response, error) { - return c.do(r, nil) - }) + return c.middleware(req) } -// SetClient sets the http client on Client. It accepts a value as opposed to a -// pointer to discourage untraced modification of a custom http client after it -// has been set. -func SetClient(client http.Client) ConfigFunc { - return func(c *Client) error { - c.c = &client - return nil +func (c *Client) Post(url string, contentType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err } + req.Header.Set("Content-Type", contentType) + return c.Do(req) } -func NewClient(fns ...ConfigFunc) (*Client, error) { - c := &Client{ - c: http.DefaultClient, - } - c.middleware = build(c) - - for _, fn := range fns { - if err := fn(c); err != nil { - return nil, err - } - } - - return c, nil +func (c *Client) PostForm(url string, data url.Values) (*http.Response, error) { + return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) } diff --git a/prometheus/promhttp/middleware.go b/prometheus/promhttp/middleware.go index 1e685c7dd..ba6dbe864 100644 --- a/prometheus/promhttp/middleware.go +++ b/prometheus/promhttp/middleware.go @@ -30,88 +30,63 @@ import ( // ClientTrace adds middleware providing a histogram of outgoing request // latencies, partitioned by http client, request host and httptrace event. -func ClientTrace(httpClientName string) middlewareFunc { - hist := prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "outgoing_httptrace", - Name: "duration_seconds", - ConstLabels: prometheus.Labels{"client": httpClientName}, - Help: "Histogram of outgoing request latencies.", - Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, - }, - []string{"host", "event"}, - ) - - if err := prometheus.Register(hist); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { - hist = are.ExistingCollector.(*prometheus.HistogramVec) - } else { - panic(err) - } +func ClientTrace(obs prometheus.ObserverVec, c httpClient) httpClient { + // The supplied histogram NEEDS a label for the httptrace event. + // TODO: Using `event` for now, but any other name is acceptable. + wc := &Client{ + c: c, } - return func(req *http.Request, next Middleware) (*http.Response, error) { + // TODO: Check for `event` label on histogram. + + wc.middleware = func(r *http.Request) (*http.Response, error) { var ( - host = req.URL.Host + host = r.URL.Host start = time.Now() ) trace := &httptrace.ClientTrace{ DNSStart: func(_ httptrace.DNSStartInfo) { - hist.WithLabelValues(host, "DNSStart").Observe(time.Since(start).Seconds()) + obs.WithLabelValues(host, "DNSStart").Observe(time.Since(start).Seconds()) }, DNSDone: func(_ httptrace.DNSDoneInfo) { - hist.WithLabelValues(host, "DNSDone").Observe(time.Since(start).Seconds()) + obs.WithLabelValues(host, "DNSDone").Observe(time.Since(start).Seconds()) }, ConnectStart: func(_, _ string) { - hist.WithLabelValues(host, "ConnectStart").Observe(time.Since(start).Seconds()) + obs.WithLabelValues(host, "ConnectStart").Observe(time.Since(start).Seconds()) }, ConnectDone: func(net, addr string, err error) { if err != nil { return } - hist.WithLabelValues(host, "ConnectDone").Observe(time.Since(start).Seconds()) + obs.WithLabelValues(host, "ConnectDone").Observe(time.Since(start).Seconds()) }, GotConn: func(_ httptrace.GotConnInfo) { - hist.WithLabelValues(host, "GotConn").Observe(time.Since(start).Seconds()) + obs.WithLabelValues(host, "GotConn").Observe(time.Since(start).Seconds()) }, GotFirstResponseByte: func() { - hist.WithLabelValues(host, "GotFirstResponseByte").Observe(time.Since(start).Seconds()) + obs.WithLabelValues(host, "GotFirstResponseByte").Observe(time.Since(start).Seconds()) }, } - req = req.WithContext(httptrace.WithClientTrace(context.Background(), trace)) + r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) - return next(req) + return wc.Do(r) } + + return wc } // InFlight is middleware that instruments number of open requests partitioned // by http client and request host. -var InFlight = func(httpClientName string) middlewareFunc { - gauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "outgoing", - Name: "open_requests", - ConstLabels: prometheus.Labels{"client": httpClientName}, - Help: "Gauge of open outgoing requests.", - }, - []string{"host"}, - ) - - if err := prometheus.Register(gauge); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { - gauge = are.ExistingCollector.(*prometheus.GaugeVec) - } else { - panic(err) - } +var InFlight = func(gauge prometheus.Gauge, c httpClient) httpClient { + wc := &Client{ + c: c, } - - return func(req *http.Request, next Middleware) (*http.Response, error) { - host := req.URL.Host - gauge.WithLabelValues(host).Inc() - - resp, err := next(req) - - gauge.WithLabelValues(host).Dec() - + wc.middleware = func(r *http.Request) (*http.Response, error) { + gauge.Inc() + resp, err := wc.Do(r) + gauge.Dec() return resp, err } + return wc } From d1470857fb26318c913ef418db324ae337946c63 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 31 Mar 2017 13:33:11 +0200 Subject: [PATCH 06/17] Rewrite client middleware --- prometheus/promhttp/client.go | 21 +++--- prometheus/promhttp/client_test.go | 88 +++++++------------------- prometheus/promhttp/middleware.go | 46 +++++++------- prometheus/promhttp/middleware_test.go | 76 ---------------------- 4 files changed, 54 insertions(+), 177 deletions(-) delete mode 100644 prometheus/promhttp/middleware_test.go diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go index d26585106..e3947962e 100644 --- a/prometheus/promhttp/client.go +++ b/prometheus/promhttp/client.go @@ -34,18 +34,13 @@ type httpClient interface { PostForm(string, url.Values) (*http.Response, error) } -type Middleware func(req *http.Request) (*http.Response, error) +type ClientMiddleware func(req *http.Request) (*http.Response, error) -type Client struct { - c httpClient - middleware Middleware +func (c ClientMiddleware) Do(r *http.Request) (*http.Response, error) { + return c(r) } -func (c *Client) Do(r *http.Request) (*http.Response, error) { - return c.middleware(r) -} - -func (c *Client) Get(url string) (resp *http.Response, err error) { +func (c ClientMiddleware) Get(url string) (resp *http.Response, err error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err @@ -53,15 +48,15 @@ func (c *Client) Get(url string) (resp *http.Response, err error) { return c.Do(req) } -func (c *Client) Head(url string) (*http.Response, error) { +func (c ClientMiddleware) Head(url string) (*http.Response, error) { req, err := http.NewRequest("HEAD", url, nil) if err != nil { return nil, err } - return c.middleware(req) + return c.Do(req) } -func (c *Client) Post(url string, contentType string, body io.Reader) (*http.Response, error) { +func (c ClientMiddleware) Post(url string, contentType string, body io.Reader) (*http.Response, error) { req, err := http.NewRequest("POST", url, body) if err != nil { return nil, err @@ -70,6 +65,6 @@ func (c *Client) Post(url string, contentType string, body io.Reader) (*http.Res return c.Do(req) } -func (c *Client) PostForm(url string, data url.Values) (*http.Response, error) { +func (c ClientMiddleware) PostForm(url string, data url.Values) (*http.Response, error) { return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) } diff --git a/prometheus/promhttp/client_test.go b/prometheus/promhttp/client_test.go index 9039d1bd2..ce720c355 100644 --- a/prometheus/promhttp/client_test.go +++ b/prometheus/promhttp/client_test.go @@ -20,89 +20,47 @@ package promhttp import ( - "log" "net/http" + "net/http/httputil" "testing" "time" "github.com/prometheus/client_golang/prometheus" ) -func TestMiddlewareWrapsFirstToLast(t *testing.T) { - order := []int{} - first := func(r *http.Request, next Middleware) (*http.Response, error) { - order = append(order, 0) - - resp, err := next(r) - - order = append(order, 3) - return resp, err - } - - second := func(req *http.Request, next Middleware) (*http.Response, error) { - order = append(order, 1) - - return next(req) - } - - third := func(req *http.Request, next Middleware) (*http.Response, error) { - order = append(order, 2) - return next(req) - } - - promclient, err := NewClient(SetMiddleware(first, second, third)) - if err != nil { - t.Fatalf("%v", err) - } - - resp, err := promclient.Get("http://google.com") - if err != nil { - t.Fatalf("%v", err) - } - defer resp.Body.Close() - - for want, got := range order { - if want != got { - t.Fatalf("wanted %d, got %d", want, got) - } - } -} - -func TestMiddlewareAPI(t *testing.T) { +func TestClientMiddlewareAPI(t *testing.T) { client := *http.DefaultClient client.Timeout = 300 * time.Millisecond inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "inFlight"}) - inFlight := func(r *http.Request, next Middleware) (*http.Response, error) { - inFlightGauge.Inc() - - resp, err := next(r) - - inFlightGauge.Dec() - - return resp, err - } - counter := prometheus.NewCounter(prometheus.CounterOpts{Name: "test_counter"}) - addFortyTwo := func(req *http.Request, next Middleware) (*http.Response, error) { - counter.Add(42) + counter := prometheus.NewCounterVec( + prometheus.CounterOpts{Name: "test_counter"}, + []string{"code", "method"}, + ) + + histVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "latency", + Buckets: prometheus.DefBuckets, + }, + []string{"event"}, + ) + + promclient := InFlightC(inFlightGauge, + CounterC(counter, + ClientTrace(histVec, &client), + ), + ) - return next(req) - } - - logging := func(req *http.Request, next Middleware) (*http.Response, error) { - log.Println("log something interesting") - return next(req) - } - - promclient, err := NewClient(SetClient(client), SetMiddleware(inFlight, addFortyTwo, logging)) + resp, err := promclient.Get("http://google.com") if err != nil { t.Fatalf("%v", err) } + defer resp.Body.Close() - resp, err := promclient.Get("http://google.com") + out, err := httputil.DumpResponse(resp, true) if err != nil { t.Fatalf("%v", err) } - defer resp.Body.Close() } diff --git a/prometheus/promhttp/middleware.go b/prometheus/promhttp/middleware.go index ba6dbe864..2ef1da5ec 100644 --- a/prometheus/promhttp/middleware.go +++ b/prometheus/promhttp/middleware.go @@ -33,60 +33,60 @@ import ( func ClientTrace(obs prometheus.ObserverVec, c httpClient) httpClient { // The supplied histogram NEEDS a label for the httptrace event. // TODO: Using `event` for now, but any other name is acceptable. - wc := &Client{ - c: c, - } // TODO: Check for `event` label on histogram. - wc.middleware = func(r *http.Request) (*http.Response, error) { + return ClientMiddleware(func(r *http.Request) (*http.Response, error) { var ( - host = r.URL.Host start = time.Now() ) trace := &httptrace.ClientTrace{ DNSStart: func(_ httptrace.DNSStartInfo) { - obs.WithLabelValues(host, "DNSStart").Observe(time.Since(start).Seconds()) + obs.WithLabelValues("DNSStart").Observe(time.Since(start).Seconds()) }, DNSDone: func(_ httptrace.DNSDoneInfo) { - obs.WithLabelValues(host, "DNSDone").Observe(time.Since(start).Seconds()) + obs.WithLabelValues("DNSDone").Observe(time.Since(start).Seconds()) }, ConnectStart: func(_, _ string) { - obs.WithLabelValues(host, "ConnectStart").Observe(time.Since(start).Seconds()) + obs.WithLabelValues("ConnectStart").Observe(time.Since(start).Seconds()) }, ConnectDone: func(net, addr string, err error) { if err != nil { return } - obs.WithLabelValues(host, "ConnectDone").Observe(time.Since(start).Seconds()) + obs.WithLabelValues("ConnectDone").Observe(time.Since(start).Seconds()) }, GotConn: func(_ httptrace.GotConnInfo) { - obs.WithLabelValues(host, "GotConn").Observe(time.Since(start).Seconds()) + obs.WithLabelValues("GotConn").Observe(time.Since(start).Seconds()) }, GotFirstResponseByte: func() { - obs.WithLabelValues(host, "GotFirstResponseByte").Observe(time.Since(start).Seconds()) + obs.WithLabelValues("GotFirstResponseByte").Observe(time.Since(start).Seconds()) }, } r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) - return wc.Do(r) - } - - return wc + return c.Do(r) + }) } // InFlight is middleware that instruments number of open requests partitioned // by http client and request host. -var InFlight = func(gauge prometheus.Gauge, c httpClient) httpClient { - wc := &Client{ - c: c, - } - wc.middleware = func(r *http.Request) (*http.Response, error) { +func InFlightC(gauge prometheus.Gauge, c httpClient) httpClient { + return ClientMiddleware(func(r *http.Request) (*http.Response, error) { gauge.Inc() - resp, err := wc.Do(r) + resp, err := c.Do(r) gauge.Dec() return resp, err - } - return wc + }) +} + +func CounterC(counter *prometheus.CounterVec, c httpClient) httpClient { + code, method := checkLabels(counter) + + return ClientMiddleware(func(r *http.Request) (*http.Response, error) { + resp, err := c.Do(r) + counter.With(labels(code, method, r.Method, resp.StatusCode)).Inc() + return resp, err + }) } diff --git a/prometheus/promhttp/middleware_test.go b/prometheus/promhttp/middleware_test.go deleted file mode 100644 index 2a1871272..000000000 --- a/prometheus/promhttp/middleware_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2016 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. - -// Copyright (c) 2013, The Prometheus Authors -// All rights reserved. -// -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package promhttp - -import ( - "log" - "net/http" - "testing" - "time" - - "github.com/prometheus/client_golang/prometheus" -) - -func TestClientTraceMiddleware(t *testing.T) {} - -func ExampleMiddleware() { - t := time.NewTicker(time.Second) - defer t.Stop() - - client := *http.DefaultClient - client.Timeout = 500 * time.Millisecond - - counter := prometheus.NewCounter(prometheus.CounterOpts{ - Name: "request_length_counter", - Help: "Counter of request length.", - }) - prometheus.MustRegister(counter) - customMiddleware := func(req *http.Request, next Middleware) (*http.Response, error) { - counter.Add(float64(req.ContentLength)) - - return next(req) - } - - promclient, err := NewClient( - SetClient(client), - SetMiddleware(ClientTrace("example_client"), customMiddleware, InFlight("example_client")), - ) - if err != nil { - log.Fatalln(err) - } - - go func() { - if err := http.ListenAndServe(":3000", prometheus.Handler()); err != nil { - log.Fatalln(err) - } - }() - - for { - select { - case <-t.C: - log.Println("sending GET") - resp, err := promclient.Get("http://example.com") - if err != nil { - log.Fatalln(err) - } - resp.Body.Close() - } - } -} From 4ddc6f50d99ed2b7f50f1229eba9ae7d84b4a3ae Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 31 Mar 2017 13:54:28 +0200 Subject: [PATCH 07/17] Add additional events for httptrace middleware --- prometheus/promhttp/middleware.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/prometheus/promhttp/middleware.go b/prometheus/promhttp/middleware.go index 2ef1da5ec..0c626291f 100644 --- a/prometheus/promhttp/middleware.go +++ b/prometheus/promhttp/middleware.go @@ -21,6 +21,7 @@ package promhttp import ( "context" + "crypto/tls" "net/http" "net/http/httptrace" "time" @@ -63,6 +64,18 @@ func ClientTrace(obs prometheus.ObserverVec, c httpClient) httpClient { GotFirstResponseByte: func() { obs.WithLabelValues("GotFirstResponseByte").Observe(time.Since(start).Seconds()) }, + TLSHandshakeStart: func() { + obs.WithLabelValues("TLSHandshakeStart").Observe(time.Since(start).Seconds()) + }, + TLSHandshakeDone: func(_ tls.ConnectionState, err error) { + if err != nil { + return + } + obs.WithLabelValues("TLSHandshakeDone").Observe(time.Since(start).Seconds()) + }, + WroteRequest: func(_ httptrace.WroteRequestInfo) { + obs.WithLabelValues("WroteRequest").Observe(time.Since(start).Seconds()) + }, } r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) @@ -76,6 +89,9 @@ func InFlightC(gauge prometheus.Gauge, c httpClient) httpClient { return ClientMiddleware(func(r *http.Request) (*http.Response, error) { gauge.Inc() resp, err := c.Do(r) + if err != nil { + return nil, err + } gauge.Dec() return resp, err }) @@ -86,6 +102,9 @@ func CounterC(counter *prometheus.CounterVec, c httpClient) httpClient { return ClientMiddleware(func(r *http.Request) (*http.Response, error) { resp, err := c.Do(r) + if err != nil { + return nil, err + } counter.With(labels(code, method, r.Method, resp.StatusCode)).Inc() return resp, err }) From 5e90e3f49ac6b1a4e33a90a6d3ff65d9fddaa54c Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 31 Mar 2017 14:05:02 +0200 Subject: [PATCH 08/17] Check for event label on tracing middleware --- prometheus/promhttp/client_test.go | 2 ++ prometheus/promhttp/middleware.go | 34 ++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/prometheus/promhttp/client_test.go b/prometheus/promhttp/client_test.go index ce720c355..1af77033c 100644 --- a/prometheus/promhttp/client_test.go +++ b/prometheus/promhttp/client_test.go @@ -20,6 +20,7 @@ package promhttp import ( + "fmt" "net/http" "net/http/httputil" "testing" @@ -63,4 +64,5 @@ func TestClientMiddlewareAPI(t *testing.T) { if err != nil { t.Fatalf("%v", err) } + fmt.Println(string(out)) } diff --git a/prometheus/promhttp/middleware.go b/prometheus/promhttp/middleware.go index 0c626291f..5b46cee51 100644 --- a/prometheus/promhttp/middleware.go +++ b/prometheus/promhttp/middleware.go @@ -27,6 +27,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" ) // ClientTrace adds middleware providing a histogram of outgoing request @@ -35,8 +36,7 @@ func ClientTrace(obs prometheus.ObserverVec, c httpClient) httpClient { // The supplied histogram NEEDS a label for the httptrace event. // TODO: Using `event` for now, but any other name is acceptable. - // TODO: Check for `event` label on histogram. - + checkEventLabel(obs) return ClientMiddleware(func(r *http.Request) (*http.Response, error) { var ( start = time.Now() @@ -109,3 +109,33 @@ func CounterC(counter *prometheus.CounterVec, c httpClient) httpClient { return resp, err }) } + +func checkEventLabel(c prometheus.Collector) { + var ( + desc *prometheus.Desc + pm dto.Metric + ) + + descc := make(chan *prometheus.Desc, 1) + c.Describe(descc) + + select { + case desc = <-descc: + default: + panic("no description provided by collector") + } + + m, err := prometheus.NewConstMetric(desc, prometheus.UntypedValue, 0, "") + if err != nil { + panic("error checking metric for labels") + } + + if err := m.Write(&pm); err != nil { + panic("error checking metric for labels") + } + + name := *pm.Label[0].Name + if name != "event" { + panic("metric partitioned with non-supported label") + } +} From db532611efbcd4b38d87a02b1c61ced994fe0051 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 31 Mar 2017 14:17:39 +0200 Subject: [PATCH 09/17] Try to make naming of `next` client clearer --- prometheus/promhttp/middleware.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/prometheus/promhttp/middleware.go b/prometheus/promhttp/middleware.go index 5b46cee51..cea92258a 100644 --- a/prometheus/promhttp/middleware.go +++ b/prometheus/promhttp/middleware.go @@ -32,7 +32,7 @@ import ( // ClientTrace adds middleware providing a histogram of outgoing request // latencies, partitioned by http client, request host and httptrace event. -func ClientTrace(obs prometheus.ObserverVec, c httpClient) httpClient { +func ClientTrace(obs prometheus.ObserverVec, next httpClient) httpClient { // The supplied histogram NEEDS a label for the httptrace event. // TODO: Using `event` for now, but any other name is acceptable. @@ -79,16 +79,16 @@ func ClientTrace(obs prometheus.ObserverVec, c httpClient) httpClient { } r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) - return c.Do(r) + return next.Do(r) }) } // InFlight is middleware that instruments number of open requests partitioned // by http client and request host. -func InFlightC(gauge prometheus.Gauge, c httpClient) httpClient { +func InFlightC(gauge prometheus.Gauge, next httpClient) httpClient { return ClientMiddleware(func(r *http.Request) (*http.Response, error) { gauge.Inc() - resp, err := c.Do(r) + resp, err := next.Do(r) if err != nil { return nil, err } @@ -97,11 +97,11 @@ func InFlightC(gauge prometheus.Gauge, c httpClient) httpClient { }) } -func CounterC(counter *prometheus.CounterVec, c httpClient) httpClient { +func CounterC(counter *prometheus.CounterVec, next httpClient) httpClient { code, method := checkLabels(counter) return ClientMiddleware(func(r *http.Request) (*http.Response, error) { - resp, err := c.Do(r) + resp, err := next.Do(r) if err != nil { return nil, err } From cd8e450f917beed201326daaa3ff00d3d0a407c4 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 31 Mar 2017 14:27:11 +0200 Subject: [PATCH 10/17] Fix naming of in_flight text example --- prometheus/promhttp/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus/promhttp/client_test.go b/prometheus/promhttp/client_test.go index 1af77033c..854b8a671 100644 --- a/prometheus/promhttp/client_test.go +++ b/prometheus/promhttp/client_test.go @@ -33,7 +33,7 @@ func TestClientMiddlewareAPI(t *testing.T) { client := *http.DefaultClient client.Timeout = 300 * time.Millisecond - inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "inFlight"}) + inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "in_flight"}) counter := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "test_counter"}, From 862df0776666ab2c0ba3d5a2eee73f968f943250 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 31 Mar 2017 14:36:29 +0200 Subject: [PATCH 11/17] Add comments to client.go --- prometheus/promhttp/client.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go index e3947962e..e48b1647d 100644 --- a/prometheus/promhttp/client.go +++ b/prometheus/promhttp/client.go @@ -34,12 +34,17 @@ type httpClient interface { PostForm(string, url.Values) (*http.Response, error) } +// ClientMiddleware is an adapter to allow wrapping an http.Client or other +// Middleware funcs, allowing the user to construct layers of middleware around +// an http client request. type ClientMiddleware func(req *http.Request) (*http.Response, error) +// Do implements the httpClient interface. func (c ClientMiddleware) Do(r *http.Request) (*http.Response, error) { return c(r) } +// Get implements the httpClient interface. func (c ClientMiddleware) Get(url string) (resp *http.Response, err error) { req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -48,6 +53,7 @@ func (c ClientMiddleware) Get(url string) (resp *http.Response, err error) { return c.Do(req) } +// Head implements the httpClient interface. func (c ClientMiddleware) Head(url string) (*http.Response, error) { req, err := http.NewRequest("HEAD", url, nil) if err != nil { @@ -56,6 +62,7 @@ func (c ClientMiddleware) Head(url string) (*http.Response, error) { return c.Do(req) } +// Post implements the httpClient interface. func (c ClientMiddleware) Post(url string, contentType string, body io.Reader) (*http.Response, error) { req, err := http.NewRequest("POST", url, body) if err != nil { @@ -65,6 +72,7 @@ func (c ClientMiddleware) Post(url string, contentType string, body io.Reader) ( return c.Do(req) } +// PostForm implements the httpClient interface. func (c ClientMiddleware) PostForm(url string, data url.Values) (*http.Response, error) { return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) } From 7397d022e1e4cf08f5560b0bcfa2563f2ea363d9 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 31 Mar 2017 14:39:33 +0200 Subject: [PATCH 12/17] Add middleware comments --- prometheus/promhttp/middleware.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/prometheus/promhttp/middleware.go b/prometheus/promhttp/middleware.go index cea92258a..10d35a34c 100644 --- a/prometheus/promhttp/middleware.go +++ b/prometheus/promhttp/middleware.go @@ -30,10 +30,12 @@ import ( dto "github.com/prometheus/client_model/go" ) -// ClientTrace adds middleware providing a histogram of outgoing request -// latencies, partitioned by http client, request host and httptrace event. +// ClientTrace accepts an ObserverVec interface and an httpClient, returning a +// new httpClient that wraps the supplied httpClient. The provided ObserverVec +// must be registered in a registry in order to be used. Note: Partitioning +// histograms is expensive. func ClientTrace(obs prometheus.ObserverVec, next httpClient) httpClient { - // The supplied histogram NEEDS a label for the httptrace event. + // The supplied ObserverVec NEEDS a label for the httptrace events. // TODO: Using `event` for now, but any other name is acceptable. checkEventLabel(obs) @@ -83,8 +85,9 @@ func ClientTrace(obs prometheus.ObserverVec, next httpClient) httpClient { }) } -// InFlight is middleware that instruments number of open requests partitioned -// by http client and request host. +// InFlightC accepts a Gauge and an httpClient, returning a new httpClient that +// wraps the supplied httpClient. The provided Gauge must be registered in a +// registry in order to be used. func InFlightC(gauge prometheus.Gauge, next httpClient) httpClient { return ClientMiddleware(func(r *http.Request) (*http.Response, error) { gauge.Inc() @@ -97,6 +100,9 @@ func InFlightC(gauge prometheus.Gauge, next httpClient) httpClient { }) } +// Counter accepts an CounterVec interface and an httpClient, returning a new +// httpClient that wraps the supplied httpClient. The provided CounterVec +// must be registered in a registry in order to be used. func CounterC(counter *prometheus.CounterVec, next httpClient) httpClient { code, method := checkLabels(counter) From 0a05bbc2d5e1eb1813d490bdb570b33f75a4c49c Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Fri, 31 Mar 2017 20:44:27 +0200 Subject: [PATCH 13/17] Simplify interface Only need to respond to Do, not every other method. --- prometheus/promhttp/client.go | 6 +----- prometheus/promhttp/middleware.go | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go index e48b1647d..bbbd7809e 100644 --- a/prometheus/promhttp/client.go +++ b/prometheus/promhttp/client.go @@ -26,12 +26,8 @@ import ( "strings" ) -type httpClient interface { +type doer interface { Do(*http.Request) (*http.Response, error) - Get(string) (*http.Response, error) - Head(string) (*http.Response, error) - Post(string, string, io.Reader) (*http.Response, error) - PostForm(string, url.Values) (*http.Response, error) } // ClientMiddleware is an adapter to allow wrapping an http.Client or other diff --git a/prometheus/promhttp/middleware.go b/prometheus/promhttp/middleware.go index 10d35a34c..ed6e90916 100644 --- a/prometheus/promhttp/middleware.go +++ b/prometheus/promhttp/middleware.go @@ -30,11 +30,11 @@ import ( dto "github.com/prometheus/client_model/go" ) -// ClientTrace accepts an ObserverVec interface and an httpClient, returning a -// new httpClient that wraps the supplied httpClient. The provided ObserverVec -// must be registered in a registry in order to be used. Note: Partitioning -// histograms is expensive. -func ClientTrace(obs prometheus.ObserverVec, next httpClient) httpClient { +// ClientTrace accepts an ObserverVec interface and a doer, returning a +// ClientMiddleware that wraps the supplied httpClient. The provided +// ObserverVec must be registered in a registry in order to be used. Note: +// Partitioning histograms is expensive. +func ClientTrace(obs prometheus.ObserverVec, next doer) ClientMiddleware { // The supplied ObserverVec NEEDS a label for the httptrace events. // TODO: Using `event` for now, but any other name is acceptable. @@ -85,10 +85,10 @@ func ClientTrace(obs prometheus.ObserverVec, next httpClient) httpClient { }) } -// InFlightC accepts a Gauge and an httpClient, returning a new httpClient that -// wraps the supplied httpClient. The provided Gauge must be registered in a -// registry in order to be used. -func InFlightC(gauge prometheus.Gauge, next httpClient) httpClient { +// InFlightC accepts a Gauge and an doer, returning a new ClientMiddleware that +// wraps the supplied doer. The provided Gauge must be registered in a registry +// in order to be used. +func InFlightC(gauge prometheus.Gauge, next doer) ClientMiddleware { return ClientMiddleware(func(r *http.Request) (*http.Response, error) { gauge.Inc() resp, err := next.Do(r) @@ -100,10 +100,10 @@ func InFlightC(gauge prometheus.Gauge, next httpClient) httpClient { }) } -// Counter accepts an CounterVec interface and an httpClient, returning a new -// httpClient that wraps the supplied httpClient. The provided CounterVec -// must be registered in a registry in order to be used. -func CounterC(counter *prometheus.CounterVec, next httpClient) httpClient { +// Counter accepts an CounterVec interface and an doer, returning a new +// ClientMiddleware that wraps the supplied doer. The provided CounterVec must +// be registered in a registry in order to be used. +func CounterC(counter *prometheus.CounterVec, next doer) ClientMiddleware { code, method := checkLabels(counter) return ClientMiddleware(func(r *http.Request) (*http.Response, error) { From bc804cc2837042ff53a667d60fb0931d9fc2784e Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Mon, 3 Apr 2017 10:30:40 +0200 Subject: [PATCH 14/17] Remove named return variables in client --- prometheus/promhttp/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go index bbbd7809e..e9a0fcdf0 100644 --- a/prometheus/promhttp/client.go +++ b/prometheus/promhttp/client.go @@ -41,7 +41,7 @@ func (c ClientMiddleware) Do(r *http.Request) (*http.Response, error) { } // Get implements the httpClient interface. -func (c ClientMiddleware) Get(url string) (resp *http.Response, err error) { +func (c ClientMiddleware) Get(url string) (*http.Response, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err From 9550ce2905b474c8474684d3606b11d252bdb985 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Tue, 4 Apr 2017 17:12:07 +0200 Subject: [PATCH 15/17] Use http.RoundTripper instead of Doer interface Same result, but uses stdlib code and doesn't mess with people's clients. --- prometheus/promhttp/client.go | 177 +++++++++++++++++++++++------ prometheus/promhttp/client_test.go | 38 +++++-- prometheus/promhttp/middleware.go | 147 ------------------------ 3 files changed, 170 insertions(+), 192 deletions(-) delete mode 100644 prometheus/promhttp/middleware.go diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go index e9a0fcdf0..457155a3f 100644 --- a/prometheus/promhttp/client.go +++ b/prometheus/promhttp/client.go @@ -20,55 +20,160 @@ package promhttp import ( - "io" + "context" + "crypto/tls" "net/http" - "net/url" - "strings" -) + "net/http/httptrace" + "time" -type doer interface { - Do(*http.Request) (*http.Response, error) -} + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" +) -// ClientMiddleware is an adapter to allow wrapping an http.Client or other +// RoundTripperFunc is an adapter to allow wrapping an http.Client or other // Middleware funcs, allowing the user to construct layers of middleware around // an http client request. -type ClientMiddleware func(req *http.Request) (*http.Response, error) +type RoundTripperFunc func(req *http.Request) (*http.Response, error) -// Do implements the httpClient interface. -func (c ClientMiddleware) Do(r *http.Request) (*http.Response, error) { - return c(r) +// RoundTrip implements the RoundTripper interface. +func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return rt(r) } -// Get implements the httpClient interface. -func (c ClientMiddleware) Get(url string) (*http.Response, error) { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - return c.Do(req) +// ClientTrace accepts an ObserverVec interface and a http.RoundTripper, +// returning a RoundTripperFunc that wraps the supplied httpClient. The +// provided ObserverVec must be registered in a registry in order to be used. +// Note: Partitioning histograms is expensive. +func ClientTrace(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc { + // The supplied ObserverVec NEEDS a label for the httptrace events. + // TODO: Using `event` for now, but any other name is acceptable. + + checkEventLabel(obs) + // TODO: Pass in struct of observers that map to the ClientTrace + // functions. + // Could use a vec if they want, but we only need an Observer (only + // call observe, they have to apply their own labels). + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + var ( + start = time.Now() + ) + + trace := &httptrace.ClientTrace{ + DNSStart: func(_ httptrace.DNSStartInfo) { + obs.WithLabelValues("DNSStart").Observe(time.Since(start).Seconds()) + }, + DNSDone: func(_ httptrace.DNSDoneInfo) { + obs.WithLabelValues("DNSDone").Observe(time.Since(start).Seconds()) + }, + ConnectStart: func(_, _ string) { + obs.WithLabelValues("ConnectStart").Observe(time.Since(start).Seconds()) + }, + ConnectDone: func(_, _ string, err error) { + if err != nil { + return + } + obs.WithLabelValues("ConnectDone").Observe(time.Since(start).Seconds()) + }, + GotFirstResponseByte: func() { + obs.WithLabelValues("GotFirstResponseByte").Observe(time.Since(start).Seconds()) + }, + TLSHandshakeStart: func() { + obs.WithLabelValues("TLSHandshakeStart").Observe(time.Since(start).Seconds()) + }, + TLSHandshakeDone: func(_ tls.ConnectionState, err error) { + if err != nil { + return + } + obs.WithLabelValues("TLSHandshakeDone").Observe(time.Since(start).Seconds()) + }, + WroteRequest: func(_ httptrace.WroteRequestInfo) { + obs.WithLabelValues("WroteRequest").Observe(time.Since(start).Seconds()) + }, + } + r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) + + return next.RoundTrip(r) + }) } -// Head implements the httpClient interface. -func (c ClientMiddleware) Head(url string) (*http.Response, error) { - req, err := http.NewRequest("HEAD", url, nil) - if err != nil { - return nil, err - } - return c.Do(req) +// InFlightC accepts a Gauge and an http.RoundTripper, returning a new +// RoundTripperFunc that wraps the supplied http.RoundTripper. The provided +// Gauge must be registered in a registry in order to be used. +func InFlightC(gauge prometheus.Gauge, next http.RoundTripper) RoundTripperFunc { + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + gauge.Inc() + resp, err := next.RoundTrip(r) + if err != nil { + return nil, err + } + gauge.Dec() + return resp, err + }) +} + +// Counter accepts an CounterVec interface and an http.RoundTripper, returning +// a new RoundTripperFunc that wraps the supplied http.RoundTripper. The +// provided CounterVec must be registered in a registry in order to be used. +func CounterC(counter *prometheus.CounterVec, next http.RoundTripper) RoundTripperFunc { + code, method := checkLabels(counter) + + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + resp, err := next.RoundTrip(r) + if err != nil { + return nil, err + } + counter.With(labels(code, method, r.Method, resp.StatusCode)).Inc() + return resp, err + }) +} + +// LatencyC accepts an ObserverVec interface and an http.RoundTripper, +// returning a new http.RoundTripper that wraps the supplied http.RoundTripper. +// The provided ObserverVec must be registered in a registry in order to be +// used. The instance labels "code" and "method" are supported on the provided +// ObserverVec. Note: Partitioning histograms is expensive. +func LatencyC(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc { + code, method := checkLabels(obs) + + return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { + var ( + start = time.Now() + resp, err = next.RoundTrip(r) + ) + if err != nil { + return nil, err + } + obs.With(labels(code, method, r.Method, resp.StatusCode)).Observe(time.Since(start).Seconds()) + return resp, err + }) } -// Post implements the httpClient interface. -func (c ClientMiddleware) Post(url string, contentType string, body io.Reader) (*http.Response, error) { - req, err := http.NewRequest("POST", url, body) +func checkEventLabel(c prometheus.Collector) { + var ( + desc *prometheus.Desc + pm dto.Metric + ) + + descc := make(chan *prometheus.Desc, 1) + c.Describe(descc) + + select { + case desc = <-descc: + default: + panic("no description provided by collector") + } + + m, err := prometheus.NewConstMetric(desc, prometheus.UntypedValue, 0, "") if err != nil { - return nil, err + panic("error checking metric for labels") } - req.Header.Set("Content-Type", contentType) - return c.Do(req) -} -// PostForm implements the httpClient interface. -func (c ClientMiddleware) PostForm(url string, data url.Values) (*http.Response, error) { - return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) + if err := m.Write(&pm); err != nil { + panic("error checking metric for labels") + } + + name := *pm.Label[0].Name + if name != "event" { + panic("metric partitioned with non-supported label") + } } diff --git a/prometheus/promhttp/client_test.go b/prometheus/promhttp/client_test.go index 854b8a671..a66b14852 100644 --- a/prometheus/promhttp/client_test.go +++ b/prometheus/promhttp/client_test.go @@ -30,31 +30,51 @@ import ( ) func TestClientMiddlewareAPI(t *testing.T) { - client := *http.DefaultClient - client.Timeout = 300 * time.Millisecond + client := http.DefaultClient + client.Timeout = 1 * time.Second - inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "in_flight"}) + inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "in_flight", + Help: "In-flight count.", + }) counter := prometheus.NewCounterVec( - prometheus.CounterOpts{Name: "test_counter"}, + prometheus.CounterOpts{ + Name: "test_counter", + Help: "Counter.", + }, []string{"code", "method"}, ) - histVec := prometheus.NewHistogramVec( + traceVec := prometheus.NewHistogramVec( prometheus.HistogramOpts{ - Name: "latency", + Name: "trace_latency", + Help: "Trace latency histogram.", Buckets: prometheus.DefBuckets, }, []string{"event"}, ) - promclient := InFlightC(inFlightGauge, + latencyVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "latency", + Help: "Overall latency histogram.", + Buckets: prometheus.DefBuckets, + }, + []string{"code", "method"}, + ) + + prometheus.MustRegister(counter, traceVec, latencyVec, inFlightGauge) + + client.Transport = InFlightC(inFlightGauge, CounterC(counter, - ClientTrace(histVec, &client), + ClientTrace(traceVec, + LatencyC(latencyVec, http.DefaultTransport), + ), ), ) - resp, err := promclient.Get("http://google.com") + resp, err := client.Get("http://google.com") if err != nil { t.Fatalf("%v", err) } diff --git a/prometheus/promhttp/middleware.go b/prometheus/promhttp/middleware.go deleted file mode 100644 index ed6e90916..000000000 --- a/prometheus/promhttp/middleware.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2016 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. - -// Copyright (c) 2013, The Prometheus Authors -// All rights reserved. -// -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package promhttp - -import ( - "context" - "crypto/tls" - "net/http" - "net/http/httptrace" - "time" - - "github.com/prometheus/client_golang/prometheus" - dto "github.com/prometheus/client_model/go" -) - -// ClientTrace accepts an ObserverVec interface and a doer, returning a -// ClientMiddleware that wraps the supplied httpClient. The provided -// ObserverVec must be registered in a registry in order to be used. Note: -// Partitioning histograms is expensive. -func ClientTrace(obs prometheus.ObserverVec, next doer) ClientMiddleware { - // The supplied ObserverVec NEEDS a label for the httptrace events. - // TODO: Using `event` for now, but any other name is acceptable. - - checkEventLabel(obs) - return ClientMiddleware(func(r *http.Request) (*http.Response, error) { - var ( - start = time.Now() - ) - - trace := &httptrace.ClientTrace{ - DNSStart: func(_ httptrace.DNSStartInfo) { - obs.WithLabelValues("DNSStart").Observe(time.Since(start).Seconds()) - }, - DNSDone: func(_ httptrace.DNSDoneInfo) { - obs.WithLabelValues("DNSDone").Observe(time.Since(start).Seconds()) - }, - ConnectStart: func(_, _ string) { - obs.WithLabelValues("ConnectStart").Observe(time.Since(start).Seconds()) - }, - ConnectDone: func(net, addr string, err error) { - if err != nil { - return - } - obs.WithLabelValues("ConnectDone").Observe(time.Since(start).Seconds()) - }, - GotConn: func(_ httptrace.GotConnInfo) { - obs.WithLabelValues("GotConn").Observe(time.Since(start).Seconds()) - }, - GotFirstResponseByte: func() { - obs.WithLabelValues("GotFirstResponseByte").Observe(time.Since(start).Seconds()) - }, - TLSHandshakeStart: func() { - obs.WithLabelValues("TLSHandshakeStart").Observe(time.Since(start).Seconds()) - }, - TLSHandshakeDone: func(_ tls.ConnectionState, err error) { - if err != nil { - return - } - obs.WithLabelValues("TLSHandshakeDone").Observe(time.Since(start).Seconds()) - }, - WroteRequest: func(_ httptrace.WroteRequestInfo) { - obs.WithLabelValues("WroteRequest").Observe(time.Since(start).Seconds()) - }, - } - r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) - - return next.Do(r) - }) -} - -// InFlightC accepts a Gauge and an doer, returning a new ClientMiddleware that -// wraps the supplied doer. The provided Gauge must be registered in a registry -// in order to be used. -func InFlightC(gauge prometheus.Gauge, next doer) ClientMiddleware { - return ClientMiddleware(func(r *http.Request) (*http.Response, error) { - gauge.Inc() - resp, err := next.Do(r) - if err != nil { - return nil, err - } - gauge.Dec() - return resp, err - }) -} - -// Counter accepts an CounterVec interface and an doer, returning a new -// ClientMiddleware that wraps the supplied doer. The provided CounterVec must -// be registered in a registry in order to be used. -func CounterC(counter *prometheus.CounterVec, next doer) ClientMiddleware { - code, method := checkLabels(counter) - - return ClientMiddleware(func(r *http.Request) (*http.Response, error) { - resp, err := next.Do(r) - if err != nil { - return nil, err - } - counter.With(labels(code, method, r.Method, resp.StatusCode)).Inc() - return resp, err - }) -} - -func checkEventLabel(c prometheus.Collector) { - var ( - desc *prometheus.Desc - pm dto.Metric - ) - - descc := make(chan *prometheus.Desc, 1) - c.Describe(descc) - - select { - case desc = <-descc: - default: - panic("no description provided by collector") - } - - m, err := prometheus.NewConstMetric(desc, prometheus.UntypedValue, 0, "") - if err != nil { - panic("error checking metric for labels") - } - - if err := m.Write(&pm); err != nil { - panic("error checking metric for labels") - } - - name := *pm.Label[0].Name - if name != "event" { - panic("metric partitioned with non-supported label") - } -} From 60166b2d1cd52eee9a083bf3fa7f5bf62527ead2 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Tue, 4 Apr 2017 18:52:21 +0200 Subject: [PATCH 16/17] Implement InstrumentTrace InstrumentTrace allows for greater flexibility in instrumenting the trace function, allowing users to pass in uniquely partitioned and bucketed histograms, e.g., or have additional counters for certain trace hooks. --- prometheus/promhttp/client.go | 89 +++++++++++++++++++++++------- prometheus/promhttp/client_test.go | 35 ++++++++++-- 2 files changed, 98 insertions(+), 26 deletions(-) diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go index 457155a3f..aabfecade 100644 --- a/prometheus/promhttp/client.go +++ b/prometheus/promhttp/client.go @@ -40,54 +40,103 @@ func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return rt(r) } -// ClientTrace accepts an ObserverVec interface and a http.RoundTripper, -// returning a RoundTripperFunc that wraps the supplied httpClient. The -// provided ObserverVec must be registered in a registry in order to be used. +// InstrumentTrace is used to offer flexibility in instrumenting the available +// httptrace.ClientTrace hooks. Each function is passed a float64 representing +// the time in seconds since the start of the http request. A user may choose +// to use separately buckets Histograms, or implement custom instance labels +// per function. +type InstrumentTrace struct { + GetConn, GotConn, PutIdleConn, GotFirstResponseByte, Got100Continue, DNSStart, DNSDone, ConnectStart, ConnectDone, TLSHandshakeStart, TLSHandshakeDone, WroteHeaders, Wait100Continue, WroteRequest func(float64) +} + +// ClientTrace accepts an InstrumentTrace structand a http.RoundTripper, +// returning a RoundTripperFunc that wraps the supplied http.RoundTripper. // Note: Partitioning histograms is expensive. -func ClientTrace(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc { - // The supplied ObserverVec NEEDS a label for the httptrace events. - // TODO: Using `event` for now, but any other name is acceptable. - - checkEventLabel(obs) - // TODO: Pass in struct of observers that map to the ClientTrace - // functions. - // Could use a vec if they want, but we only need an Observer (only - // call observe, they have to apply their own labels). +func ClientTrace(it *InstrumentTrace, next http.RoundTripper) RoundTripperFunc { return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { var ( start = time.Now() ) trace := &httptrace.ClientTrace{ + GetConn: func(_ string) { + if it.GetConn != nil { + it.GetConn(time.Since(start).Seconds()) + } + }, + GotConn: func(_ httptrace.GotConnInfo) { + if it.GotConn != nil { + it.GotConn(time.Since(start).Seconds()) + } + }, + PutIdleConn: func(err error) { + if err != nil { + return + } + if it.PutIdleConn != nil { + it.PutIdleConn(time.Since(start).Seconds()) + } + }, DNSStart: func(_ httptrace.DNSStartInfo) { - obs.WithLabelValues("DNSStart").Observe(time.Since(start).Seconds()) + if it.DNSStart != nil { + it.DNSStart(time.Since(start).Seconds()) + } }, DNSDone: func(_ httptrace.DNSDoneInfo) { - obs.WithLabelValues("DNSDone").Observe(time.Since(start).Seconds()) + if it.DNSStart != nil { + it.DNSStart(time.Since(start).Seconds()) + } }, ConnectStart: func(_, _ string) { - obs.WithLabelValues("ConnectStart").Observe(time.Since(start).Seconds()) + if it.ConnectStart != nil { + it.ConnectStart(time.Since(start).Seconds()) + } }, ConnectDone: func(_, _ string, err error) { if err != nil { return } - obs.WithLabelValues("ConnectDone").Observe(time.Since(start).Seconds()) + if it.ConnectDone != nil { + it.ConnectDone(time.Since(start).Seconds()) + } }, GotFirstResponseByte: func() { - obs.WithLabelValues("GotFirstResponseByte").Observe(time.Since(start).Seconds()) + if it.GotFirstResponseByte != nil { + it.GotFirstResponseByte(time.Since(start).Seconds()) + } + }, + Got100Continue: func() { + if it.Got100Continue != nil { + it.Got100Continue(time.Since(start).Seconds()) + } }, TLSHandshakeStart: func() { - obs.WithLabelValues("TLSHandshakeStart").Observe(time.Since(start).Seconds()) + if it.TLSHandshakeStart != nil { + it.TLSHandshakeStart(time.Since(start).Seconds()) + } }, TLSHandshakeDone: func(_ tls.ConnectionState, err error) { if err != nil { return } - obs.WithLabelValues("TLSHandshakeDone").Observe(time.Since(start).Seconds()) + if it.TLSHandshakeDone != nil { + it.TLSHandshakeDone(time.Since(start).Seconds()) + } + }, + WroteHeaders: func() { + if it.WroteHeaders != nil { + it.WroteHeaders(time.Since(start).Seconds()) + } + }, + Wait100Continue: func() { + if it.Wait100Continue != nil { + it.Wait100Continue(time.Since(start).Seconds()) + } }, WroteRequest: func(_ httptrace.WroteRequestInfo) { - obs.WithLabelValues("WroteRequest").Observe(time.Since(start).Seconds()) + if it.WroteRequest != nil { + it.WroteRequest(time.Since(start).Seconds()) + } }, } r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) diff --git a/prometheus/promhttp/client_test.go b/prometheus/promhttp/client_test.go index a66b14852..f60255c9b 100644 --- a/prometheus/promhttp/client_test.go +++ b/prometheus/promhttp/client_test.go @@ -46,11 +46,19 @@ func TestClientMiddlewareAPI(t *testing.T) { []string{"code", "method"}, ) - traceVec := prometheus.NewHistogramVec( + dnsLatencyVec := prometheus.NewHistogramVec( prometheus.HistogramOpts{ - Name: "trace_latency", - Help: "Trace latency histogram.", - Buckets: prometheus.DefBuckets, + Name: "dns_latency", + Help: "Trace dns latency histogram.", + Buckets: []float64{.005, .01, .025, .05}, + }, + []string{"event"}, + ) + tlsLatencyVec := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "tls_latency", + Help: "Trace tls latency histogram.", + Buckets: []float64{.05, .1, .25, .5}, }, []string{"event"}, ) @@ -64,11 +72,26 @@ func TestClientMiddlewareAPI(t *testing.T) { []string{"code", "method"}, ) - prometheus.MustRegister(counter, traceVec, latencyVec, inFlightGauge) + prometheus.MustRegister(counter, tlsLatencyVec, dnsLatencyVec, latencyVec, inFlightGauge) + + trace := &InstrumentTrace{ + DNSStart: func(t float64) { + dnsLatencyVec.WithLabelValues("DNSStart") + }, + DNSDone: func(t float64) { + dnsLatencyVec.WithLabelValues("DNSDone") + }, + TLSHandshakeStart: func(t float64) { + tlsLatencyVec.WithLabelValues("TLSHandshakeStart") + }, + TLSHandshakeDone: func(t float64) { + tlsLatencyVec.WithLabelValues("TLSHandshakeDone") + }, + } client.Transport = InFlightC(inFlightGauge, CounterC(counter, - ClientTrace(traceVec, + ClientTrace(trace, LatencyC(latencyVec, http.DefaultTransport), ), ), From ff6630365b4f5bf64c7b072c5cb11e5f1ba8a7ae Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Wed, 5 Apr 2017 15:29:53 +0200 Subject: [PATCH 17/17] Update naming --- prometheus/promhttp/client.go | 43 ++++++++++++++---------------- prometheus/promhttp/client_test.go | 8 +++--- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/prometheus/promhttp/client.go b/prometheus/promhttp/client.go index aabfecade..5011c03ba 100644 --- a/prometheus/promhttp/client.go +++ b/prometheus/promhttp/client.go @@ -46,24 +46,20 @@ func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { // to use separately buckets Histograms, or implement custom instance labels // per function. type InstrumentTrace struct { - GetConn, GotConn, PutIdleConn, GotFirstResponseByte, Got100Continue, DNSStart, DNSDone, ConnectStart, ConnectDone, TLSHandshakeStart, TLSHandshakeDone, WroteHeaders, Wait100Continue, WroteRequest func(float64) + GotConn, PutIdleConn, GotFirstResponseByte, Got100Continue, DNSStart, DNSDone, ConnectStart, ConnectDone, TLSHandshakeStart, TLSHandshakeDone, WroteHeaders, Wait100Continue, WroteRequest func(float64) } -// ClientTrace accepts an InstrumentTrace structand a http.RoundTripper, -// returning a RoundTripperFunc that wraps the supplied http.RoundTripper. +// InstrumentRoundTripperTrace accepts an InstrumentTrace structand a +// http.RoundTripper, returning a RoundTripperFunc that wraps the supplied +// http.RoundTripper. // Note: Partitioning histograms is expensive. -func ClientTrace(it *InstrumentTrace, next http.RoundTripper) RoundTripperFunc { +func InstrumentRoundTripperTrace(it *InstrumentTrace, next http.RoundTripper) RoundTripperFunc { return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { var ( start = time.Now() ) trace := &httptrace.ClientTrace{ - GetConn: func(_ string) { - if it.GetConn != nil { - it.GetConn(time.Since(start).Seconds()) - } - }, GotConn: func(_ httptrace.GotConnInfo) { if it.GotConn != nil { it.GotConn(time.Since(start).Seconds()) @@ -145,10 +141,10 @@ func ClientTrace(it *InstrumentTrace, next http.RoundTripper) RoundTripperFunc { }) } -// InFlightC accepts a Gauge and an http.RoundTripper, returning a new -// RoundTripperFunc that wraps the supplied http.RoundTripper. The provided -// Gauge must be registered in a registry in order to be used. -func InFlightC(gauge prometheus.Gauge, next http.RoundTripper) RoundTripperFunc { +// InstrumentRoundTripperInFlight accepts a Gauge and an http.RoundTripper, +// returning a new RoundTripperFunc that wraps the supplied http.RoundTripper. +// The provided Gauge must be registered in a registry in order to be used. +func InstrumentRoundTripperInFlight(gauge prometheus.Gauge, next http.RoundTripper) RoundTripperFunc { return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { gauge.Inc() resp, err := next.RoundTrip(r) @@ -160,10 +156,11 @@ func InFlightC(gauge prometheus.Gauge, next http.RoundTripper) RoundTripperFunc }) } -// Counter accepts an CounterVec interface and an http.RoundTripper, returning -// a new RoundTripperFunc that wraps the supplied http.RoundTripper. The -// provided CounterVec must be registered in a registry in order to be used. -func CounterC(counter *prometheus.CounterVec, next http.RoundTripper) RoundTripperFunc { +// InstrumentRoundTripperCounter accepts an CounterVec interface and an +// http.RoundTripper, returning a new RoundTripperFunc that wraps the supplied +// http.RoundTripper. The provided CounterVec must be registered in a registry +// in order to be used. +func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.RoundTripper) RoundTripperFunc { code, method := checkLabels(counter) return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { @@ -176,12 +173,12 @@ func CounterC(counter *prometheus.CounterVec, next http.RoundTripper) RoundTripp }) } -// LatencyC accepts an ObserverVec interface and an http.RoundTripper, -// returning a new http.RoundTripper that wraps the supplied http.RoundTripper. -// The provided ObserverVec must be registered in a registry in order to be -// used. The instance labels "code" and "method" are supported on the provided -// ObserverVec. Note: Partitioning histograms is expensive. -func LatencyC(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc { +// InstrumentRoundTripperDuration accepts an ObserverVec interface and an +// http.RoundTripper, returning a new http.RoundTripper that wraps the supplied +// http.RoundTripper. The provided ObserverVec must be registered in a registry +// in order to be used. The instance labels "code" and "method" are supported +// on the provided ObserverVec. Note: Partitioning histograms is expensive. +func InstrumentRoundTripperDuration(obs prometheus.ObserverVec, next http.RoundTripper) RoundTripperFunc { code, method := checkLabels(obs) return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { diff --git a/prometheus/promhttp/client_test.go b/prometheus/promhttp/client_test.go index f60255c9b..3bd922a75 100644 --- a/prometheus/promhttp/client_test.go +++ b/prometheus/promhttp/client_test.go @@ -89,10 +89,10 @@ func TestClientMiddlewareAPI(t *testing.T) { }, } - client.Transport = InFlightC(inFlightGauge, - CounterC(counter, - ClientTrace(trace, - LatencyC(latencyVec, http.DefaultTransport), + client.Transport = InstrumentRoundTripperInFlight(inFlightGauge, + InstrumentRoundTripperCounter(counter, + InstrumentRoundTripperTrace(trace, + InstrumentRoundTripperDuration(latencyVec, http.DefaultTransport), ), ), )