Merge pull request #22 from prometheus/feature/accept-header-discrimination

WIP — Protocol Buffer negotiation support in handler.
This commit is contained in:
Matt T. Proud 2013-07-01 08:16:10 -07:00
commit b6cac8669e
18 changed files with 546 additions and 105 deletions

View File

@ -20,7 +20,10 @@ preparation:
ln -sf "$(PWD)" $(PROMETHEUS_TARGET) ln -sf "$(PWD)" $(PROMETHEUS_TARGET)
dependencies: dependencies:
go get code.google.com/p/goprotobuf/proto
go get github.com/matttproud/gocheck 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 test: dependencies preparation
$(MAKE) test $(MAKE) test

View File

@ -33,11 +33,10 @@ func ProcessorForRequestHeader(header http.Header) (Processor, error) {
} }
switch mediatype { switch mediatype {
case "application/vnd.google.protobuf": case "application/vnd.google.protobuf":
// BUG(matt): Version?
if params["proto"] != "io.prometheus.client.MetricFamily" { if params["proto"] != "io.prometheus.client.MetricFamily" {
return nil, fmt.Errorf("Unrecognized Protocol Message %s", params["proto"]) 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 nil, fmt.Errorf("Unsupported Encoding %s", params["encoding"])
} }
return MetricFamilyProcessor, nil return MetricFamilyProcessor, nil

View File

@ -57,12 +57,12 @@ func testDiscriminatorHttpHeader(t test.Tester) {
err: nil, 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, output: MetricFamilyProcessor,
err: nil, 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, output: nil,
err: fmt.Errorf("Unrecognized Protocol Message illegal"), err: fmt.Errorf("Unrecognized Protocol Message illegal"),
}, },

0
foo Normal file
View File

View File

@ -28,9 +28,11 @@ const (
// The content type and schema information set on telemetry data responses. // The content type and schema information set on telemetry data responses.
TelemetryContentType = `application/json; schema="prometheus/telemetry"; version=` + APIVersion 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. // The customary web services endpoint on which telemetric data is exposed.
ExpositionResource = "/metrics.json" ExpositionResource = "/metrics"
baseLabelsKey = "baseLabels" baseLabelsKey = "baseLabels"
docstringKey = "docstring" docstringKey = "docstring"

View File

@ -10,6 +10,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"sync" "sync"
dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto"
) )
// TODO(matt): Refactor to de-duplicate behaviors. // TODO(matt): Refactor to de-duplicate behaviors.
@ -31,13 +35,13 @@ type counterVector struct {
func NewCounter() Counter { func NewCounter() Counter {
return &counter{ return &counter{
values: map[string]*counterVector{}, values: map[uint64]*counterVector{},
} }
} }
type counter struct { type counter struct {
mutex sync.RWMutex mutex sync.RWMutex
values map[string]*counterVector values map[uint64]*counterVector
} }
func (metric *counter) Set(labels map[string]string, value float64) float64 { func (metric *counter) Set(labels map[string]string, value float64) float64 {
@ -147,3 +151,31 @@ func (metric *counter) MarshalJSON() ([]byte, error) {
typeKey: counterTypeValue, 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)
}
}

View File

@ -10,6 +10,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"sync" "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 // A gauge metric merely provides an instantaneous representation of a scalar
@ -28,13 +32,13 @@ type gaugeVector struct {
func NewGauge() Gauge { func NewGauge() Gauge {
return &gauge{ return &gauge{
values: map[string]*gaugeVector{}, values: map[uint64]*gaugeVector{},
} }
} }
type gauge struct { type gauge struct {
mutex sync.RWMutex mutex sync.RWMutex
values map[string]*gaugeVector values map[uint64]*gaugeVector
} }
func (metric *gauge) String() string { func (metric *gauge) String() string {
@ -94,3 +98,31 @@ func (metric *gauge) MarshalJSON() ([]byte, error) {
valueKey: values, 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)
}
}

View File

@ -13,6 +13,10 @@ import (
"math" "math"
"strconv" "strconv"
"sync" "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 // 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 // These are the buckets that capture samples as they are emitted to the
// histogram. Please consult the reference interface and its implements for // histogram. Please consult the reference interface and its implements for
// further details about behavior expectations. // further details about behavior expectations.
values map[string]*histogramVector values map[uint64]*histogramVector
// These are the percentile values that will be reported on marshalling. // These are the percentile values that will be reported on marshalling.
reportablePercentiles []float64 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. // 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++ { for i := currentIndex; i < bucketCount; i++ {
if observationsByBucket[i] == 0 { if observationsByBucket[i] == 0 {
continue 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 // 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 // may occur if the underlying bucket catalogs values and employs an eviction
// strategy. // 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) bucketCount := len(h.bucketStarts)
// This captures the quantity of samples in a given bucket's range. // 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 // Return the histogram's estimate of the value for a given percentile of
// collected samples. The requested percentile is expected to be a real // collected samples. The requested percentile is expected to be a real
// value within (0, 1.0]. // 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) bucket, index := h.bucketForPercentile(signature, percentile)
return (*bucket).ValueForIndex(index) return (*bucket).ValueForIndex(index)
@ -284,7 +288,7 @@ func NewHistogram(specification *HistogramSpecification) Histogram {
bucketMaker: specification.BucketBuilder, bucketMaker: specification.BucketBuilder,
bucketStarts: specification.Starts, bucketStarts: specification.Starts,
reportablePercentiles: specification.ReportablePercentiles, reportablePercentiles: specification.ReportablePercentiles,
values: map[string]*histogramVector{}, values: map[uint64]*histogramVector{},
} }
return metric 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)
}
}

View File

@ -6,7 +6,11 @@
package prometheus 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. // A Metric is something that can be exposed via the registry framework.
type Metric interface { type Metric interface {
@ -16,4 +20,6 @@ type Metric interface {
ResetAll() ResetAll()
// Produce a human-consumable representation of the metric. // Produce a human-consumable representation of the metric.
String() string String() string
// dumpChildren populates the child metrics of the given family.
dumpChildren(*dto.MetricFamily)
} }

View File

@ -19,25 +19,26 @@ import (
"strings" "strings"
"sync" "sync"
"time" "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 ( const (
acceptEncodingHeader = "Accept-Encoding"
authorization = "Authorization" authorization = "Authorization"
authorizationHeader = "WWW-Authenticate" authorizationHeader = "WWW-Authenticate"
authorizationHeaderValue = "Basic" authorizationHeaderValue = "Basic"
acceptEncodingHeader = "Accept-Encoding"
contentEncodingHeader = "Content-Encoding" contentEncodingHeader = "Content-Encoding"
contentTypeHeader = "Content-Type" contentTypeHeader = "Content-Type"
gzipAcceptEncodingValue = "gzip" gzipAcceptEncodingValue = "gzip"
gzipContentEncodingValue = "gzip" gzipContentEncodingValue = "gzip"
jsonContentType = "application/json" jsonContentType = "application/json"
jsonSuffix = ".json"
)
var (
abortOnMisuse bool
debugRegistration bool
useAggressiveSanityChecks bool
) )
// container represents a top-level registered metric that encompasses its // container represents a top-level registered metric that encompasses its
@ -49,9 +50,23 @@ type container struct {
name string 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 { type registry struct {
mutex sync.RWMutex mutex sync.RWMutex
signatureContainers map[string]container signatureContainers map[uint64]*container
} }
// Registry is a registrar where metrics are listed. // 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 // This builds a new metric registry. It is not needed in the majority of
// cases. // cases.
func NewRegistry() Registry { func NewRegistry() Registry {
return registry{ return &registry{
signatureContainers: make(map[string]container), signatureContainers: make(map[uint64]*container),
} }
} }
@ -83,33 +98,28 @@ func Register(name, docstring string, baseLabels map[string]string, metric Metri
} }
// Implements json.Marshaler // Implements json.Marshaler
func (r registry) MarshalJSON() (_ []byte, err error) { func (r *registry) MarshalJSON() ([]byte, error) {
metrics := make([]interface{}, 0, len(r.signatureContainers)) containers := make(containers, 0, len(r.signatureContainers))
keys := make([]string, 0, len(metrics)) for _, container := range r.signatureContainers {
for key := range r.signatureContainers { containers = append(containers, container)
keys = append(keys, key)
} }
sort.Strings(keys) sort.Sort(containers)
for _, key := range keys { return json.Marshal(containers)
metrics = append(metrics, r.signatureContainers[key])
}
return json.Marshal(metrics)
} }
// isValidCandidate returns true if the candidate is acceptable for use. In the // isValidCandidate returns true if the candidate is acceptable for use. In the
// event of any apparent incorrect use it will report the problem, invalidate // event of any apparent incorrect use it will report the problem, invalidate
// the candidate, or outright abort. // 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 { if len(name) == 0 {
err = fmt.Errorf("unnamed metric named with baseLabels %s is invalid", baseLabels) err = fmt.Errorf("unnamed metric named with baseLabels %s is invalid", baseLabels)
if abortOnMisuse { if *abortOnMisuse {
panic(err) panic(err)
} else if debugRegistration { } else if *debugRegistration {
log.Println(err) log.Println(err)
} }
} }
@ -117,13 +127,13 @@ func (r registry) isValidCandidate(name string, baseLabels map[string]string) (s
if _, contains := baseLabels[nameLabel]; contains { if _, contains := baseLabels[nameLabel]; contains {
err = fmt.Errorf("metric named %s with baseLabels %s contains reserved label name %s in baseLabels", name, baseLabels, nameLabel) 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) panic(err)
} else if debugRegistration { } else if *debugRegistration {
log.Println(err) log.Println(err)
} }
return return signature, err
} }
baseLabels[nameLabel] = name baseLabels[nameLabel] = name
@ -131,34 +141,34 @@ func (r registry) isValidCandidate(name string, baseLabels map[string]string) (s
if _, contains := r.signatureContainers[signature]; contains { if _, contains := r.signatureContainers[signature]; contains {
err = fmt.Errorf("metric named %s with baseLabels %s is already registered", name, baseLabels) err = fmt.Errorf("metric named %s with baseLabels %s is already registered", name, baseLabels)
if abortOnMisuse { if *abortOnMisuse {
panic(err) panic(err)
} else if debugRegistration { } else if *debugRegistration {
log.Println(err) log.Println(err)
} }
return return signature, err
} }
if useAggressiveSanityChecks { if *useAggressiveSanityChecks {
for _, container := range r.signatureContainers { for _, container := range r.signatureContainers {
if container.name == name { 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) 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) panic(err)
} else if debugRegistration { } else if *debugRegistration {
log.Println(err) 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() r.mutex.Lock()
defer r.mutex.Unlock() 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) signature, err := r.isValidCandidate(name, baseLabels)
if err != nil { if err != nil {
return return err
} }
r.signatureContainers[signature] = container{ r.signatureContainers[signature] = &container{
BaseLabels: baseLabels, BaseLabels: baseLabels,
Docstring: docstring, Docstring: docstring,
Metric: metric, Metric: metric,
name: name, name: name,
} }
return return nil
} }
// YieldBasicAuthExporter creates a http.HandlerFunc that is protected by HTTP's // YieldBasicAuthExporter creates a http.HandlerFunc that is protected by HTTP's
// basic authentication. // 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 // XXX: Work with Daniel to get this removed from the library, as it is really
// superfluous and can be much more elegantly accomplished via // superfluous and can be much more elegantly accomplished via
// delegation. // delegation.
log.Println("Registry.YieldBasicAuthExporter is deprecated.")
exporter := register.YieldExporter() exporter := register.YieldExporter()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 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.") log.Println("Registry.YieldExporter is deprecated in favor of Registry.Handler.")
return 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) { return func(w http.ResponseWriter, r *http.Request) {
defer requestLatencyAccumulator(time.Now()) defer requestLatencyAccumulator(time.Now())
requestCount.Increment(nil) requestCount.Increment(nil)
url := r.URL
if strings.HasSuffix(url.Path, jsonSuffix) {
header := w.Header() header := w.Header()
header.Set(contentTypeHeader, TelemetryContentType)
writer := decorateWriter(r, w) writer := decorateWriter(r, w)
@ -248,15 +283,34 @@ func (registry registry) Handler() http.HandlerFunc {
defer closer.Close() defer closer.Close()
} }
json.NewEncoder(writer).Encode(registry) accepts := goautoneg.ParseAccept(r.Header.Get("Accept"))
} else { for _, accept := range accepts {
w.WriteHeader(http.StatusNotFound) 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() { header.Set(contentTypeHeader, TelemetryContentType)
flag.BoolVar(&abortOnMisuse, FlagNamespace+"abortonmisuse", false, "abort if a semantic misuse is encountered (bool).") json.NewEncoder(writer).Encode(registry)
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).")
} }
}
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).")
)

