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)
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ®istry{
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header.Set(contentTypeHeader, TelemetryContentType)
|
||||||
|
json.NewEncoder(writer).Encode(registry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
var (
|
||||||
flag.BoolVar(&abortOnMisuse, FlagNamespace+"abortonmisuse", false, "abort if a semantic misuse is encountered (bool).")
|
abortOnMisuse = flag.Bool(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).")
|
debugRegistration = flag.Bool(FlagNamespace+"debugregistration", false, "display information about the metric registration process (bool).")
|
||||||
flag.BoolVar(&useAggressiveSanityChecks, FlagNamespace+"useaggressivesanitychecks", false, "perform expensive validation of metrics (bool).")
|
useAggressiveSanityChecks = flag.Bool(FlagNamespace+"useaggressivesanitychecks", false, "perform expensive validation of metrics (bool).")
|
||||||
}
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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