-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Instrument RoundTripper via middleware #295
Changes from 4 commits
78c294c
29ba5d1
ec79472
2c1b043
0fba3c1
890cefe
e7cbef5
3b0e2d1
60ff76b
1544107
51be061
63bb61a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,8 @@ sudo: false | |
language: go | ||
|
||
go: | ||
- 1.6.3 | ||
- 1.7 | ||
- 1.8.1 | ||
|
||
script: | ||
- go test -short ./... | ||
- go test -short ./... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
// 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 ( | ||
"context" | ||
"crypto/tls" | ||
"net/http" | ||
"net/http/httptrace" | ||
"time" | ||
|
||
"github.com/prometheus/client_golang/prometheus" | ||
dto "github.com/prometheus/client_model/go" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line should be before the previous one and separated by a blank line (coming from a different repo). |
||
) | ||
|
||
// RoundTripperFunc is an adapter to allow wrapping an interface implementing | ||
// http.RoundTripper, allowing the user to construct layers of middleware. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The doc comment is the wrong way around. I'd just mirror the doc comment of the standard HandlerFunc, i.e. "The RoundTripperFunc type is an adapter to allow the use of ordinary functions as RoundTrippers. If f is a function with the appropriate signature, RountTriiperFunc(f) is a RoundTripper that calls f." |
||
type RoundTripperFunc func(req *http.Request) (*http.Response, error) | ||
|
||
// RoundTrip implements the RoundTripper interface. | ||
func (rt RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { | ||
return rt(r) | ||
} | ||
|
||
// InstrumentTrace is used to offer flexibility in instrumenting the available | ||
// httptrace.ClientTrace hook functions. 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 on a per function basis. | ||
type InstrumentTrace struct { | ||
GotConn, PutIdleConn, GotFirstResponseByte, Got100Continue, DNSStart, DNSDone, ConnectStart, ConnectDone, TLSHandshakeStart, TLSHandshakeDone, WroteHeaders, Wait100Continue, WroteRequest func(float64) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to list one struct member per line, even if that means repeating the value, than having such a long line. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally cool with me |
||
} | ||
|
||
// InstrumentRoundTripperTrace is a middleware that wraps the provided | ||
// RoundTripper and reports times to hook functions provided in the | ||
// InstrumentTrace struct. Hook functions that are not present in the provided | ||
// InstrumentTrace struct are ignored. Times reported to the hook functions are | ||
// time since the start of the request. Note that partitioning of Histograms | ||
// is expensive and should be used judiciously. | ||
// | ||
// See the example for ExampleInstrumentRoundTripperDuration for example usage. | ||
func InstrumentRoundTripperTrace(it *InstrumentTrace, next http.RoundTripper) RoundTripperFunc { | ||
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) { | ||
var ( | ||
start = time.Now() | ||
) | ||
|
||
trace := &httptrace.ClientTrace{ | ||
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) { | ||
if it.DNSStart != nil { | ||
it.DNSStart(time.Since(start).Seconds()) | ||
} | ||
}, | ||
DNSDone: func(_ httptrace.DNSDoneInfo) { | ||
if it.DNSStart != nil { | ||
it.DNSStart(time.Since(start).Seconds()) | ||
} | ||
}, | ||
ConnectStart: func(_, _ string) { | ||
if it.ConnectStart != nil { | ||
it.ConnectStart(time.Since(start).Seconds()) | ||
} | ||
}, | ||
ConnectDone: func(_, _ string, err error) { | ||
if err != nil { | ||
return | ||
} | ||
if it.ConnectDone != nil { | ||
it.ConnectDone(time.Since(start).Seconds()) | ||
} | ||
}, | ||
GotFirstResponseByte: func() { | ||
if it.GotFirstResponseByte != nil { | ||
it.GotFirstResponseByte(time.Since(start).Seconds()) | ||
} | ||
}, | ||
Got100Continue: func() { | ||
if it.Got100Continue != nil { | ||
it.Got100Continue(time.Since(start).Seconds()) | ||
} | ||
}, | ||
TLSHandshakeStart: func() { | ||
if it.TLSHandshakeStart != nil { | ||
it.TLSHandshakeStart(time.Since(start).Seconds()) | ||
} | ||
}, | ||
TLSHandshakeDone: func(_ tls.ConnectionState, err error) { | ||
if err != nil { | ||
return | ||
} | ||
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) { | ||
if it.WroteRequest != nil { | ||
it.WroteRequest(time.Since(start).Seconds()) | ||
} | ||
}, | ||
} | ||
r = r.WithContext(httptrace.WithClientTrace(context.Background(), trace)) | ||
|
||
return next.RoundTrip(r) | ||
}) | ||
} | ||
|
||
// InstrumentRoundTripperInFlight is a middleware that wraps the provided | ||
// http.RoundTripper. It sets the provided prometheus.Gauge to the number of | ||
// requests currently handled by the wrapped http.RoundTripper. | ||
// | ||
// See the example for ExampleInstrumentRoundTripperDuration for example usage. | ||
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) | ||
if err != nil { | ||
return nil, err | ||
} | ||
gauge.Dec() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be |
||
return resp, err | ||
}) | ||
} | ||
|
||
// InstrumentRoundTripperCounter is a middleware that wraps the provided | ||
// http.RoundTripper to observe the request result with the provided CounterVec. | ||
// The CounterVec must have zero, one, or two labels. The only allowed label | ||
// names are "code" and "method". The function panics if any other instance | ||
// labels are provided. 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 observations, use a CounterVec with | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. observations → counting This actually applies to |
||
// zero labels. | ||
// | ||
// If the wrapped RoundTripper panics, the Counter is not incremented. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...panics or returns a non-nil error, ... |
||
// | ||
// See the example for ExampleInstrumentRoundTripperDuration for example usage. | ||
func InstrumentRoundTripperCounter(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 | ||
}) | ||
} | ||
|
||
// InstrumentRoundTripperDuration is a middleware that wraps the provided | ||
// http.RoundTripper to observe the request duration with the provided ObserverVec. | ||
// The ObserverVec must have zero, one, or two labels. The only allowed label | ||
// names are "code" and "method". The function panics if any other instance | ||
// labels are provided. 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 RoundTripper panics, no values are reported. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... or returns a non-nil error... |
||
func InstrumentRoundTripperDuration(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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if the otherwise neat |
||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
obs.With(labels(code, method, r.Method, resp.StatusCode)).Observe(time.Since(start).Seconds()) | ||
return resp, err | ||
}) | ||
} | ||
|
||
func checkEventLabel(c prometheus.Collector) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't used anywhere. |
||
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") | ||
} | ||
select { | ||
case <-descc: | ||
panic("more than one description provided by collector") | ||
default: | ||
} | ||
|
||
close(descc) | ||
|
||
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") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We just had a discussion about supported versions in #291 and the decision was to test against 1.6 but to just build tags for 1.7+ features.