1// Copyright 2014 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14// Copyright (c) 2013, The Prometheus Authors
15// All rights reserved.
16//
17// Use of this source code is governed by a BSD-style license that can be found
18// in the LICENSE file.
19
20package prometheus_test
21
22import (
23	"bytes"
24	"net/http"
25	"net/http/httptest"
26	"testing"
27
28	dto "github.com/prometheus/client_model/go"
29
30	"github.com/golang/protobuf/proto"
31	"github.com/prometheus/common/expfmt"
32
33	"github.com/prometheus/client_golang/prometheus"
34	"github.com/prometheus/client_golang/prometheus/promhttp"
35)
36
37func testHandler(t testing.TB) {
38
39	metricVec := prometheus.NewCounterVec(
40		prometheus.CounterOpts{
41			Name:        "name",
42			Help:        "docstring",
43			ConstLabels: prometheus.Labels{"constname": "constvalue"},
44		},
45		[]string{"labelname"},
46	)
47
48	metricVec.WithLabelValues("val1").Inc()
49	metricVec.WithLabelValues("val2").Inc()
50
51	externalMetricFamily := &dto.MetricFamily{
52		Name: proto.String("externalname"),
53		Help: proto.String("externaldocstring"),
54		Type: dto.MetricType_COUNTER.Enum(),
55		Metric: []*dto.Metric{
56			{
57				Label: []*dto.LabelPair{
58					{
59						Name:  proto.String("externalconstname"),
60						Value: proto.String("externalconstvalue"),
61					},
62					{
63						Name:  proto.String("externallabelname"),
64						Value: proto.String("externalval1"),
65					},
66				},
67				Counter: &dto.Counter{
68					Value: proto.Float64(1),
69				},
70			},
71		},
72	}
73	externalBuf := &bytes.Buffer{}
74	enc := expfmt.NewEncoder(externalBuf, expfmt.FmtProtoDelim)
75	if err := enc.Encode(externalMetricFamily); err != nil {
76		t.Fatal(err)
77	}
78	externalMetricFamilyAsBytes := externalBuf.Bytes()
79	externalMetricFamilyAsText := []byte(`# HELP externalname externaldocstring
80# TYPE externalname counter
81externalname{externalconstname="externalconstvalue",externallabelname="externalval1"} 1
82`)
83	externalMetricFamilyAsProtoText := []byte(`name: "externalname"
84help: "externaldocstring"
85type: COUNTER
86metric: <
87  label: <
88    name: "externalconstname"
89    value: "externalconstvalue"
90  >
91  label: <
92    name: "externallabelname"
93    value: "externalval1"
94  >
95  counter: <
96    value: 1
97  >
98>
99
100`)
101	externalMetricFamilyAsProtoCompactText := []byte(`name:"externalname" help:"externaldocstring" type:COUNTER metric:<label:<name:"externalconstname" value:"externalconstvalue" > label:<name:"externallabelname" value:"externalval1" > counter:<value:1 > >
102`)
103
104	expectedMetricFamily := &dto.MetricFamily{
105		Name: proto.String("name"),
106		Help: proto.String("docstring"),
107		Type: dto.MetricType_COUNTER.Enum(),
108		Metric: []*dto.Metric{
109			{
110				Label: []*dto.LabelPair{
111					{
112						Name:  proto.String("constname"),
113						Value: proto.String("constvalue"),
114					},
115					{
116						Name:  proto.String("labelname"),
117						Value: proto.String("val1"),
118					},
119				},
120				Counter: &dto.Counter{
121					Value: proto.Float64(1),
122				},
123			},
124			{
125				Label: []*dto.LabelPair{
126					{
127						Name:  proto.String("constname"),
128						Value: proto.String("constvalue"),
129					},
130					{
131						Name:  proto.String("labelname"),
132						Value: proto.String("val2"),
133					},
134				},
135				Counter: &dto.Counter{
136					Value: proto.Float64(1),
137				},
138			},
139		},
140	}
141	buf := &bytes.Buffer{}
142	enc = expfmt.NewEncoder(buf, expfmt.FmtProtoDelim)
143	if err := enc.Encode(expectedMetricFamily); err != nil {
144		t.Fatal(err)
145	}
146	expectedMetricFamilyAsBytes := buf.Bytes()
147	expectedMetricFamilyAsText := []byte(`# HELP name docstring
148# TYPE name counter
149name{constname="constvalue",labelname="val1"} 1
150name{constname="constvalue",labelname="val2"} 1
151`)
152	expectedMetricFamilyAsProtoText := []byte(`name: "name"
153help: "docstring"
154type: COUNTER
155metric: <
156  label: <
157    name: "constname"
158    value: "constvalue"
159  >
160  label: <
161    name: "labelname"
162    value: "val1"
163  >
164  counter: <
165    value: 1
166  >
167>
168metric: <
169  label: <
170    name: "constname"
171    value: "constvalue"
172  >
173  label: <
174    name: "labelname"
175    value: "val2"
176  >
177  counter: <
178    value: 1
179  >
180>
181
182`)
183	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 > >
184`)
185
186	externalMetricFamilyWithSameName := &dto.MetricFamily{
187		Name: proto.String("name"),
188		Help: proto.String("docstring"),
189		Type: dto.MetricType_COUNTER.Enum(),
190		Metric: []*dto.Metric{
191			{
192				Label: []*dto.LabelPair{
193					{
194						Name:  proto.String("constname"),
195						Value: proto.String("constvalue"),
196					},
197					{
198						Name:  proto.String("labelname"),
199						Value: proto.String("different_val"),
200					},
201				},
202				Counter: &dto.Counter{
203					Value: proto.Float64(42),
204				},
205			},
206		},
207	}
208
209	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 > >
210`)
211
212	externalMetricFamilyWithInvalidLabelValue := &dto.MetricFamily{
213		Name: proto.String("name"),
214		Help: proto.String("docstring"),
215		Type: dto.MetricType_COUNTER.Enum(),
216		Metric: []*dto.Metric{
217			{
218				Label: []*dto.LabelPair{
219					{
220						Name:  proto.String("constname"),
221						Value: proto.String("\xFF"),
222					},
223					{
224						Name:  proto.String("labelname"),
225						Value: proto.String("different_val"),
226					},
227				},
228				Counter: &dto.Counter{
229					Value: proto.Float64(42),
230				},
231			},
232		},
233	}
234
235	expectedMetricFamilyInvalidLabelValueAsText := []byte(`An error has occurred during metrics gathering:
236
237collected metric's label constname is not utf8: "\xff"
238`)
239
240	type output struct {
241		headers map[string]string
242		body    []byte
243	}
244
245	var scenarios = []struct {
246		headers    map[string]string
247		out        output
248		collector  prometheus.Collector
249		externalMF []*dto.MetricFamily
250	}{
251		{ // 0
252			headers: map[string]string{
253				"Accept": "foo/bar;q=0.2, dings/bums;q=0.8",
254			},
255			out: output{
256				headers: map[string]string{
257					"Content-Type": `text/plain; version=0.0.4`,
258				},
259				body: []byte{},
260			},
261		},
262		{ // 1
263			headers: map[string]string{
264				"Accept": "foo/bar;q=0.2, application/quark;q=0.8",
265			},
266			out: output{
267				headers: map[string]string{
268					"Content-Type": `text/plain; version=0.0.4`,
269				},
270				body: []byte{},
271			},
272		},
273		{ // 2
274			headers: map[string]string{
275				"Accept": "foo/bar;q=0.2, application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=bla;q=0.8",
276			},
277			out: output{
278				headers: map[string]string{
279					"Content-Type": `text/plain; version=0.0.4`,
280				},
281				body: []byte{},
282			},
283		},
284		{ // 3
285			headers: map[string]string{
286				"Accept": "text/plain;q=0.2, application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.8",
287			},
288			out: output{
289				headers: map[string]string{
290					"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
291				},
292				body: []byte{},
293			},
294		},
295		{ // 4
296			headers: map[string]string{
297				"Accept": "application/json",
298			},
299			out: output{
300				headers: map[string]string{
301					"Content-Type": `text/plain; version=0.0.4`,
302				},
303				body: expectedMetricFamilyAsText,
304			},
305			collector: metricVec,
306		},
307		{ // 5
308			headers: map[string]string{
309				"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
310			},
311			out: output{
312				headers: map[string]string{
313					"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
314				},
315				body: expectedMetricFamilyAsBytes,
316			},
317			collector: metricVec,
318		},
319		{ // 6
320			headers: map[string]string{
321				"Accept": "application/json",
322			},
323			out: output{
324				headers: map[string]string{
325					"Content-Type": `text/plain; version=0.0.4`,
326				},
327				body: externalMetricFamilyAsText,
328			},
329			externalMF: []*dto.MetricFamily{externalMetricFamily},
330		},
331		{ // 7
332			headers: map[string]string{
333				"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
334			},
335			out: output{
336				headers: map[string]string{
337					"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
338				},
339				body: externalMetricFamilyAsBytes,
340			},
341			externalMF: []*dto.MetricFamily{externalMetricFamily},
342		},
343		{ // 8
344			headers: map[string]string{
345				"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
346			},
347			out: output{
348				headers: map[string]string{
349					"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
350				},
351				body: bytes.Join(
352					[][]byte{
353						externalMetricFamilyAsBytes,
354						expectedMetricFamilyAsBytes,
355					},
356					[]byte{},
357				),
358			},
359			collector:  metricVec,
360			externalMF: []*dto.MetricFamily{externalMetricFamily},
361		},
362		{ // 9
363			headers: map[string]string{
364				"Accept": "text/plain",
365			},
366			out: output{
367				headers: map[string]string{
368					"Content-Type": `text/plain; version=0.0.4`,
369				},
370				body: []byte{},
371			},
372		},
373		{ // 10
374			headers: map[string]string{
375				"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=bla;q=0.2, text/plain;q=0.5",
376			},
377			out: output{
378				headers: map[string]string{
379					"Content-Type": `text/plain; version=0.0.4`,
380				},
381				body: expectedMetricFamilyAsText,
382			},
383			collector: metricVec,
384		},
385		{ // 11
386			headers: map[string]string{
387				"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=bla;q=0.2, text/plain;q=0.5;version=0.0.4",
388			},
389			out: output{
390				headers: map[string]string{
391					"Content-Type": `text/plain; version=0.0.4`,
392				},
393				body: bytes.Join(
394					[][]byte{
395						externalMetricFamilyAsText,
396						expectedMetricFamilyAsText,
397					},
398					[]byte{},
399				),
400			},
401			collector:  metricVec,
402			externalMF: []*dto.MetricFamily{externalMetricFamily},
403		},
404		{ // 12
405			headers: map[string]string{
406				"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.2, text/plain;q=0.5;version=0.0.2",
407			},
408			out: output{
409				headers: map[string]string{
410					"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
411				},
412				body: bytes.Join(
413					[][]byte{
414						externalMetricFamilyAsBytes,
415						expectedMetricFamilyAsBytes,
416					},
417					[]byte{},
418				),
419			},
420			collector:  metricVec,
421			externalMF: []*dto.MetricFamily{externalMetricFamily},
422		},
423		{ // 13
424			headers: map[string]string{
425				"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",
426			},
427			out: output{
428				headers: map[string]string{
429					"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text`,
430				},
431				body: bytes.Join(
432					[][]byte{
433						externalMetricFamilyAsProtoText,
434						expectedMetricFamilyAsProtoText,
435					},
436					[]byte{},
437				),
438			},
439			collector:  metricVec,
440			externalMF: []*dto.MetricFamily{externalMetricFamily},
441		},
442		{ // 14
443			headers: map[string]string{
444				"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text",
445			},
446			out: output{
447				headers: map[string]string{
448					"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text`,
449				},
450				body: bytes.Join(
451					[][]byte{
452						externalMetricFamilyAsProtoCompactText,
453						expectedMetricFamilyAsProtoCompactText,
454					},
455					[]byte{},
456				),
457			},
458			collector:  metricVec,
459			externalMF: []*dto.MetricFamily{externalMetricFamily},
460		},
461		{ // 15
462			headers: map[string]string{
463				"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text",
464			},
465			out: output{
466				headers: map[string]string{
467					"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text`,
468				},
469				body: bytes.Join(
470					[][]byte{
471						externalMetricFamilyAsProtoCompactText,
472						expectedMetricFamilyMergedWithExternalAsProtoCompactText,
473					},
474					[]byte{},
475				),
476			},
477			collector: metricVec,
478			externalMF: []*dto.MetricFamily{
479				externalMetricFamily,
480				externalMetricFamilyWithSameName,
481			},
482		},
483		{ // 16
484			headers: map[string]string{
485				"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text",
486			},
487			out: output{
488				headers: map[string]string{
489					"Content-Type": `text/plain; charset=utf-8`,
490				},
491				body: expectedMetricFamilyInvalidLabelValueAsText,
492			},
493			collector: metricVec,
494			externalMF: []*dto.MetricFamily{
495				externalMetricFamily,
496				externalMetricFamilyWithInvalidLabelValue,
497			},
498		},
499	}
500	for i, scenario := range scenarios {
501		registry := prometheus.NewPedanticRegistry()
502		gatherer := prometheus.Gatherer(registry)
503		if scenario.externalMF != nil {
504			gatherer = prometheus.Gatherers{
505				registry,
506				prometheus.GathererFunc(func() ([]*dto.MetricFamily, error) {
507					return scenario.externalMF, nil
508				}),
509			}
510		}
511
512		if scenario.collector != nil {
513			registry.Register(scenario.collector)
514		}
515		writer := httptest.NewRecorder()
516		handler := prometheus.InstrumentHandler("prometheus", promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{}))
517		request, _ := http.NewRequest("GET", "/", nil)
518		for key, value := range scenario.headers {
519			request.Header.Add(key, value)
520		}
521		handler(writer, request)
522
523		for key, value := range scenario.out.headers {
524			if writer.HeaderMap.Get(key) != value {
525				t.Errorf(
526					"%d. expected %q for header %q, got %q",
527					i, value, key, writer.Header().Get(key),
528				)
529			}
530		}
531
532		if !bytes.Equal(scenario.out.body, writer.Body.Bytes()) {
533			t.Errorf(
534				"%d. expected body:\n%s\ngot body:\n%s\n",
535				i, scenario.out.body, writer.Body.Bytes(),
536			)
537		}
538	}
539}
540
541func TestHandler(t *testing.T) {
542	testHandler(t)
543}
544
545func BenchmarkHandler(b *testing.B) {
546	for i := 0; i < b.N; i++ {
547		testHandler(b)
548	}
549}
550
551func TestRegisterWithOrGet(t *testing.T) {
552	// Replace the default registerer just to be sure. This is bad, but this
553	// whole test will go away once RegisterOrGet is removed.
554	oldRegisterer := prometheus.DefaultRegisterer
555	defer func() {
556		prometheus.DefaultRegisterer = oldRegisterer
557	}()
558	prometheus.DefaultRegisterer = prometheus.NewRegistry()
559	original := prometheus.NewCounterVec(
560		prometheus.CounterOpts{
561			Name: "test",
562			Help: "help",
563		},
564		[]string{"foo", "bar"},
565	)
566	equalButNotSame := prometheus.NewCounterVec(
567		prometheus.CounterOpts{
568			Name: "test",
569			Help: "help",
570		},
571		[]string{"foo", "bar"},
572	)
573	var err error
574	if err = prometheus.Register(original); err != nil {
575		t.Fatal(err)
576	}
577	if err = prometheus.Register(equalButNotSame); err == nil {
578		t.Fatal("expected error when registringe equal collector")
579	}
580	if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
581		if are.ExistingCollector != original {
582			t.Error("expected original collector but got something else")
583		}
584		if are.ExistingCollector == equalButNotSame {
585			t.Error("expected original callector but got new one")
586		}
587	} else {
588		t.Error("unexpected error:", err)
589	}
590}
591