Add HTTP multiplexor wrapper for automatic telemetry.

This commit is contained in:
Matt T. Proud 2013-03-28 21:03:01 +01:00
parent 6c08aa2e95
commit c5b4952f97
11 changed files with 342 additions and 78 deletions

3
.gitignore vendored
View File

@ -20,3 +20,6 @@ _cgo_export.*
_testmain.go
*.exe
*~
*#

39
Makefile Normal file
View File

@ -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

View File

@ -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

View File

@ -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}
}

View File

@ -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)
}

View File

@ -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.")
)

View File

@ -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.")
)

111
exp/coarsemux.go Normal file
View File

@ -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)
}

11
exp/documentation.go Normal file
View File

@ -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

View File

@ -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,
}
}

View File

@ -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