From c5b4952f9728d39145f3b229840b53aefc391fae Mon Sep 17 00:00:00 2001 From: "Matt T. Proud" Date: Thu, 28 Mar 2013 21:03:01 +0100 Subject: [PATCH] Add HTTP multiplexor wrapper for automatic telemetry. --- .gitignore | 3 + Makefile | 39 +++++++++ contributor/documentation.go | 9 -- contributor/responsewriter_delegator.go | 36 -------- examples/delegator/main.go | 54 ++++++++++++ examples/random/main.go | 41 ++++----- examples/simple/main.go | 14 ++- exp/coarsemux.go | 111 ++++++++++++++++++++++++ exp/documentation.go | 11 +++ exp/responsewriter_delegator.go | 100 +++++++++++++++++++++ registry_test.go | 2 +- 11 files changed, 342 insertions(+), 78 deletions(-) create mode 100644 Makefile delete mode 100644 contributor/documentation.go delete mode 100644 contributor/responsewriter_delegator.go create mode 100644 examples/delegator/main.go create mode 100644 exp/coarsemux.go create mode 100644 exp/documentation.go create mode 100644 exp/responsewriter_delegator.go diff --git a/.gitignore b/.gitignore index 0026861..4482aca 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ _cgo_export.* _testmain.go *.exe + +*~ +*# diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5527b63 --- /dev/null +++ b/Makefile @@ -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 diff --git a/contributor/documentation.go b/contributor/documentation.go deleted file mode 100644 index 7eace5f..0000000 --- a/contributor/documentation.go +++ /dev/null @@ -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 diff --git a/contributor/responsewriter_delegator.go b/contributor/responsewriter_delegator.go deleted file mode 100644 index 79f91bc..0000000 --- a/contributor/responsewriter_delegator.go +++ /dev/null @@ -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} -} diff --git a/examples/delegator/main.go b/examples/delegator/main.go new file mode 100644 index 0000000..87258b1 --- /dev/null +++ b/examples/delegator/main.go @@ -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) +} diff --git a/examples/random/main.go b/examples/random/main.go index a055ae2..f3bc001 100644 --- a/examples/random/main.go +++ b/examples/random/main.go @@ -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.") +) diff --git a/examples/simple/main.go b/examples/simple/main.go index c6c8582..0b4e9df 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -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.") +) diff --git a/exp/coarsemux.go b/exp/coarsemux.go new file mode 100644 index 0000000..74af6d7 --- /dev/null +++ b/exp/coarsemux.go @@ -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) +} diff --git a/exp/documentation.go b/exp/documentation.go new file mode 100644 index 0000000..7d358ca --- /dev/null +++ b/exp/documentation.go @@ -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 diff --git a/exp/responsewriter_delegator.go b/exp/responsewriter_delegator.go new file mode 100644 index 0000000..c92ec46 --- /dev/null +++ b/exp/responsewriter_delegator.go @@ -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, + } +} diff --git a/registry_test.go b/registry_test.go index b607ff3..175c3a0 100644 --- a/registry_test.go +++ b/registry_test.go @@ -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