Merge pull request #6 from prometheus/feature/user-love/http-mux-wrapper
Add HTTP multiplexor wrapper for automatic telemetry.
This commit is contained in:
commit
a087e013a5
|
@ -20,3 +20,6 @@ _cgo_export.*
|
|||
_testmain.go
|
||||
|
||||
*.exe
|
||||
|
||||
*~
|
||||
*#
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# Copyright 2013 Prometheus Team
|
||||
# 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.
|
||||
|
||||
MAKE_ARTIFACTS = search_index
|
||||
|
||||
all: test
|
||||
|
||||
build:
|
||||
go build ./...
|
||||
|
||||
test: build
|
||||
go test ./... $(GO_TEST_FLAGS)
|
||||
|
||||
format:
|
||||
find . -iname '*.go' -exec gofmt -w -s=true '{}' ';'
|
||||
|
||||
advice:
|
||||
go tool vet .
|
||||
|
||||
search_index:
|
||||
godoc -index -write_index -index_files='search_index'
|
||||
|
||||
documentation: search_index
|
||||
godoc -http=:6060 -index -index_files='search_index'
|
||||
|
||||
clean:
|
||||
rm -f $(MAKE_ARTIFACTS)
|
||||
|
||||
.PHONY: advice build clean documentation format test
|
|
@ -1,9 +0,0 @@
|
|||
// Copyright (c) 2013, Matt T. Proud
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
// A repository of various contributed Prometheus client components that may
|
||||
// assist in your use of the library.
|
||||
package contributor
|
|
@ -1,36 +0,0 @@
|
|||
// Copyright (c) 2013, Matt T. Proud
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package contributor
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
unknownStatusCode = "unknown"
|
||||
)
|
||||
|
||||
// ResponseWriterDelegator is a means of wrapping http.ResponseWriter to divine
|
||||
// the response code from a given answer, especially in systems where the
|
||||
// response is treated as a blackbox.
|
||||
type ResponseWriterDelegator struct {
|
||||
http.ResponseWriter
|
||||
Status *string
|
||||
}
|
||||
|
||||
func (r ResponseWriterDelegator) WriteHeader(code int) {
|
||||
*r.Status = strconv.Itoa(code)
|
||||
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func NewResponseWriterDelegator(delegate http.ResponseWriter) ResponseWriterDelegator {
|
||||
defaultStatusCode := unknownStatusCode
|
||||
|
||||
return ResponseWriterDelegator{delegate, &defaultStatusCode}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2012, Matt T. Proud
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This skeletal example of the telemetry library is provided to demonstrate the
|
||||
// use of boilerplate HTTP delegation telemetry methods.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/prometheus/client_golang"
|
||||
"github.com/prometheus/client_golang/exp"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// helloHandler demonstrates the DefaultCoarseMux's ability to sniff a
|
||||
// http.ResponseWriter (specifically http.response) implicit setting of
|
||||
// a response code.
|
||||
func helloHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Hello, hello, hello..."))
|
||||
}
|
||||
|
||||
// goodbyeHandler demonstrates the DefaultCoarseMux's ability to sniff an
|
||||
// http.ResponseWriter (specifically http.response) explicit setting of
|
||||
// a response code.
|
||||
func goodbyeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusGone)
|
||||
w.Write([]byte("... and now for the big goodbye!"))
|
||||
}
|
||||
|
||||
// teapotHandler demonstrates the DefaultCoarseMux's ability to sniff an
|
||||
// http.ResponseWriter (specifically http.response) explicit setting of
|
||||
// a response code for pure comedic value.
|
||||
func teapotHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
w.Write([]byte("Short and stout..."))
|
||||
}
|
||||
|
||||
var (
|
||||
listeningAddress = flag.String("listeningAddress", ":8080", "The address to listen to requests on.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
exp.HandleFunc("/hello", helloHandler)
|
||||
exp.HandleFunc("/goodbye", goodbyeHandler)
|
||||
exp.HandleFunc("/teapot", teapotHandler)
|
||||
exp.Handle(registry.ExpositionResource, registry.DefaultHandler)
|
||||
|
||||
http.ListenAndServe(*listeningAddress, exp.DefaultCoarseMux)
|
||||
}
|
|
@ -22,15 +22,13 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
listeningAddress string
|
||||
|
||||
barDomain float64
|
||||
barMean float64
|
||||
fooDomain float64
|
||||
barDomain = flag.Float64("random.fooDomain", 200, "The domain for the random parameter foo.")
|
||||
barMean = flag.Float64("random.barDomain", 10, "The domain for the random parameter bar.")
|
||||
fooDomain = flag.Float64("random.barMean", 100, "The mean for the random parameter bar.")
|
||||
|
||||
// Create a histogram to track fictitious interservice RPC latency for three
|
||||
// distinct services.
|
||||
rpc_latency = metrics.NewHistogram(&metrics.HistogramSpecification{
|
||||
rpcLatency = metrics.NewHistogram(&metrics.HistogramSpecification{
|
||||
// Four distinct histogram buckets for values:
|
||||
// - equally-sized,
|
||||
// - 0 to 50, 50 to 100, 100 to 150, and 150 to 200.
|
||||
|
@ -45,7 +43,7 @@ var (
|
|||
ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.90, 0.99},
|
||||
})
|
||||
|
||||
rpc_calls = metrics.NewCounter()
|
||||
rpcCalls = metrics.NewCounter()
|
||||
|
||||
// If for whatever reason you are resistant to the idea of having a static
|
||||
// registry for metrics, which is a really bad idea when using Prometheus-
|
||||
|
@ -53,36 +51,33 @@ var (
|
|||
customRegistry = registry.NewRegistry()
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&listeningAddress, "listeningAddress", ":8080", "The address to listen to requests on.")
|
||||
flag.Float64Var(&fooDomain, "random.fooDomain", 200, "The domain for the random parameter foo.")
|
||||
flag.Float64Var(&barDomain, "random.barDomain", 10, "The domain for the random parameter bar.")
|
||||
flag.Float64Var(&barMean, "random.barMean", 100, "The mean for the random parameter bar.")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
rpc_latency.Add(map[string]string{"service": "foo"}, rand.Float64()*fooDomain)
|
||||
rpc_calls.Increment(map[string]string{"service": "foo"})
|
||||
rpcLatency.Add(map[string]string{"service": "foo"}, rand.Float64()**fooDomain)
|
||||
rpcCalls.Increment(map[string]string{"service": "foo"})
|
||||
|
||||
rpc_latency.Add(map[string]string{"service": "bar"}, (rand.NormFloat64()*barDomain)+barMean)
|
||||
rpc_calls.Increment(map[string]string{"service": "bar"})
|
||||
rpcLatency.Add(map[string]string{"service": "bar"}, (rand.NormFloat64()**barDomain)+*barMean)
|
||||
rpcCalls.Increment(map[string]string{"service": "bar"})
|
||||
|
||||
rpc_latency.Add(map[string]string{"service": "zed"}, rand.ExpFloat64())
|
||||
rpc_calls.Increment(map[string]string{"service": "zed"})
|
||||
rpcLatency.Add(map[string]string{"service": "zed"}, rand.ExpFloat64())
|
||||
rpcCalls.Increment(map[string]string{"service": "zed"})
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
http.Handle(registry.ExpositionResource, customRegistry.Handler())
|
||||
http.ListenAndServe(listeningAddress, nil)
|
||||
http.ListenAndServe(*listeningAddress, nil)
|
||||
}
|
||||
|
||||
func init() {
|
||||
customRegistry.Register("rpc_latency_microseconds", "RPC latency.", registry.NilLabels, rpc_latency)
|
||||
customRegistry.Register("rpc_calls_total", "RPC calls.", registry.NilLabels, rpc_calls)
|
||||
customRegistry.Register("rpc_latency_microseconds", "RPC latency.", registry.NilLabels, rpcLatency)
|
||||
customRegistry.Register("rpc_calls_total", "RPC calls.", registry.NilLabels, rpcCalls)
|
||||
}
|
||||
|
||||
var (
|
||||
listeningAddress = flag.String("listeningAddress", ":8080", "The address to listen to requests on.")
|
||||
)
|
||||
|
|
|
@ -14,17 +14,13 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
listeningAddress string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&listeningAddress, "listeningAddress", ":8080", "The address to listen to requests on.")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
http.Handle(registry.ExpositionResource, registry.DefaultHandler)
|
||||
http.ListenAndServe(listeningAddress, nil)
|
||||
http.ListenAndServe(*listeningAddress, nil)
|
||||
}
|
||||
|
||||
var (
|
||||
listeningAddress = flag.String("listeningAddress", ":8080", "The address to listen to requests on.")
|
||||
)
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright (c) 2013, Matt T. Proud
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package exp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/prometheus/client_golang"
|
||||
"github.com/prometheus/client_golang/metrics"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
handler = "handler"
|
||||
code = "code"
|
||||
method = "method"
|
||||
)
|
||||
|
||||
type (
|
||||
coarseMux struct {
|
||||
*http.ServeMux
|
||||
}
|
||||
|
||||
handlerDelegator struct {
|
||||
delegate http.Handler
|
||||
pattern string
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
requestCounts = metrics.NewCounter()
|
||||
requestDuration = metrics.NewCounter()
|
||||
requestDurations = metrics.NewDefaultHistogram()
|
||||
requestBytes = metrics.NewCounter()
|
||||
responseBytes = metrics.NewCounter()
|
||||
|
||||
// DefaultCoarseMux is a drop-in replacement for http.DefaultServeMux that
|
||||
// provides standardized telemetry for Go's standard HTTP handler registration
|
||||
// and dispatch API.
|
||||
//
|
||||
// The name is due to the coarse grouping of telemetry by (HTTP Method, HTTP Response Code,
|
||||
// and handler match pattern) triples.
|
||||
DefaultCoarseMux = newCoarseMux()
|
||||
)
|
||||
|
||||
func (h handlerDelegator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rwd := NewResponseWriterDelegator(w)
|
||||
|
||||
defer func() {
|
||||
duration := float64(time.Since(start) / time.Microsecond)
|
||||
status := rwd.Status()
|
||||
labels := map[string]string{handler: h.pattern, code: status, method: strings.ToLower(r.Method)}
|
||||
requestCounts.Increment(labels)
|
||||
requestDuration.IncrementBy(labels, duration)
|
||||
requestDurations.Add(labels, duration)
|
||||
requestBytes.IncrementBy(labels, float64(computeApproximateRequestSize(*r)))
|
||||
responseBytes.IncrementBy(labels, float64(rwd.BytesWritten))
|
||||
}()
|
||||
|
||||
h.delegate.ServeHTTP(rwd, r)
|
||||
}
|
||||
|
||||
func (h handlerDelegator) String() string {
|
||||
return fmt.Sprintf("handlerDelegator wrapping %s for %s", h.delegate, h.pattern)
|
||||
}
|
||||
|
||||
// Handle registers a http.Handler to this CoarseMux. See http.ServeMux.Handle.
|
||||
func (m *coarseMux) handle(pattern string, handler http.Handler) {
|
||||
m.ServeMux.Handle(pattern, handlerDelegator{
|
||||
delegate: handler,
|
||||
pattern: pattern,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle registers a handler to this CoarseMux. See http.ServeMux.HandleFunc.
|
||||
func (m *coarseMux) handleFunc(pattern string, handler http.HandlerFunc) {
|
||||
m.ServeMux.Handle(pattern, handlerDelegator{
|
||||
delegate: handler,
|
||||
pattern: pattern,
|
||||
})
|
||||
}
|
||||
|
||||
func newCoarseMux() *coarseMux {
|
||||
return &coarseMux{
|
||||
ServeMux: http.NewServeMux(),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle registers a http.Handler to DefaultCoarseMux. See http.Handle.
|
||||
func Handle(pattern string, handler http.Handler) {
|
||||
DefaultCoarseMux.handle(pattern, handler)
|
||||
}
|
||||
|
||||
// HandleFunc registers a handler to DefaultCoarseMux. See http.HandleFunc.
|
||||
func HandleFunc(pattern string, handler http.HandlerFunc) {
|
||||
DefaultCoarseMux.handleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Register("http_requests_total", "A counter of the total number of HTTP requests made against the default multiplexor.", registry.NilLabels, requestCounts)
|
||||
registry.Register("http_request_durations_total_microseconds", "The total amount of time the default multiplexor has spent answering HTTP requests (microseconds).", registry.NilLabels, requestDuration)
|
||||
registry.Register("http_request_durations_microseconds", "The amounts of time the default multiplexor has spent answering HTTP requests (microseconds).", registry.NilLabels, requestDurations)
|
||||
registry.Register("http_request_bytes_total", "The total volume of content body sizes received (bytes).", registry.NilLabels, requestBytes)
|
||||
registry.Register("http_response_bytes_total", "The total volume of response payloads emitted (bytes).", registry.NilLabels, responseBytes)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) 2013, Matt T. Proud
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
// A repository of various immature Prometheus client components that may
|
||||
// assist in your use of the library. Items contained herein are regarded as
|
||||
// especially interface unstable and may change without warning. Upon
|
||||
// maturation, they should be migrated into a formal package for users.
|
||||
package exp
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) 2013, Matt T. Proud
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package exp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
unknownStatusCode = "unknown"
|
||||
statusFieldName = "status"
|
||||
)
|
||||
|
||||
type status string
|
||||
|
||||
func (s status) unknown() bool {
|
||||
return len(s) == 0
|
||||
}
|
||||
|
||||
func (s status) String() string {
|
||||
if s.unknown() {
|
||||
return unknownStatusCode
|
||||
}
|
||||
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func computeApproximateRequestSize(r http.Request) (s int) {
|
||||
s += len(r.Method)
|
||||
if r.URL != nil {
|
||||
s += len(r.URL.String())
|
||||
}
|
||||
s += len(r.Proto)
|
||||
for name, values := range r.Header {
|
||||
s += len(name)
|
||||
for _, value := range values {
|
||||
s += len(value)
|
||||
}
|
||||
}
|
||||
|
||||
s += len(r.Host)
|
||||
|
||||
// N.B. r.Form and r.MultipartForm are assumed to be included in r.URL.
|
||||
|
||||
if r.ContentLength != -1 {
|
||||
s += int(r.ContentLength)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ResponseWriterDelegator is a means of wrapping http.ResponseWriter to divine
|
||||
// the response code from a given answer, especially in systems where the
|
||||
// response is treated as a blackbox.
|
||||
type ResponseWriterDelegator struct {
|
||||
http.ResponseWriter
|
||||
status status
|
||||
BytesWritten int
|
||||
}
|
||||
|
||||
func (r ResponseWriterDelegator) String() string {
|
||||
return fmt.Sprintf("ResponseWriterDelegator decorating %s with status %s and %d bytes written.", r.ResponseWriter, r.status, r.BytesWritten)
|
||||
}
|
||||
|
||||
func (r *ResponseWriterDelegator) WriteHeader(code int) {
|
||||
r.status = status(strconv.Itoa(code))
|
||||
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (r *ResponseWriterDelegator) Status() string {
|
||||
if r.status.unknown() {
|
||||
delegate := reflect.ValueOf(r.ResponseWriter).Elem()
|
||||
statusField := delegate.FieldByName(statusFieldName)
|
||||
if statusField.IsValid() {
|
||||
r.status = status(strconv.Itoa(int(statusField.Int())))
|
||||
}
|
||||
}
|
||||
|
||||
return r.status.String()
|
||||
}
|
||||
|
||||
func (r *ResponseWriterDelegator) Write(b []byte) (n int, err error) {
|
||||
n, err = r.ResponseWriter.Write(b)
|
||||
r.BytesWritten += n
|
||||
return
|
||||
}
|
||||
|
||||
func NewResponseWriterDelegator(delegate http.ResponseWriter) *ResponseWriterDelegator {
|
||||
return &ResponseWriterDelegator{
|
||||
ResponseWriter: delegate,
|
||||
}
|
||||
}
|
|
@ -137,7 +137,7 @@ func testRegister(t test.Tester) {
|
|||
|
||||
for i, scenario := range scenarios {
|
||||
if len(scenario.inputs) != len(scenario.outputs) {
|
||||
t.Fatalf("%d. len(scenario.inputs) != len(scenario.outputs)")
|
||||
t.Fatalf("%d. expected scenario output length %d, got %d", i, len(scenario.inputs), len(scenario.outputs))
|
||||
}
|
||||
|
||||
abortOnMisuse = false
|
||||
|
|
Loading…
Reference in New Issue