Merge pull request #22 from prometheus/feature/accept-header-discrimination
WIP — Protocol Buffer negotiation support in handler.
This commit is contained in:
commit
b6cac8669e
|
@ -20,7 +20,10 @@ preparation:
|
|||
ln -sf "$(PWD)" $(PROMETHEUS_TARGET)
|
||||
|
||||
dependencies:
|
||||
go get code.google.com/p/goprotobuf/proto
|
||||
go get github.com/matttproud/gocheck
|
||||
go get github.com/matttproud/golang_protobuf_extensions/ext
|
||||
go get github.com/prometheus/client_model/go
|
||||
|
||||
test: dependencies preparation
|
||||
$(MAKE) test
|
||||
|
|
|
@ -33,11 +33,10 @@ func ProcessorForRequestHeader(header http.Header) (Processor, error) {
|
|||
}
|
||||
switch mediatype {
|
||||
case "application/vnd.google.protobuf":
|
||||
// BUG(matt): Version?
|
||||
if params["proto"] != "io.prometheus.client.MetricFamily" {
|
||||
return nil, fmt.Errorf("Unrecognized Protocol Message %s", params["proto"])
|
||||
}
|
||||
if params["encoding"] != "varint record length-delimited" {
|
||||
if params["encoding"] != "delimited" {
|
||||
return nil, fmt.Errorf("Unsupported Encoding %s", params["encoding"])
|
||||
}
|
||||
return MetricFamilyProcessor, nil
|
||||
|
|
|
@ -57,12 +57,12 @@ func testDiscriminatorHttpHeader(t test.Tester) {
|
|||
err: nil,
|
||||
},
|
||||
{
|
||||
input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="varint record length-delimited"`},
|
||||
input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="delimited"`},
|
||||
output: MetricFamilyProcessor,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="illegal"; encoding="varint record length-delimited"`},
|
||||
input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="illegal"; encoding="delimited"`},
|
||||
output: nil,
|
||||
err: fmt.Errorf("Unrecognized Protocol Message illegal"),
|
||||
},
|
||||
|
|
|
@ -28,9 +28,11 @@ const (
|
|||
|
||||
// The content type and schema information set on telemetry data responses.
|
||||
TelemetryContentType = `application/json; schema="prometheus/telemetry"; version=` + APIVersion
|
||||
// The content type and schema information set on telemetry data responses.
|
||||
DelimitedTelemetryContentType = `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="delimited"`
|
||||
|
||||
// The customary web services endpoint on which telemetric data is exposed.
|
||||
ExpositionResource = "/metrics.json"
|
||||
ExpositionResource = "/metrics"
|
||||
|
||||
baseLabelsKey = "baseLabels"
|
||||
docstringKey = "docstring"
|
||||
|
|
|
@ -10,6 +10,10 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"code.google.com/p/goprotobuf/proto"
|
||||
)
|
||||
|
||||
// TODO(matt): Refactor to de-duplicate behaviors.
|
||||
|
@ -31,13 +35,13 @@ type counterVector struct {
|
|||
|
||||
func NewCounter() Counter {
|
||||
return &counter{
|
||||
values: map[string]*counterVector{},
|
||||
values: map[uint64]*counterVector{},
|
||||
}
|
||||
}
|
||||
|
||||
type counter struct {
|
||||
mutex sync.RWMutex
|
||||
values map[string]*counterVector
|
||||
values map[uint64]*counterVector
|
||||
}
|
||||
|
||||
func (metric *counter) Set(labels map[string]string, value float64) float64 {
|
||||
|
@ -147,3 +151,31 @@ func (metric *counter) MarshalJSON() ([]byte, error) {
|
|||
typeKey: counterTypeValue,
|
||||
})
|
||||
}
|
||||
|
||||
func (metric *counter) dumpChildren(f *dto.MetricFamily) {
|
||||
metric.mutex.RLock()
|
||||
defer metric.mutex.RUnlock()
|
||||
|
||||
f.Type = dto.MetricType_COUNTER.Enum()
|
||||
|
||||
for _, child := range metric.values {
|
||||
c := &dto.Counter{
|
||||
Value: proto.Float64(child.Value),
|
||||
}
|
||||
|
||||
m := &dto.Metric{
|
||||
Counter: c,
|
||||
}
|
||||
|
||||
for name, value := range child.Labels {
|
||||
p := &dto.LabelPair{
|
||||
Name: proto.String(name),
|
||||
Value: proto.String(value),
|
||||
}
|
||||
|
||||
m.Label = append(m.Label, p)
|
||||
}
|
||||
|
||||
f.Metric = append(f.Metric, m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"code.google.com/p/goprotobuf/proto"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
// A gauge metric merely provides an instantaneous representation of a scalar
|
||||
|
@ -28,13 +32,13 @@ type gaugeVector struct {
|
|||
|
||||
func NewGauge() Gauge {
|
||||
return &gauge{
|
||||
values: map[string]*gaugeVector{},
|
||||
values: map[uint64]*gaugeVector{},
|
||||
}
|
||||
}
|
||||
|
||||
type gauge struct {
|
||||
mutex sync.RWMutex
|
||||
values map[string]*gaugeVector
|
||||
values map[uint64]*gaugeVector
|
||||
}
|
||||
|
||||
func (metric *gauge) String() string {
|
||||
|
@ -94,3 +98,31 @@ func (metric *gauge) MarshalJSON() ([]byte, error) {
|
|||
valueKey: values,
|
||||
})
|
||||
}
|
||||
|
||||
func (metric *gauge) dumpChildren(f *dto.MetricFamily) {
|
||||
metric.mutex.RLock()
|
||||
defer metric.mutex.RUnlock()
|
||||
|
||||
f.Type = dto.MetricType_GAUGE.Enum()
|
||||
|
||||
for _, child := range metric.values {
|
||||
c := &dto.Gauge{
|
||||
Value: proto.Float64(child.Value),
|
||||
}
|
||||
|
||||
m := &dto.Metric{
|
||||
Gauge: c,
|
||||
}
|
||||
|
||||
for name, value := range child.Labels {
|
||||
p := &dto.LabelPair{
|
||||
Name: proto.String(name),
|
||||
Value: proto.String(value),
|
||||
}
|
||||
|
||||
m.Label = append(m.Label, p)
|
||||
}
|
||||
|
||||
f.Metric = append(f.Metric, m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,10 @@ import (
|
|||
"math"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"code.google.com/p/goprotobuf/proto"
|
||||
)
|
||||
|
||||
// This generates count-buckets of equal size distributed along the open
|
||||
|
@ -75,7 +79,7 @@ type histogram struct {
|
|||
// These are the buckets that capture samples as they are emitted to the
|
||||
// histogram. Please consult the reference interface and its implements for
|
||||
// further details about behavior expectations.
|
||||
values map[string]*histogramVector
|
||||
values map[uint64]*histogramVector
|
||||
// These are the percentile values that will be reported on marshalling.
|
||||
reportablePercentiles []float64
|
||||
}
|
||||
|
@ -157,7 +161,7 @@ func prospectiveIndexForPercentile(percentile float64, totalObservations int) in
|
|||
}
|
||||
|
||||
// Determine the next bucket element when interim bucket intervals may be empty.
|
||||
func (h histogram) nextNonEmptyBucketElement(signature string, currentIndex, bucketCount int, observationsByBucket []int) (*Bucket, int) {
|
||||
func (h histogram) nextNonEmptyBucketElement(signature uint64, currentIndex, bucketCount int, observationsByBucket []int) (*Bucket, int) {
|
||||
for i := currentIndex; i < bucketCount; i++ {
|
||||
if observationsByBucket[i] == 0 {
|
||||
continue
|
||||
|
@ -176,7 +180,7 @@ func (h histogram) nextNonEmptyBucketElement(signature string, currentIndex, buc
|
|||
// longer contained by the bucket, the index of the last item is returned. This
|
||||
// may occur if the underlying bucket catalogs values and employs an eviction
|
||||
// strategy.
|
||||
func (h histogram) bucketForPercentile(signature string, percentile float64) (*Bucket, int) {
|
||||
func (h histogram) bucketForPercentile(signature uint64, percentile float64) (*Bucket, int) {
|
||||
bucketCount := len(h.bucketStarts)
|
||||
|
||||
// This captures the quantity of samples in a given bucket's range.
|
||||
|
@ -229,7 +233,7 @@ func (h histogram) bucketForPercentile(signature string, percentile float64) (*B
|
|||
// Return the histogram's estimate of the value for a given percentile of
|
||||
// collected samples. The requested percentile is expected to be a real
|
||||
// value within (0, 1.0].
|
||||
func (h histogram) percentile(signature string, percentile float64) float64 {
|
||||
func (h histogram) percentile(signature uint64, percentile float64) float64 {
|
||||
bucket, index := h.bucketForPercentile(signature, percentile)
|
||||
|
||||
return (*bucket).ValueForIndex(index)
|
||||
|
@ -284,7 +288,7 @@ func NewHistogram(specification *HistogramSpecification) Histogram {
|
|||
bucketMaker: specification.BucketBuilder,
|
||||
bucketStarts: specification.Starts,
|
||||
reportablePercentiles: specification.ReportablePercentiles,
|
||||
values: map[string]*histogramVector{},
|
||||
values: map[uint64]*histogramVector{},
|
||||
}
|
||||
|
||||
return metric
|
||||
|
@ -301,3 +305,38 @@ func NewDefaultHistogram() Histogram {
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (metric *histogram) dumpChildren(f *dto.MetricFamily) {
|
||||
metric.mutex.RLock()
|
||||
defer metric.mutex.RUnlock()
|
||||
|
||||
f.Type = dto.MetricType_SUMMARY.Enum()
|
||||
|
||||
for signature, child := range metric.values {
|
||||
c := &dto.Summary{}
|
||||
|
||||
m := &dto.Metric{
|
||||
Summary: c,
|
||||
}
|
||||
|
||||
for name, value := range child.labels {
|
||||
p := &dto.LabelPair{
|
||||
Name: proto.String(name),
|
||||
Value: proto.String(value),
|
||||
}
|
||||
|
||||
m.Label = append(m.Label, p)
|
||||
}
|
||||
|
||||
for _, percentile := range metric.reportablePercentiles {
|
||||
q := &dto.Quantile{
|
||||
Quantile: proto.Float64(percentile),
|
||||
Value: proto.Float64(metric.percentile(signature, percentile)),
|
||||
}
|
||||
|
||||
c.Quantile = append(c.Quantile, q)
|
||||
}
|
||||
|
||||
f.Metric = append(f.Metric, m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
|
||||
package prometheus
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
// A Metric is something that can be exposed via the registry framework.
|
||||
type Metric interface {
|
||||
|
@ -16,4 +20,6 @@ type Metric interface {
|
|||
ResetAll()
|
||||
// Produce a human-consumable representation of the metric.
|
||||
String() string
|
||||
// dumpChildren populates the child metrics of the given family.
|
||||
dumpChildren(*dto.MetricFamily)
|
||||
}
|
||||
|
|
|
@ -19,25 +19,26 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"code.google.com/p/goprotobuf/proto"
|
||||
"github.com/matttproud/golang_protobuf_extensions/ext"
|
||||
|
||||
"github.com/prometheus/client_golang/vendor/goautoneg"
|
||||
)
|
||||
|
||||
const (
|
||||
acceptEncodingHeader = "Accept-Encoding"
|
||||
authorization = "Authorization"
|
||||
authorizationHeader = "WWW-Authenticate"
|
||||
authorizationHeaderValue = "Basic"
|
||||
|
||||
acceptEncodingHeader = "Accept-Encoding"
|
||||
contentEncodingHeader = "Content-Encoding"
|
||||
contentTypeHeader = "Content-Type"
|
||||
gzipAcceptEncodingValue = "gzip"
|
||||
gzipContentEncodingValue = "gzip"
|
||||
jsonContentType = "application/json"
|
||||
jsonSuffix = ".json"
|
||||
)
|
||||
|
||||
var (
|
||||
abortOnMisuse bool
|
||||
debugRegistration bool
|
||||
useAggressiveSanityChecks bool
|
||||
)
|
||||
|
||||
// container represents a top-level registered metric that encompasses its
|
||||
|
@ -49,9 +50,23 @@ type container struct {
|
|||
name string
|
||||
}
|
||||
|
||||
type containers []*container
|
||||
|
||||
func (c containers) Len() int {
|
||||
return len(c)
|
||||
}
|
||||
|
||||
func (c containers) Swap(i, j int) {
|
||||
c[i], c[j] = c[j], c[i]
|
||||
}
|
||||
|
||||
func (c containers) Less(i, j int) bool {
|
||||
return c[i].name < c[j].name
|
||||
}
|
||||
|
||||
type registry struct {
|
||||
mutex sync.RWMutex
|
||||
signatureContainers map[string]container
|
||||
signatureContainers map[uint64]*container
|
||||
}
|
||||
|
||||
// Registry is a registrar where metrics are listed.
|
||||
|
@ -72,8 +87,8 @@ type Registry interface {
|
|||
// 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),
|
||||
return ®istry{
|
||||
signatureContainers: make(map[uint64]*container),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,33 +98,28 @@ func Register(name, docstring string, baseLabels map[string]string, metric Metri
|
|||
}
|
||||
|
||||
// Implements json.Marshaler
|
||||
func (r registry) MarshalJSON() (_ []byte, err error) {
|
||||
metrics := make([]interface{}, 0, len(r.signatureContainers))
|
||||
func (r *registry) MarshalJSON() ([]byte, error) {
|
||||
containers := make(containers, 0, len(r.signatureContainers))
|
||||
|
||||
keys := make([]string, 0, len(metrics))
|
||||
for key := range r.signatureContainers {
|
||||
keys = append(keys, key)
|
||||
for _, container := range r.signatureContainers {
|
||||
containers = append(containers, container)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
sort.Sort(containers)
|
||||
|
||||
for _, key := range keys {
|
||||
metrics = append(metrics, r.signatureContainers[key])
|
||||
}
|
||||
|
||||
return json.Marshal(metrics)
|
||||
return json.Marshal(containers)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func (r *registry) isValidCandidate(name string, baseLabels map[string]string) (signature uint64, err error) {
|
||||
if len(name) == 0 {
|
||||
err = fmt.Errorf("unnamed metric named with baseLabels %s is invalid", baseLabels)
|
||||
|
||||
if abortOnMisuse {
|
||||
if *abortOnMisuse {
|
||||
panic(err)
|
||||
} else if debugRegistration {
|
||||
} else if *debugRegistration {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
@ -117,13 +127,13 @@ func (r registry) isValidCandidate(name string, baseLabels map[string]string) (s
|
|||
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 {
|
||||
if *abortOnMisuse {
|
||||
panic(err)
|
||||
} else if debugRegistration {
|
||||
} else if *debugRegistration {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
return
|
||||
return signature, err
|
||||
}
|
||||
|
||||
baseLabels[nameLabel] = name
|
||||
|
@ -131,34 +141,34 @@ func (r registry) isValidCandidate(name string, baseLabels map[string]string) (s
|
|||
|
||||
if _, contains := r.signatureContainers[signature]; contains {
|
||||
err = fmt.Errorf("metric named %s with baseLabels %s is already registered", name, baseLabels)
|
||||
if abortOnMisuse {
|
||||
if *abortOnMisuse {
|
||||
panic(err)
|
||||
} else if debugRegistration {
|
||||
} else if *debugRegistration {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
return
|
||||
return signature, err
|
||||
}
|
||||
|
||||
if useAggressiveSanityChecks {
|
||||
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 {
|
||||
if *abortOnMisuse {
|
||||
panic(err)
|
||||
} else if debugRegistration {
|
||||
} else if *debugRegistration {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
return
|
||||
return signature, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return signature, err
|
||||
}
|
||||
|
||||
func (r registry) Register(name, docstring string, baseLabels map[string]string, metric Metric) (err error) {
|
||||
func (r *registry) Register(name, docstring string, baseLabels map[string]string, metric Metric) error {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
|
@ -168,25 +178,26 @@ func (r registry) Register(name, docstring string, baseLabels map[string]string,
|
|||
|
||||
signature, err := r.isValidCandidate(name, baseLabels)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
r.signatureContainers[signature] = container{
|
||||
r.signatureContainers[signature] = &container{
|
||||
BaseLabels: baseLabels,
|
||||
Docstring: docstring,
|
||||
Metric: metric,
|
||||
name: name,
|
||||
}
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// YieldBasicAuthExporter creates a http.HandlerFunc that is protected by HTTP's
|
||||
// basic authentication.
|
||||
func (register registry) YieldBasicAuthExporter(username, password string) http.HandlerFunc {
|
||||
func (register *registry) YieldBasicAuthExporter(username, password string) http.HandlerFunc {
|
||||
// XXX: Work with Daniel to get this removed from the library, as it is really
|
||||
// superfluous and can be much more elegantly accomplished via
|
||||
// delegation.
|
||||
log.Println("Registry.YieldBasicAuthExporter is deprecated.")
|
||||
exporter := register.YieldExporter()
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -225,22 +236,46 @@ func decorateWriter(request *http.Request, writer http.ResponseWriter) io.Writer
|
|||
return gziper
|
||||
}
|
||||
|
||||
func (registry registry) YieldExporter() http.HandlerFunc {
|
||||
func (registry *registry) YieldExporter() http.HandlerFunc {
|
||||
log.Println("Registry.YieldExporter is deprecated in favor of Registry.Handler.")
|
||||
|
||||
return registry.Handler()
|
||||
}
|
||||
|
||||
func (registry registry) Handler() http.HandlerFunc {
|
||||
func (r *registry) dumpDelimitedPB(w io.Writer) {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
f := new(dto.MetricFamily)
|
||||
for _, container := range r.signatureContainers {
|
||||
f.Reset()
|
||||
|
||||
f.Name = proto.String(container.name)
|
||||
f.Help = proto.String(container.Docstring)
|
||||
|
||||
container.Metric.dumpChildren(f)
|
||||
|
||||
for name, value := range container.BaseLabels {
|
||||
p := &dto.LabelPair{
|
||||
Name: proto.String(name),
|
||||
Value: proto.String(value),
|
||||
}
|
||||
|
||||
for _, child := range f.Metric {
|
||||
child.Label = append(child.Label, p)
|
||||
}
|
||||
}
|
||||
|
||||
ext.WriteDelimited(w, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (registry *registry) Handler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
defer requestLatencyAccumulator(time.Now())
|
||||
|
||||
requestCount.Increment(nil)
|
||||
url := r.URL
|
||||
|
||||
if strings.HasSuffix(url.Path, jsonSuffix) {
|
||||
header := w.Header()
|
||||
header.Set(contentTypeHeader, TelemetryContentType)
|
||||
|
||||
writer := decorateWriter(r, w)
|
||||
|
||||
|
@ -248,15 +283,34 @@ func (registry registry) Handler() http.HandlerFunc {
|
|||
defer closer.Close()
|
||||
}
|
||||
|
||||
json.NewEncoder(writer).Encode(registry)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
accepts := goautoneg.ParseAccept(r.Header.Get("Accept"))
|
||||
for _, accept := range accepts {
|
||||
if accept.Type != "application" {
|
||||
continue
|
||||
}
|
||||
|
||||
if accept.SubType == "vnd.google.protobuf" {
|
||||
if accept.Params["proto"] != "io.prometheus.client.MetricFamily" {
|
||||
continue
|
||||
}
|
||||
if accept.Params["encoding"] != "delimited" {
|
||||
continue
|
||||
}
|
||||
|
||||
header.Set(contentTypeHeader, DelimitedTelemetryContentType)
|
||||
registry.dumpDelimitedPB(writer)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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).")
|
||||
header.Set(contentTypeHeader, TelemetryContentType)
|
||||
json.NewEncoder(writer).Encode(registry)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
abortOnMisuse = flag.Bool(FlagNamespace+"abortonmisuse", false, "abort if a semantic misuse is encountered (bool).")
|
||||
debugRegistration = flag.Bool(FlagNamespace+"debugregistration", false, "display information about the metric registration process (bool).")
|
||||
useAggressiveSanityChecks = flag.Bool(FlagNamespace+"useaggressivesanitychecks", false, "perform expensive validation of metrics (bool).")
|
||||
)
|
||||
|
|
|
@ -13,6 +13,8 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.google.com/p/goprotobuf/proto"
|
||||
)
|
||||
|
||||
func testRegister(t tester) {
|
||||
|
@ -21,14 +23,14 @@ func testRegister(t tester) {
|
|||
debugRegistration bool
|
||||
useAggressiveSanityChecks bool
|
||||
}{
|
||||
abortOnMisuse: abortOnMisuse,
|
||||
debugRegistration: debugRegistration,
|
||||
useAggressiveSanityChecks: useAggressiveSanityChecks,
|
||||
abortOnMisuse: *abortOnMisuse,
|
||||
debugRegistration: *debugRegistration,
|
||||
useAggressiveSanityChecks: *useAggressiveSanityChecks,
|
||||
}
|
||||
defer func() {
|
||||
abortOnMisuse = oldState.abortOnMisuse
|
||||
debugRegistration = oldState.debugRegistration
|
||||
useAggressiveSanityChecks = oldState.useAggressiveSanityChecks
|
||||
abortOnMisuse = &(oldState.abortOnMisuse)
|
||||
debugRegistration = &(oldState.debugRegistration)
|
||||
useAggressiveSanityChecks = &(oldState.useAggressiveSanityChecks)
|
||||
}()
|
||||
|
||||
type input struct {
|
||||
|
@ -139,9 +141,9 @@ func testRegister(t tester) {
|
|||
t.Fatalf("%d. expected scenario output length %d, got %d", i, len(scenario.inputs), len(scenario.outputs))
|
||||
}
|
||||
|
||||
abortOnMisuse = false
|
||||
debugRegistration = false
|
||||
useAggressiveSanityChecks = true
|
||||
abortOnMisuse = proto.Bool(false)
|
||||
debugRegistration = proto.Bool(false)
|
||||
useAggressiveSanityChecks = proto.Bool(true)
|
||||
|
||||
registry := NewRegistry()
|
||||
|
||||
|
@ -297,7 +299,7 @@ func testDumpToWriter(t tester) {
|
|||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
registry := NewRegistry().(registry)
|
||||
registry := NewRegistry().(*registry)
|
||||
|
||||
for name, metric := range scenario.in.metrics {
|
||||
err := registry.Register(name, fmt.Sprintf("metric %s", name), map[string]string{fmt.Sprintf("label_%s", name): name}, metric)
|
||||
|
|
|
@ -7,35 +7,28 @@
|
|||
package prometheus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
delimiter = "|"
|
||||
"github.com/prometheus/client_golang/model"
|
||||
)
|
||||
|
||||
// LabelsToSignature provides a way of building a unique signature
|
||||
// (i.e., fingerprint) for a given label set sequence.
|
||||
func labelsToSignature(labels map[string]string) string {
|
||||
// TODO(matt): This is a wart, and we'll want to validate that collisions
|
||||
// do not occur in less-than-diligent environments.
|
||||
cardinality := len(labels)
|
||||
keys := make([]string, 0, cardinality)
|
||||
|
||||
for label := range labels {
|
||||
keys = append(keys, label)
|
||||
func labelsToSignature(labels map[string]string) uint64 {
|
||||
names := make(model.LabelNames, 0, len(labels))
|
||||
for name := range labels {
|
||||
names = append(names, model.LabelName(name))
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
sort.Sort(names)
|
||||
|
||||
buffer := bytes.Buffer{}
|
||||
hasher := fnv.New64a()
|
||||
|
||||
for _, label := range keys {
|
||||
buffer.WriteString(label)
|
||||
buffer.WriteString(delimiter)
|
||||
buffer.WriteString(labels[label])
|
||||
for _, name := range names {
|
||||
fmt.Fprintf(hasher, string(name), labels[string(name)])
|
||||
}
|
||||
|
||||
return buffer.String()
|
||||
return hasher.Sum64()
|
||||
}
|
||||
|
|
|
@ -13,13 +13,16 @@ import (
|
|||
func testLabelsToSignature(t tester) {
|
||||
var scenarios = []struct {
|
||||
in map[string]string
|
||||
out string
|
||||
out uint64
|
||||
}{
|
||||
{
|
||||
in: map[string]string{},
|
||||
out: "",
|
||||
out: 14695981039346656037,
|
||||
},
|
||||
{
|
||||
in: map[string]string{"name": "garland, briggs", "fear": "love is not enough"},
|
||||
out: 15753083015552662396,
|
||||
},
|
||||
{},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Imported at 75cd24fc2f2c from https://bitbucket.org/ww/goautoneg.
|
|
@ -0,0 +1,13 @@
|
|||
include $(GOROOT)/src/Make.inc
|
||||
|
||||
TARG=bitbucket.org/ww/goautoneg
|
||||
GOFILES=autoneg.go
|
||||
|
||||
include $(GOROOT)/src/Make.pkg
|
||||
|
||||
format:
|
||||
gofmt -w *.go
|
||||
|
||||
docs:
|
||||
gomake clean
|
||||
godoc ${TARG} > README.txt
|
|
@ -0,0 +1,67 @@
|
|||
PACKAGE
|
||||
|
||||
package goautoneg
|
||||
import "bitbucket.org/ww/goautoneg"
|
||||
|
||||
HTTP Content-Type Autonegotiation.
|
||||
|
||||
The functions in this package implement the behaviour specified in
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||
|
||||
Copyright (c) 2011, Open Knowledge Foundation Ltd.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
Neither the name of the Open Knowledge Foundation Ltd. nor the
|
||||
names of its contributors may be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
FUNCTIONS
|
||||
|
||||
func Negotiate(header string, alternatives []string) (content_type string)
|
||||
Negotiate the most appropriate content_type given the accept header
|
||||
and a list of alternatives.
|
||||
|
||||
func ParseAccept(header string) (accept []Accept)
|
||||
Parse an Accept Header string returning a sorted list
|
||||
of clauses
|
||||
|
||||
|
||||
TYPES
|
||||
|
||||
type Accept struct {
|
||||
Type, SubType string
|
||||
Q float32
|
||||
Params map[string]string
|
||||
}
|
||||
Structure to represent a clause in an HTTP Accept Header
|
||||
|
||||
|
||||
SUBDIRECTORIES
|
||||
|
||||
.hg
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
HTTP Content-Type Autonegotiation.
|
||||
|
||||
The functions in this package implement the behaviour specified in
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||
|
||||
Copyright (c) 2011, Open Knowledge Foundation Ltd.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
Neither the name of the Open Knowledge Foundation Ltd. nor the
|
||||
names of its contributors may be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
*/
|
||||
package goautoneg
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Structure to represent a clause in an HTTP Accept Header
|
||||
type Accept struct {
|
||||
Type, SubType string
|
||||
Q float64
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
// For internal use, so that we can use the sort interface
|
||||
type accept_slice []Accept
|
||||
|
||||
func (accept accept_slice) Len() int {
|
||||
slice := []Accept(accept)
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
func (accept accept_slice) Less(i, j int) bool {
|
||||
slice := []Accept(accept)
|
||||
ai, aj := slice[i], slice[j]
|
||||
if ai.Q > aj.Q {
|
||||
return true
|
||||
}
|
||||
if ai.Type != "*" && aj.Type == "*" {
|
||||
return true
|
||||
}
|
||||
if ai.SubType != "*" && aj.SubType == "*" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (accept accept_slice) Swap(i, j int) {
|
||||
slice := []Accept(accept)
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
|
||||
// Parse an Accept Header string returning a sorted list
|
||||
// of clauses
|
||||
func ParseAccept(header string) (accept []Accept) {
|
||||
parts := strings.Split(header, ",")
|
||||
accept = make([]Accept, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part := strings.Trim(part, " ")
|
||||
|
||||
a := Accept{}
|
||||
a.Params = make(map[string]string)
|
||||
a.Q = 1.0
|
||||
|
||||
mrp := strings.Split(part, ";")
|
||||
|
||||
media_range := mrp[0]
|
||||
sp := strings.Split(media_range, "/")
|
||||
a.Type = strings.Trim(sp[0], " ")
|
||||
|
||||
switch {
|
||||
case len(sp) == 1 && a.Type == "*":
|
||||
a.SubType = "*"
|
||||
case len(sp) == 2:
|
||||
a.SubType = strings.Trim(sp[1], " ")
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if len(mrp) == 1 {
|
||||
accept = append(accept, a)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, param := range mrp[1:] {
|
||||
sp := strings.SplitN(param, "=", 2)
|
||||
if len(sp) != 2 {
|
||||
continue
|
||||
}
|
||||
token := strings.Trim(sp[0], " ")
|
||||
if token == "q" {
|
||||
a.Q, _ = strconv.ParseFloat(sp[1], 32)
|
||||
} else {
|
||||
a.Params[token] = strings.Trim(sp[1], " ")
|
||||
}
|
||||
}
|
||||
|
||||
accept = append(accept, a)
|
||||
}
|
||||
|
||||
slice := accept_slice(accept)
|
||||
sort.Sort(slice)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Negotiate the most appropriate content_type given the accept header
|
||||
// and a list of alternatives.
|
||||
func Negotiate(header string, alternatives []string) (content_type string) {
|
||||
asp := make([][]string, 0, len(alternatives))
|
||||
for _, ctype := range alternatives {
|
||||
asp = append(asp, strings.SplitN(ctype, "/", 2))
|
||||
}
|
||||
for _, clause := range ParseAccept(header) {
|
||||
for i, ctsp := range asp {
|
||||
if clause.Type == ctsp[0] && clause.SubType == ctsp[1] {
|
||||
content_type = alternatives[i]
|
||||
return
|
||||
}
|
||||
if clause.Type == ctsp[0] && clause.SubType == "*" {
|
||||
content_type = alternatives[i]
|
||||
return
|
||||
}
|
||||
if clause.Type == "*" && clause.SubType == "*" {
|
||||
content_type = alternatives[i]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package goautoneg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var chrome = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
|
||||
|
||||
func TestParseAccept(t *testing.T) {
|
||||
alternatives := []string{"text/html", "image/png"}
|
||||
content_type := Negotiate(chrome, alternatives)
|
||||
if content_type != "image/png" {
|
||||
t.Errorf("got %s expected image/png", content_type)
|
||||
}
|
||||
|
||||
alternatives = []string{"text/html", "text/plain", "text/n3"}
|
||||
content_type = Negotiate(chrome, alternatives)
|
||||
if content_type != "text/html" {
|
||||
t.Errorf("got %s expected text/html", content_type)
|
||||
}
|
||||
|
||||
alternatives = []string{"text/n3", "text/plain"}
|
||||
content_type = Negotiate(chrome, alternatives)
|
||||
if content_type != "text/plain" {
|
||||
t.Errorf("got %s expected text/plain", content_type)
|
||||
}
|
||||
|
||||
alternatives = []string{"text/n3", "application/rdf+xml"}
|
||||
content_type = Negotiate(chrome, alternatives)
|
||||
if content_type != "text/n3" {
|
||||
t.Errorf("got %s expected text/n3", content_type)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue