1// Copyright The OpenTelemetry Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package prometheus_test
16
17import (
18	"bytes"
19	"context"
20	"io/ioutil"
21	"net/http"
22	"net/http/httptest"
23	"sort"
24	"strings"
25	"testing"
26
27	"github.com/stretchr/testify/require"
28
29	"go.opentelemetry.io/otel/exporters/metric/prometheus"
30	"go.opentelemetry.io/otel/label"
31	"go.opentelemetry.io/otel/metric"
32	"go.opentelemetry.io/otel/sdk/metric/controller/pull"
33	"go.opentelemetry.io/otel/sdk/resource"
34)
35
36func TestPrometheusExporter(t *testing.T) {
37	// #TODO: This test does not adequately verify the type of
38	// prometheus metric exported for all types - for example,
39	// it does not verify that an UpDown- counter is exported
40	// as a gauge. To be improved.
41	exporter, err := prometheus.NewExportPipeline(
42		prometheus.Config{
43			DefaultHistogramBoundaries: []float64{-0.5, 1},
44		},
45		pull.WithCachePeriod(0),
46		pull.WithResource(resource.NewWithAttributes(label.String("R", "V"))),
47	)
48	require.NoError(t, err)
49
50	meter := exporter.MeterProvider().Meter("test")
51	upDownCounter := metric.Must(meter).NewFloat64UpDownCounter("updowncounter")
52	counter := metric.Must(meter).NewFloat64Counter("counter")
53	valuerecorder := metric.Must(meter).NewFloat64ValueRecorder("valuerecorder")
54
55	labels := []label.KeyValue{
56		label.Key("A").String("B"),
57		label.Key("C").String("D"),
58	}
59	ctx := context.Background()
60
61	var expected []string
62
63	counter.Add(ctx, 10, labels...)
64	counter.Add(ctx, 5.3, labels...)
65
66	expected = append(expected, `counter{A="B",C="D",R="V"} 15.3`)
67
68	_ = metric.Must(meter).NewInt64ValueObserver("intobserver", func(_ context.Context, result metric.Int64ObserverResult) {
69		result.Observe(1, labels...)
70	})
71
72	expected = append(expected, `intobserver{A="B",C="D",R="V"} 1`)
73
74	valuerecorder.Record(ctx, -0.6, labels...)
75	valuerecorder.Record(ctx, -0.4, labels...)
76	valuerecorder.Record(ctx, 0.6, labels...)
77	valuerecorder.Record(ctx, 20, labels...)
78
79	expected = append(expected, `valuerecorder_bucket{A="B",C="D",R="V",le="+Inf"} 4`)
80	expected = append(expected, `valuerecorder_bucket{A="B",C="D",R="V",le="-0.5"} 1`)
81	expected = append(expected, `valuerecorder_bucket{A="B",C="D",R="V",le="1"} 3`)
82	expected = append(expected, `valuerecorder_count{A="B",C="D",R="V"} 4`)
83	expected = append(expected, `valuerecorder_sum{A="B",C="D",R="V"} 19.6`)
84
85	upDownCounter.Add(ctx, 10, labels...)
86	upDownCounter.Add(ctx, -3.2, labels...)
87
88	expected = append(expected, `updowncounter{A="B",C="D",R="V"} 6.8`)
89
90	compareExport(t, exporter, expected)
91	compareExport(t, exporter, expected)
92}
93
94func compareExport(t *testing.T, exporter *prometheus.Exporter, expected []string) {
95	rec := httptest.NewRecorder()
96	req := httptest.NewRequest("GET", "/metrics", nil)
97	exporter.ServeHTTP(rec, req)
98
99	output := rec.Body.String()
100	lines := strings.Split(output, "\n")
101
102	var metricsOnly []string
103	for _, line := range lines {
104		if !strings.HasPrefix(line, "#") && line != "" {
105			metricsOnly = append(metricsOnly, line)
106		}
107	}
108
109	sort.Strings(metricsOnly)
110	sort.Strings(expected)
111
112	require.Equal(t, strings.Join(expected, "\n"), strings.Join(metricsOnly, "\n"))
113}
114
115func TestPrometheusStatefulness(t *testing.T) {
116	// Create a meter
117	exporter, err := prometheus.NewExportPipeline(
118		prometheus.Config{},
119		pull.WithCachePeriod(0),
120	)
121	require.NoError(t, err)
122
123	meter := exporter.MeterProvider().Meter("test")
124
125	// GET the HTTP endpoint
126	scrape := func() string {
127		var input bytes.Buffer
128		resp := httptest.NewRecorder()
129		req, err := http.NewRequest("GET", "/", &input)
130		require.NoError(t, err)
131
132		exporter.ServeHTTP(resp, req)
133		data, err := ioutil.ReadAll(resp.Result().Body)
134		require.NoError(t, err)
135
136		return string(data)
137	}
138
139	ctx := context.Background()
140
141	counter := metric.Must(meter).NewInt64Counter(
142		"a.counter",
143		metric.WithDescription("Counts things"),
144	)
145
146	counter.Add(ctx, 100, label.String("key", "value"))
147
148	require.Equal(t, `# HELP a_counter Counts things
149# TYPE a_counter counter
150a_counter{key="value"} 100
151`, scrape())
152
153	counter.Add(ctx, 100, label.String("key", "value"))
154
155	require.Equal(t, `# HELP a_counter Counts things
156# TYPE a_counter counter
157a_counter{key="value"} 200
158`, scrape())
159
160}
161