View File

@ -13,6 +13,8 @@ import (
"io" "io"
"net/http" "net/http"
"testing" "testing"
"code.google.com/p/goprotobuf/proto"
) )
func testRegister(t tester) { func testRegister(t tester) {
@ -21,14 +23,14 @@ func testRegister(t tester) {
debugRegistration bool debugRegistration bool
useAggressiveSanityChecks bool useAggressiveSanityChecks bool
}{ }{
abortOnMisuse: abortOnMisuse, abortOnMisuse: *abortOnMisuse,
debugRegistration: debugRegistration, debugRegistration: *debugRegistration,
useAggressiveSanityChecks: useAggressiveSanityChecks, useAggressiveSanityChecks: *useAggressiveSanityChecks,
} }
defer func() { defer func() {
abortOnMisuse = oldState.abortOnMisuse abortOnMisuse = &(oldState.abortOnMisuse)
debugRegistration = oldState.debugRegistration debugRegistration = &(oldState.debugRegistration)
useAggressiveSanityChecks = oldState.useAggressiveSanityChecks useAggressiveSanityChecks = &(oldState.useAggressiveSanityChecks)
}() }()
type input struct { 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)) t.Fatalf("%d. expected scenario output length %d, got %d", i, len(scenario.inputs), len(scenario.outputs))
} }
abortOnMisuse = false abortOnMisuse = proto.Bool(false)
debugRegistration = false debugRegistration = proto.Bool(false)
useAggressiveSanityChecks = true useAggressiveSanityChecks = proto.Bool(true)
registry := NewRegistry() registry := NewRegistry()
@ -297,7 +299,7 @@ func testDumpToWriter(t tester) {
} }
for i, scenario := range scenarios { for i, scenario := range scenarios {
registry := NewRegistry().(registry) registry := NewRegistry().(*registry)
for name, metric := range scenario.in.metrics { 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) err := registry.Register(name, fmt.Sprintf("metric %s", name), map[string]string{fmt.Sprintf("label_%s", name): name}, metric)

View File

@ -7,35 +7,28 @@
package prometheus package prometheus
import ( import (
"bytes" "fmt"
"hash/fnv"
"sort" "sort"
)
const ( "github.com/prometheus/client_golang/model"
delimiter = "|"
) )
// LabelsToSignature provides a way of building a unique signature // LabelsToSignature provides a way of building a unique signature
// (i.e., fingerprint) for a given label set sequence. // (i.e., fingerprint) for a given label set sequence.
func labelsToSignature(labels map[string]string) string { func labelsToSignature(labels map[string]string) uint64 {
// TODO(matt): This is a wart, and we'll want to validate that collisions names := make(model.LabelNames, 0, len(labels))
// do not occur in less-than-diligent environments. for name := range labels {
cardinality := len(labels) names = append(names, model.LabelName(name))
keys := make([]string, 0, cardinality)
for label := range labels {
keys = append(keys, label)
} }
sort.Strings(keys) sort.Sort(names)
buffer := bytes.Buffer{} hasher := fnv.New64a()
for _, label := range keys { for _, name := range names {
buffer.WriteString(label) fmt.Fprintf(hasher, string(name), labels[string(name)])
buffer.WriteString(delimiter)
buffer.WriteString(labels[label])
} }
return buffer.String() return hasher.Sum64()
} }

View File

@ -13,13 +13,16 @@ import (
func testLabelsToSignature(t tester) { func testLabelsToSignature(t tester) {
var scenarios = []struct { var scenarios = []struct {
in map[string]string in map[string]string
out string out uint64
}{ }{
{ {
in: map[string]string{}, 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 { for i, scenario := range scenarios {

1
vendor/goautoneg/MANIFEST vendored Normal file
View File

@ -0,0 +1 @@
Imported at 75cd24fc2f2c from https://bitbucket.org/ww/goautoneg.

13
vendor/goautoneg/Makefile vendored Normal file
View File

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

67
vendor/goautoneg/README.txt vendored Normal file
View File

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

162
vendor/goautoneg/autoneg.go vendored Normal file
View File

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

33
vendor/goautoneg/autoneg_test.go vendored Normal file
View File

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