tavern/common/http.go

202 lines
5.5 KiB
Go

package common
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// HTTPClient provides the http.Do function in a generic way.
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type AcceptHeader struct {
Type string
SubType string
}
type AcceptHeaders []AcceptHeader
// DefaultHTTPClient returns an HTTPClient with a reasonable default timeout.
func DefaultHTTPClient() *http.Client {
return &http.Client{
Timeout: 10 * time.Second,
// By default, the HTTP client shouldn't follow redirects.
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
func InstrumentedDefaultHTTPClient(metricFactory promauto.Factory, namespace, subsystem string) *http.Client {
client := &http.Client{
Timeout: 10 * time.Second,
// By default, the HTTP client shouldn't follow redirects.
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
inFlightGauge := metricFactory.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "in_flight_requests",
Help: "A gauge of in-flight requests for the wrapped client.",
})
counter := metricFactory.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "requests_total",
Help: "A counter for requests from the wrapped client.",
},
[]string{"code", "method"},
)
dnsLatencyVec := metricFactory.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "dns_duration_seconds",
Help: "Trace dns latency histogram.",
Buckets: []float64{.005, .01, .025, .05},
},
[]string{"event"},
)
tlsLatencyVec := metricFactory.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "tls_duration_seconds",
Help: "Trace tls latency histogram.",
Buckets: []float64{.05, .1, .25, .5},
},
[]string{"event"},
)
histVec := metricFactory.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "request_duration_seconds",
Help: "A histogram of request latencies.",
Buckets: prometheus.DefBuckets,
},
[]string{"method"},
)
trace := &promhttp.InstrumentTrace{
DNSStart: func(t float64) {
dnsLatencyVec.WithLabelValues("dns_start").Observe(t)
},
DNSDone: func(t float64) {
dnsLatencyVec.WithLabelValues("dns_done").Observe(t)
},
TLSHandshakeStart: func(t float64) {
tlsLatencyVec.WithLabelValues("tls_handshake_start").Observe(t)
},
TLSHandshakeDone: func(t float64) {
tlsLatencyVec.WithLabelValues("tls_handshake_done").Observe(t)
},
}
client.Transport = promhttp.InstrumentRoundTripperInFlight(inFlightGauge,
promhttp.InstrumentRoundTripperCounter(counter,
promhttp.InstrumentRoundTripperTrace(trace,
promhttp.InstrumentRoundTripperDuration(histVec, http.DefaultTransport),
),
),
)
return client
}
func ParseAccept(input string) AcceptHeaders {
// From the spec, https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
// If no Accept header field is present, then it is assumed that the client
// accepts all media types.
if len(input) == 0 {
return []AcceptHeader{
{Type: "*", SubType: "*"},
}
}
parts := strings.FieldsFunc(input, func(r rune) bool {
return r == ','
})
results := make([]AcceptHeader, 0)
for _, part := range parts {
mediaType := part
if pos := strings.IndexRune(mediaType, ';'); pos > -1 {
mediaType = mediaType[0:pos]
}
mediaTypeParts := strings.Split(mediaType, "/")
if len(mediaTypeParts) == 2 {
results = append(results, AcceptHeader{
Type: mediaTypeParts[0],
SubType: mediaTypeParts[1],
})
}
}
return results
}
func (a AcceptHeader) String() string {
return fmt.Sprintf("%s/%s", a.Type, a.SubType)
}
// Match returns true if a given type is contained. This assumes the order of
// accepted headers and matching types is provided as intended.
// (text/plain, */*).Match(text/plain) => true (text/plain)
// (text/plain, */*).Match(*/*) => true (*/*)
// (text/*, */*).Match(text/plain) => true (text/plain)
// (text/*, text/plain).Match(text/plain) => true (text/*)
func (a AcceptHeaders) Match(exact bool, types ...string) (string, string, bool) {
parsedTypes := make(AcceptHeaders, 0)
for _, t := range types {
mediaTypeParts := strings.Split(t, "/")
if len(mediaTypeParts) == 2 {
parsedTypes = append(parsedTypes, AcceptHeader{
Type: mediaTypeParts[0],
SubType: mediaTypeParts[1],
})
}
}
for _, acceptHeader := range a {
for _, parsedType := range parsedTypes {
// fmt.Println("accept", acceptHeader.Type, acceptHeader.SubType, "parsed", parsedType.Type, parsedType.SubType)
if !exact && acceptHeader.Type == "*" && acceptHeader.SubType == "*" {
return acceptHeader.String(), parsedType.String(), true
}
if acceptHeader.Type == parsedType.Type {
if !exact && acceptHeader.SubType == "*" {
return acceptHeader.String(), parsedType.String(), true
}
if acceptHeader.SubType == parsedType.SubType {
return acceptHeader.String(), parsedType.String(), true
}
}
}
}
return "", "", false
}
func (a AcceptHeaders) MatchJRD(exact bool) bool {
_, _, m := a.Match(exact, "application/json")
return m
}
func (a AcceptHeaders) MatchHTML(exact bool) bool {
_, _, m := a.Match(exact, "text/html")
return m
}