client_golang/prometheus/registry_test.go

875 lines
23 KiB
Go

// Copyright 2014 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Copyright (c) 2013, The Prometheus Authors
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
package prometheus_test
import (
"bytes"
"math/rand"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
dto "github.com/prometheus/client_model/go"
"github.com/golang/protobuf/proto"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// uncheckedCollector wraps a Collector but its Describe method yields no Desc.
type uncheckedCollector struct {
c prometheus.Collector
}
func (u uncheckedCollector) Describe(_ chan<- *prometheus.Desc) {}
func (u uncheckedCollector) Collect(c chan<- prometheus.Metric) {
u.c.Collect(c)
}
func testHandler(t testing.TB) {
// TODO(beorn7): This test is a bit too "end-to-end". It tests quite a
// few moving parts that are not strongly coupled. They could/should be
// tested separately. However, the changes planned for v0.10 will
// require a major rework of this test anyway, at which time I will
// structure it in a better way.
metricVec := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "name",
Help: "docstring",
ConstLabels: prometheus.Labels{"constname": "constvalue"},
},
[]string{"labelname"},
)
metricVec.WithLabelValues("val1").Inc()
metricVec.WithLabelValues("val2").Inc()
externalMetricFamily := &dto.MetricFamily{
Name: proto.String("externalname"),
Help: proto.String("externaldocstring"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("externalconstname"),
Value: proto.String("externalconstvalue"),
},
{
Name: proto.String("externallabelname"),
Value: proto.String("externalval1"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(1),
},
},
},
}
externalBuf := &bytes.Buffer{}
enc := expfmt.NewEncoder(externalBuf, expfmt.FmtProtoDelim)
if err := enc.Encode(externalMetricFamily); err != nil {
t.Fatal(err)
}
externalMetricFamilyAsBytes := externalBuf.Bytes()
externalMetricFamilyAsText := []byte(`# HELP externalname externaldocstring
# TYPE externalname counter
externalname{externalconstname="externalconstvalue",externallabelname="externalval1"} 1
`)
externalMetricFamilyAsProtoText := []byte(`name: "externalname"
help: "externaldocstring"
type: COUNTER
metric: <
label: <
name: "externalconstname"
value: "externalconstvalue"
>
label: <
name: "externallabelname"
value: "externalval1"
>
counter: <
value: 1
>
>
`)
externalMetricFamilyAsProtoCompactText := []byte(`name:"externalname" help:"externaldocstring" type:COUNTER metric:<label:<name:"externalconstname" value:"externalconstvalue" > label:<name:"externallabelname" value:"externalval1" > counter:<value:1 > >
`)
expectedMetricFamily := &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("docstring"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("constname"),
Value: proto.String("constvalue"),
},
{
Name: proto.String("labelname"),
Value: proto.String("val1"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(1),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("constname"),
Value: proto.String("constvalue"),
},
{
Name: proto.String("labelname"),
Value: proto.String("val2"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(1),
},
},
},
}
buf := &bytes.Buffer{}
enc = expfmt.NewEncoder(buf, expfmt.FmtProtoDelim)
if err := enc.Encode(expectedMetricFamily); err != nil {
t.Fatal(err)
}
expectedMetricFamilyAsBytes := buf.Bytes()
expectedMetricFamilyAsText := []byte(`# HELP name docstring
# TYPE name counter
name{constname="constvalue",labelname="val1"} 1
name{constname="constvalue",labelname="val2"} 1
`)
expectedMetricFamilyAsProtoText := []byte(`name: "name"
help: "docstring"
type: COUNTER
metric: <
label: <
name: "constname"
value: "constvalue"
>
label: <
name: "labelname"
value: "val1"
>
counter: <
value: 1
>
>
metric: <
label: <
name: "constname"
value: "constvalue"
>
label: <
name: "labelname"
value: "val2"
>
counter: <
value: 1
>
>
`)
expectedMetricFamilyAsProtoCompactText := []byte(`name:"name" help:"docstring" type:COUNTER metric:<label:<name:"constname" value:"constvalue" > label:<name:"labelname" value:"val1" > counter:<value:1 > > metric:<label:<name:"constname" value:"constvalue" > label:<name:"labelname" value:"val2" > counter:<value:1 > >
`)
externalMetricFamilyWithSameName := &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("docstring"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("constname"),
Value: proto.String("constvalue"),
},
{
Name: proto.String("labelname"),
Value: proto.String("different_val"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(42),
},
},
},
}
expectedMetricFamilyMergedWithExternalAsProtoCompactText := []byte(`name:"name" help:"docstring" type:COUNTER metric:<label:<name:"constname" value:"constvalue" > label:<name:"labelname" value:"different_val" > counter:<value:42 > > metric:<label:<name:"constname" value:"constvalue" > label:<name:"labelname" value:"val1" > counter:<value:1 > > metric:<label:<name:"constname" value:"constvalue" > label:<name:"labelname" value:"val2" > counter:<value:1 > >
`)
externalMetricFamilyWithInvalidLabelValue := &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("docstring"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("constname"),
Value: proto.String("\xFF"),
},
{
Name: proto.String("labelname"),
Value: proto.String("different_val"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(42),
},
},
},
}
expectedMetricFamilyInvalidLabelValueAsText := []byte(`An error has occurred while serving metrics:
collected metric "name" { label:<name:"constname" value:"\377" > label:<name:"labelname" value:"different_val" > counter:<value:42 > } has a label named "constname" whose value is not utf8: "\xff"
`)
summary := prometheus.NewSummary(prometheus.SummaryOpts{
Name: "complex",
Help: "A metric to check collisions with _sum and _count.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
})
summaryAsText := []byte(`# HELP complex A metric to check collisions with _sum and _count.
# TYPE complex summary
complex{quantile="0.5"} NaN
complex{quantile="0.9"} NaN
complex{quantile="0.99"} NaN
complex_sum 0
complex_count 0
`)
histogram := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "complex",
Help: "A metric to check collisions with _sun, _count, and _bucket.",
})
externalMetricFamilyWithBucketSuffix := &dto.MetricFamily{
Name: proto.String("complex_bucket"),
Help: proto.String("externaldocstring"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(1),
},
},
},
}
externalMetricFamilyWithBucketSuffixAsText := []byte(`# HELP complex_bucket externaldocstring
# TYPE complex_bucket counter
complex_bucket 1
`)
externalMetricFamilyWithCountSuffix := &dto.MetricFamily{
Name: proto.String("complex_count"),
Help: proto.String("externaldocstring"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(1),
},
},
},
}
bucketCollisionMsg := []byte(`An error has occurred while serving metrics:
collected metric named "complex_bucket" collides with previously collected histogram named "complex"
`)
summaryCountCollisionMsg := []byte(`An error has occurred while serving metrics:
collected metric named "complex_count" collides with previously collected summary named "complex"
`)
histogramCountCollisionMsg := []byte(`An error has occurred while serving metrics:
collected metric named "complex_count" collides with previously collected histogram named "complex"
`)
externalMetricFamilyWithDuplicateLabel := &dto.MetricFamily{
Name: proto.String("broken_metric"),
Help: proto.String("The registry should detect the duplicate label."),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("foo"),
Value: proto.String("bar"),
},
{
Name: proto.String("foo"),
Value: proto.String("baz"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(2.7),
},
},
},
}
duplicateLabelMsg := []byte(`An error has occurred while serving metrics:
collected metric "broken_metric" { label:<name:"foo" value:"bar" > label:<name:"foo" value:"baz" > counter:<value:2.7 > } has two or more labels with the same name: foo
`)
type output struct {
headers map[string]string
body []byte
}
var scenarios = []struct {
headers map[string]string
out output
collector prometheus.Collector
externalMF []*dto.MetricFamily
}{
{ // 0
headers: map[string]string{
"Accept": "foo/bar;q=0.2, dings/bums;q=0.8",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: []byte{},
},
},
{ // 1
headers: map[string]string{
"Accept": "foo/bar;q=0.2, application/quark;q=0.8",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: []byte{},
},
},
{ // 2
headers: map[string]string{
"Accept": "foo/bar;q=0.2, application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=bla;q=0.8",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: []byte{},
},
},
{ // 3
headers: map[string]string{
"Accept": "text/plain;q=0.2, application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.8",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: []byte{},
},
},
{ // 4
headers: map[string]string{
"Accept": "application/json",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: expectedMetricFamilyAsText,
},
collector: metricVec,
},
{ // 5
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: expectedMetricFamilyAsBytes,
},
collector: metricVec,
},
{ // 6
headers: map[string]string{
"Accept": "application/json",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: externalMetricFamilyAsText,
},
externalMF: []*dto.MetricFamily{externalMetricFamily},
},
{ // 7
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: externalMetricFamilyAsBytes,
},
externalMF: []*dto.MetricFamily{externalMetricFamily},
},
{ // 8
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsBytes,
expectedMetricFamilyAsBytes,
},
[]byte{},
),
},
collector: metricVec,
externalMF: []*dto.MetricFamily{externalMetricFamily},
},
{ // 9
headers: map[string]string{
"Accept": "text/plain",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: []byte{},
},
},
{ // 10
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=bla;q=0.2, text/plain;q=0.5",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: expectedMetricFamilyAsText,
},
collector: metricVec,
},
{ // 11
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=bla;q=0.2, text/plain;q=0.5;version=0.0.4",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsText,
expectedMetricFamilyAsText,
},
[]byte{},
),
},
collector: metricVec,
externalMF: []*dto.MetricFamily{externalMetricFamily},
},
{ // 12
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.2, text/plain;q=0.5;version=0.0.2",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsBytes,
expectedMetricFamilyAsBytes,
},
[]byte{},
),
},
collector: metricVec,
externalMF: []*dto.MetricFamily{externalMetricFamily},
},
{ // 13
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=text;q=0.5, application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.4",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsProtoText,
expectedMetricFamilyAsProtoText,
},
[]byte{},
),
},
collector: metricVec,
externalMF: []*dto.MetricFamily{externalMetricFamily},
},
{ // 14
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsProtoCompactText,
expectedMetricFamilyAsProtoCompactText,
},
[]byte{},
),
},
collector: metricVec,
externalMF: []*dto.MetricFamily{externalMetricFamily},
},
{ // 15
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsProtoCompactText,
expectedMetricFamilyMergedWithExternalAsProtoCompactText,
},
[]byte{},
),
},
collector: metricVec,
externalMF: []*dto.MetricFamily{
externalMetricFamily,
externalMetricFamilyWithSameName,
},
},
{ // 16
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; charset=utf-8`,
},
body: expectedMetricFamilyInvalidLabelValueAsText,
},
collector: metricVec,
externalMF: []*dto.MetricFamily{
externalMetricFamily,
externalMetricFamilyWithInvalidLabelValue,
},
},
{ // 17
headers: map[string]string{
"Accept": "text/plain",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: expectedMetricFamilyAsText,
},
collector: uncheckedCollector{metricVec},
},
{ // 18
headers: map[string]string{
"Accept": "text/plain",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; charset=utf-8`,
},
body: histogramCountCollisionMsg,
},
collector: histogram,
externalMF: []*dto.MetricFamily{
externalMetricFamilyWithCountSuffix,
},
},
{ // 19
headers: map[string]string{
"Accept": "text/plain",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; charset=utf-8`,
},
body: bucketCollisionMsg,
},
collector: histogram,
externalMF: []*dto.MetricFamily{
externalMetricFamilyWithBucketSuffix,
},
},
{ // 20
headers: map[string]string{
"Accept": "text/plain",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; charset=utf-8`,
},
body: summaryCountCollisionMsg,
},
collector: summary,
externalMF: []*dto.MetricFamily{
externalMetricFamilyWithCountSuffix,
},
},
{ // 21
headers: map[string]string{
"Accept": "text/plain",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4; charset=utf-8`,
},
body: bytes.Join(
[][]byte{
summaryAsText,
externalMetricFamilyWithBucketSuffixAsText,
},
[]byte{},
),
},
collector: summary,
externalMF: []*dto.MetricFamily{
externalMetricFamilyWithBucketSuffix,
},
},
{ // 22
headers: map[string]string{
"Accept": "text/plain",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; charset=utf-8`,
},
body: duplicateLabelMsg,
},
externalMF: []*dto.MetricFamily{
externalMetricFamilyWithDuplicateLabel,
},
},
}
for i, scenario := range scenarios {
registry := prometheus.NewPedanticRegistry()
gatherer := prometheus.Gatherer(registry)
if scenario.externalMF != nil {
gatherer = prometheus.Gatherers{
registry,
prometheus.GathererFunc(func() ([]*dto.MetricFamily, error) {
return scenario.externalMF, nil
}),
}
}
if scenario.collector != nil {
registry.MustRegister(scenario.collector)
}
writer := httptest.NewRecorder()
handler := promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{})
request, _ := http.NewRequest("GET", "/", nil)
for key, value := range scenario.headers {
request.Header.Add(key, value)
}
handler.ServeHTTP(writer, request)
for key, value := range scenario.out.headers {
if writer.Header().Get(key) != value {
t.Errorf(
"%d. expected %q for header %q, got %q",
i, value, key, writer.Header().Get(key),
)
}
}
if !bytes.Equal(scenario.out.body, writer.Body.Bytes()) {
t.Errorf(
"%d. expected body:\n%s\ngot body:\n%s\n",
i, scenario.out.body, writer.Body.Bytes(),
)
}
}
}
func TestHandler(t *testing.T) {
testHandler(t)
}
func BenchmarkHandler(b *testing.B) {
for i := 0; i < b.N; i++ {
testHandler(b)
}
}
func TestAlreadyRegistered(t *testing.T) {
reg := prometheus.NewRegistry()
original := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "test",
Help: "help",
},
[]string{"foo", "bar"},
)
equalButNotSame := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "test",
Help: "help",
},
[]string{"foo", "bar"},
)
var err error
if err = reg.Register(original); err != nil {
t.Fatal(err)
}
if err = reg.Register(equalButNotSame); err == nil {
t.Fatal("expected error when registering equal collector")
}
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
if are.ExistingCollector != original {
t.Error("expected original collector but got something else")
}
if are.ExistingCollector == equalButNotSame {
t.Error("expected original callector but got new one")
}
} else {
t.Error("unexpected error:", err)
}
}
// TestHistogramVecRegisterGatherConcurrency is an end-to-end test that
// concurrently calls Observe on random elements of a HistogramVec while the
// same HistogramVec is registered concurrently and the Gather method of the
// registry is called concurrently.
func TestHistogramVecRegisterGatherConcurrency(t *testing.T) {
var (
reg = prometheus.NewPedanticRegistry()
hv = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "test_histogram",
Help: "This helps testing.",
ConstLabels: prometheus.Labels{"foo": "bar"},
},
[]string{"one", "two", "three"},
)
labelValues = []string{"a", "b", "c", "alpha", "beta", "gamma", "aleph", "beth", "gimel"}
quit = make(chan struct{})
wg sync.WaitGroup
)
observe := func() {
defer wg.Done()
for {
select {
case <-quit:
return
default:
obs := rand.NormFloat64()*.1 + .2
hv.WithLabelValues(
labelValues[rand.Intn(len(labelValues))],
labelValues[rand.Intn(len(labelValues))],
labelValues[rand.Intn(len(labelValues))],
).Observe(obs)
}
}
}
register := func() {
defer wg.Done()
for {
select {
case <-quit:
return
default:
if err := reg.Register(hv); err != nil {
if _, ok := err.(prometheus.AlreadyRegisteredError); !ok {
t.Error("Registering failed:", err)
}
}
time.Sleep(7 * time.Millisecond)
}
}
}
gather := func() {
defer wg.Done()
for {
select {
case <-quit:
return
default:
if g, err := reg.Gather(); err != nil {
t.Error("Gathering failed:", err)
} else {
if len(g) == 0 {
continue
}
if len(g) != 1 {
t.Error("Gathered unexpected number of metric families:", len(g))
}
if len(g[0].Metric[0].Label) != 4 {
t.Error("Gathered unexpected number of label pairs:", len(g[0].Metric[0].Label))
}
}
time.Sleep(4 * time.Millisecond)
}
}
}
wg.Add(10)
go observe()
go observe()
go register()
go observe()
go gather()
go observe()
go register()
go observe()
go gather()
go observe()
time.Sleep(time.Second)
close(quit)
wg.Wait()
}