318 lines
7.9 KiB
Go
318 lines
7.9 KiB
Go
/*
|
|
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.
|
|
*/
|
|
|
|
package registry
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/prometheus/client_golang/metrics"
|
|
"github.com/prometheus/client_golang/utility"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
acceptEncodingHeader = "Accept-Encoding"
|
|
authorization = "Authorization"
|
|
authorizationHeader = "WWW-Authenticate"
|
|
authorizationHeaderValue = "Basic"
|
|
contentEncodingHeader = "Content-Encoding"
|
|
contentTypeHeader = "Content-Type"
|
|
gzipAcceptEncodingValue = "gzip"
|
|
gzipContentEncodingValue = "gzip"
|
|
jsonContentType = "application/json"
|
|
jsonSuffix = ".json"
|
|
)
|
|
|
|
var (
|
|
abortOnMisuse bool
|
|
debugRegistration bool
|
|
useAggressiveSanityChecks bool
|
|
)
|
|
|
|
/*
|
|
This callback accumulates the microsecond duration of the reporting framework's
|
|
overhead such that it can be reported.
|
|
*/
|
|
var requestLatencyAccumulator metrics.CompletionCallback = func(duration time.Duration) {
|
|
microseconds := float64(duration / time.Microsecond)
|
|
|
|
requestLatency.Add(nil, microseconds)
|
|
}
|
|
|
|
// container represents a top-level registered metric that encompasses its
|
|
// static metadata.
|
|
type container struct {
|
|
baseLabels map[string]string
|
|
docstring string
|
|
metric metrics.Metric
|
|
name string
|
|
}
|
|
|
|
/*
|
|
Registry is, as the name implies, a registrar where metrics are listed.
|
|
|
|
In most situations, using DefaultRegistry is sufficient versus creating one's
|
|
own.
|
|
*/
|
|
type Registry struct {
|
|
mutex sync.RWMutex
|
|
signatureContainers map[string]container
|
|
}
|
|
|
|
/*
|
|
This builds a new metric registry. It is not needed in the majority of
|
|
cases.
|
|
*/
|
|
func NewRegistry() *Registry {
|
|
return &Registry{
|
|
signatureContainers: make(map[string]container),
|
|
}
|
|
}
|
|
|
|
/*
|
|
This is the default registry with which Metric objects are associated. It
|
|
is primarily a read-only object after server instantiation.
|
|
*/
|
|
var DefaultRegistry = NewRegistry()
|
|
|
|
/*
|
|
Associate a Metric with the DefaultRegistry.
|
|
*/
|
|
func Register(name, docstring string, baseLabels map[string]string, metric metrics.Metric) error {
|
|
return DefaultRegistry.Register(name, docstring, baseLabels, metric)
|
|
}
|
|
|
|
// isValidCandidate returns true if the candidate is acceptable for use. In the
|
|
// event of any apparent incorrect use it will report the problem, invalidate
|
|
// the candidate, or outright abort.
|
|
func (r *Registry) isValidCandidate(name string, baseLabels map[string]string) (signature string, err error) {
|
|
if len(name) == 0 {
|
|
err = fmt.Errorf("unnamed metric named with baseLabels %s is invalid", baseLabels)
|
|
|
|
if abortOnMisuse {
|
|
panic(err)
|
|
} else if debugRegistration {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
|
|
if _, contains := baseLabels[nameLabel]; contains {
|
|
err = fmt.Errorf("metric named %s with baseLabels %s contains reserved label name %s in baseLabels", name, baseLabels, nameLabel)
|
|
|
|
if abortOnMisuse {
|
|
panic(err)
|
|
} else if debugRegistration {
|
|
log.Println(err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
baseLabels[nameLabel] = name
|
|
signature = utility.LabelsToSignature(baseLabels)
|
|
|
|
if _, contains := r.signatureContainers[signature]; contains {
|
|
err = fmt.Errorf("metric named %s with baseLabels %s is already registered", name, baseLabels)
|
|
if abortOnMisuse {
|
|
panic(err)
|
|
} else if debugRegistration {
|
|
log.Println(err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if useAggressiveSanityChecks {
|
|
for _, container := range r.signatureContainers {
|
|
if container.name == name {
|
|
err = fmt.Errorf("metric named %s with baseLabels %s is already registered as %s and risks causing confusion", name, baseLabels, container.baseLabels)
|
|
if abortOnMisuse {
|
|
panic(err)
|
|
} else if debugRegistration {
|
|
log.Println(err)
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
Register a metric with a given name. Name should be globally unique.
|
|
*/
|
|
func (r *Registry) Register(name, docstring string, baseLabels map[string]string, metric metrics.Metric) (err error) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
if baseLabels == nil {
|
|
baseLabels = map[string]string{}
|
|
}
|
|
|
|
signature, err := r.isValidCandidate(name, baseLabels)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
r.signatureContainers[signature] = container{
|
|
baseLabels: baseLabels,
|
|
docstring: docstring,
|
|
metric: metric,
|
|
name: name,
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// YieldBasicAuthExporter creates a http.HandlerFunc that is protected by HTTP's
|
|
// basic authentication.
|
|
func (register *Registry) YieldBasicAuthExporter(username, password string) http.HandlerFunc {
|
|
exporter := register.YieldExporter()
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
authenticated := false
|
|
|
|
if auth := r.Header.Get(authorization); auth != "" {
|
|
base64Encoded := strings.SplitAfter(auth, " ")[1]
|
|
decoded, err := base64.URLEncoding.DecodeString(base64Encoded)
|
|
if err == nil {
|
|
usernamePassword := strings.Split(string(decoded), ":")
|
|
if usernamePassword[0] == username && usernamePassword[1] == password {
|
|
authenticated = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if authenticated {
|
|
exporter.ServeHTTP(w, r)
|
|
} else {
|
|
w.Header().Add(authorizationHeader, authorizationHeaderValue)
|
|
http.Error(w, "access forbidden", 401)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (registry *Registry) dumpToWriter(writer io.Writer) (err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
dumpErrorCount.Increment(nil)
|
|
}
|
|
}()
|
|
|
|
numberOfMetrics := len(registry.signatureContainers)
|
|
keys := make([]string, 0, numberOfMetrics)
|
|
for key := range registry.signatureContainers {
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
_, err = writer.Write([]byte("["))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
index := 0
|
|
|
|
for _, key := range keys {
|
|
container := registry.signatureContainers[key]
|
|
intermediate := map[string]interface{}{
|
|
baseLabelsKey: container.baseLabels,
|
|
docstringKey: container.docstring,
|
|
metricKey: container.metric.AsMarshallable(),
|
|
}
|
|
marshaled, err := json.Marshal(intermediate)
|
|
if err != nil {
|
|
marshalErrorCount.Increment(nil)
|
|
index++
|
|
continue
|
|
}
|
|
|
|
if index > 0 && index < numberOfMetrics {
|
|
_, err = writer.Write([]byte(","))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err = writer.Write(marshaled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
index++
|
|
}
|
|
|
|
_, err = writer.Write([]byte("]"))
|
|
|
|
return
|
|
}
|
|
|
|
// decorateWriter annotates the response writer to handle any other behaviors
|
|
// that might be beneficial to the client---e.g., GZIP encoding.
|
|
func decorateWriter(request *http.Request, writer http.ResponseWriter) io.Writer {
|
|
if !strings.Contains(request.Header.Get(acceptEncodingHeader), gzipAcceptEncodingValue) {
|
|
return writer
|
|
}
|
|
|
|
writer.Header().Set(contentEncodingHeader, gzipContentEncodingValue)
|
|
gziper := gzip.NewWriter(writer)
|
|
|
|
return gziper
|
|
}
|
|
|
|
/*
|
|
Create a http.HandlerFunc that is tied to r Registry such that requests
|
|
against it generate a representation of the housed metrics.
|
|
*/
|
|
func (registry *Registry) YieldExporter() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var instrumentable metrics.InstrumentableCall = func() {
|
|
requestCount.Increment(nil)
|
|
url := r.URL
|
|
|
|
if strings.HasSuffix(url.Path, jsonSuffix) {
|
|
header := w.Header()
|
|
header.Set(ProtocolVersionHeader, APIVersion)
|
|
header.Set(contentTypeHeader, jsonContentType)
|
|
|
|
writer := decorateWriter(r, w)
|
|
|
|
// TODO(matt): Migrate to ioutil.NopCloser.
|
|
if closer, ok := writer.(io.Closer); ok {
|
|
defer closer.Close()
|
|
}
|
|
|
|
registry.dumpToWriter(writer)
|
|
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
|
|
}
|
|
metrics.InstrumentCall(instrumentable, requestLatencyAccumulator)
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
flag.BoolVar(&abortOnMisuse, FlagNamespace+"abortonmisuse", false, "abort if a semantic misuse is encountered (bool).")
|
|
flag.BoolVar(&debugRegistration, FlagNamespace+"debugregistration", false, "display information about the metric registration process (bool).")
|
|
flag.BoolVar(&useAggressiveSanityChecks, FlagNamespace+"useaggressivesanitychecks", false, "perform expensive validation of metrics (bool).")
|
|
}
|