prometheus
433 строки · 12.9 Кб
1// Copyright 2016 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
14package web
15
16import (
17"bytes"
18"context"
19"errors"
20"fmt"
21"io"
22"net/http"
23"net/http/httptest"
24"sort"
25"strconv"
26"strings"
27"testing"
28"time"
29
30"github.com/prometheus/common/model"
31"github.com/stretchr/testify/require"
32
33"github.com/prometheus/prometheus/config"
34"github.com/prometheus/prometheus/model/histogram"
35"github.com/prometheus/prometheus/model/labels"
36"github.com/prometheus/prometheus/model/textparse"
37"github.com/prometheus/prometheus/promql"
38"github.com/prometheus/prometheus/promql/promqltest"
39"github.com/prometheus/prometheus/storage"
40"github.com/prometheus/prometheus/tsdb"
41"github.com/prometheus/prometheus/util/teststorage"
42"github.com/prometheus/prometheus/util/testutil"
43)
44
45var scenarios = map[string]struct {
46params string
47externalLabels labels.Labels
48code int
49body string
50}{
51"empty": {
52params: "",
53code: http.StatusOK,
54body: ``,
55},
56"match nothing": {
57params: "match[]=does_not_match_anything",
58code: http.StatusOK,
59body: ``,
60},
61"invalid params from the beginning": {
62params: "match[]=-not-a-valid-metric-name",
63code: http.StatusBadRequest,
64body: `1:1: parse error: unexpected <op:->
65`,
66},
67"invalid params somewhere in the middle": {
68params: "match[]=not-a-valid-metric-name",
69code: http.StatusBadRequest,
70body: `1:4: parse error: unexpected <op:->
71`,
72},
73"test_metric1": {
74params: "match[]=test_metric1",
75code: http.StatusOK,
76body: `# TYPE test_metric1 untyped
77test_metric1{foo="bar",instance="i"} 10000 6000000
78test_metric1{foo="boo",instance="i"} 1 6000000
79`,
80},
81"test_metric2": {
82params: "match[]=test_metric2",
83code: http.StatusOK,
84body: `# TYPE test_metric2 untyped
85test_metric2{foo="boo",instance="i"} 1 6000000
86`,
87},
88"test_metric_without_labels": {
89params: "match[]=test_metric_without_labels",
90code: http.StatusOK,
91body: `# TYPE test_metric_without_labels untyped
92test_metric_without_labels{instance=""} 1001 6000000
93`,
94},
95"test_stale_metric": {
96params: "match[]=test_metric_stale",
97code: http.StatusOK,
98body: ``,
99},
100"test_old_metric": {
101params: "match[]=test_metric_old",
102code: http.StatusOK,
103body: `# TYPE test_metric_old untyped
104test_metric_old{instance=""} 981 5880000
105`,
106},
107"{foo='boo'}": {
108params: "match[]={foo='boo'}",
109code: http.StatusOK,
110body: `# TYPE test_metric1 untyped
111test_metric1{foo="boo",instance="i"} 1 6000000
112# TYPE test_metric2 untyped
113test_metric2{foo="boo",instance="i"} 1 6000000
114`,
115},
116"two matchers": {
117params: "match[]=test_metric1&match[]=test_metric2",
118code: http.StatusOK,
119body: `# TYPE test_metric1 untyped
120test_metric1{foo="bar",instance="i"} 10000 6000000
121test_metric1{foo="boo",instance="i"} 1 6000000
122# TYPE test_metric2 untyped
123test_metric2{foo="boo",instance="i"} 1 6000000
124`,
125},
126"two matchers with overlap": {
127params: "match[]={__name__=~'test_metric1'}&match[]={foo='bar'}",
128code: http.StatusOK,
129body: `# TYPE test_metric1 untyped
130test_metric1{foo="bar",instance="i"} 10000 6000000
131test_metric1{foo="boo",instance="i"} 1 6000000
132`,
133},
134"everything": {
135params: "match[]={__name__=~'.%2b'}", // '%2b' is an URL-encoded '+'.
136code: http.StatusOK,
137body: `# TYPE test_metric1 untyped
138test_metric1{foo="bar",instance="i"} 10000 6000000
139test_metric1{foo="boo",instance="i"} 1 6000000
140# TYPE test_metric2 untyped
141test_metric2{foo="boo",instance="i"} 1 6000000
142# TYPE test_metric_old untyped
143test_metric_old{instance=""} 981 5880000
144# TYPE test_metric_without_labels untyped
145test_metric_without_labels{instance=""} 1001 6000000
146`,
147},
148"empty label value matches everything that doesn't have that label": {
149params: "match[]={foo='',__name__=~'.%2b'}",
150code: http.StatusOK,
151body: `# TYPE test_metric_old untyped
152test_metric_old{instance=""} 981 5880000
153# TYPE test_metric_without_labels untyped
154test_metric_without_labels{instance=""} 1001 6000000
155`,
156},
157"empty label value for a label that doesn't exist at all, matches everything": {
158params: "match[]={bar='',__name__=~'.%2b'}",
159code: http.StatusOK,
160body: `# TYPE test_metric1 untyped
161test_metric1{foo="bar",instance="i"} 10000 6000000
162test_metric1{foo="boo",instance="i"} 1 6000000
163# TYPE test_metric2 untyped
164test_metric2{foo="boo",instance="i"} 1 6000000
165# TYPE test_metric_old untyped
166test_metric_old{instance=""} 981 5880000
167# TYPE test_metric_without_labels untyped
168test_metric_without_labels{instance=""} 1001 6000000
169`,
170},
171"external labels are added if not already present": {
172params: "match[]={__name__=~'.%2b'}", // '%2b' is an URL-encoded '+'.
173externalLabels: labels.FromStrings("foo", "baz", "zone", "ie"),
174code: http.StatusOK,
175body: `# TYPE test_metric1 untyped
176test_metric1{foo="bar",instance="i",zone="ie"} 10000 6000000
177test_metric1{foo="boo",instance="i",zone="ie"} 1 6000000
178# TYPE test_metric2 untyped
179test_metric2{foo="boo",instance="i",zone="ie"} 1 6000000
180# TYPE test_metric_old untyped
181test_metric_old{foo="baz",instance="",zone="ie"} 981 5880000
182# TYPE test_metric_without_labels untyped
183test_metric_without_labels{foo="baz",instance="",zone="ie"} 1001 6000000
184`,
185},
186"instance is an external label": {
187// This makes no sense as a configuration, but we should
188// know what it does anyway.
189params: "match[]={__name__=~'.%2b'}", // '%2b' is an URL-encoded '+'.
190externalLabels: labels.FromStrings("instance", "baz"),
191code: http.StatusOK,
192body: `# TYPE test_metric1 untyped
193test_metric1{foo="bar",instance="i"} 10000 6000000
194test_metric1{foo="boo",instance="i"} 1 6000000
195# TYPE test_metric2 untyped
196test_metric2{foo="boo",instance="i"} 1 6000000
197# TYPE test_metric_old untyped
198test_metric_old{instance="baz"} 981 5880000
199# TYPE test_metric_without_labels untyped
200test_metric_without_labels{instance="baz"} 1001 6000000
201`,
202},
203}
204
205func TestFederation(t *testing.T) {
206storage := promqltest.LoadedStorage(t, `
207load 1m
208test_metric1{foo="bar",instance="i"} 0+100x100
209test_metric1{foo="boo",instance="i"} 1+0x100
210test_metric2{foo="boo",instance="i"} 1+0x100
211test_metric_without_labels 1+10x100
212test_metric_stale 1+10x99 stale
213test_metric_old 1+10x98
214`)
215t.Cleanup(func() { storage.Close() })
216
217h := &Handler{
218localStorage: &dbAdapter{storage.DB},
219lookbackDelta: 5 * time.Minute,
220now: func() model.Time { return 101 * 60 * 1000 }, // 101min after epoch.
221config: &config.Config{
222GlobalConfig: config.GlobalConfig{},
223},
224}
225
226for name, scenario := range scenarios {
227t.Run(name, func(t *testing.T) {
228h.config.GlobalConfig.ExternalLabels = scenario.externalLabels
229req := httptest.NewRequest(http.MethodGet, "http://example.org/federate?"+scenario.params, nil)
230res := httptest.NewRecorder()
231
232h.federation(res, req)
233require.Equal(t, scenario.code, res.Code)
234require.Equal(t, scenario.body, normalizeBody(res.Body))
235})
236}
237}
238
239type notReadyReadStorage struct {
240LocalStorage
241}
242
243func (notReadyReadStorage) Querier(int64, int64) (storage.Querier, error) {
244return nil, fmt.Errorf("wrap: %w", tsdb.ErrNotReady)
245}
246
247func (notReadyReadStorage) StartTime() (int64, error) {
248return 0, fmt.Errorf("wrap: %w", tsdb.ErrNotReady)
249}
250
251func (notReadyReadStorage) Stats(string, int) (*tsdb.Stats, error) {
252return nil, fmt.Errorf("wrap: %w", tsdb.ErrNotReady)
253}
254
255// Regression test for https://github.com/prometheus/prometheus/issues/7181.
256func TestFederation_NotReady(t *testing.T) {
257for name, scenario := range scenarios {
258t.Run(name, func(t *testing.T) {
259h := &Handler{
260localStorage: notReadyReadStorage{},
261lookbackDelta: 5 * time.Minute,
262now: func() model.Time { return 101 * 60 * 1000 }, // 101min after epoch.
263config: &config.Config{
264GlobalConfig: config.GlobalConfig{
265ExternalLabels: scenario.externalLabels,
266},
267},
268}
269
270req := httptest.NewRequest(http.MethodGet, "http://example.org/federate?"+scenario.params, nil)
271res := httptest.NewRecorder()
272
273h.federation(res, req)
274if scenario.code == http.StatusBadRequest {
275// Request are expected to be checked before DB readiness.
276require.Equal(t, http.StatusBadRequest, res.Code)
277return
278}
279require.Equal(t, http.StatusServiceUnavailable, res.Code)
280})
281}
282}
283
284// normalizeBody sorts the lines within a metric to make it easy to verify the body.
285// (Federation is not taking care of sorting within a metric family.)
286func normalizeBody(body *bytes.Buffer) string {
287var (
288lines []string
289lastHash int
290)
291for line, err := body.ReadString('\n'); err == nil; line, err = body.ReadString('\n') {
292if line[0] == '#' && len(lines) > 0 {
293sort.Strings(lines[lastHash+1:])
294lastHash = len(lines)
295}
296lines = append(lines, line)
297}
298if len(lines) > 0 {
299sort.Strings(lines[lastHash+1:])
300}
301return strings.Join(lines, "")
302}
303
304func TestFederationWithNativeHistograms(t *testing.T) {
305storage := teststorage.New(t)
306t.Cleanup(func() { storage.Close() })
307
308var expVec promql.Vector
309
310db := storage.DB
311hist := &histogram.Histogram{
312Count: 12,
313ZeroCount: 2,
314ZeroThreshold: 0.001,
315Sum: 39.4,
316Schema: 1,
317PositiveSpans: []histogram.Span{
318{Offset: 0, Length: 2},
319{Offset: 1, Length: 2},
320},
321PositiveBuckets: []int64{1, 1, -1, 0},
322NegativeSpans: []histogram.Span{
323{Offset: 0, Length: 2},
324{Offset: 1, Length: 2},
325},
326NegativeBuckets: []int64{1, 1, -1, 0},
327}
328histWithoutZeroBucket := &histogram.Histogram{
329Count: 20,
330Sum: 99.23,
331Schema: 1,
332PositiveSpans: []histogram.Span{
333{Offset: 0, Length: 2},
334{Offset: 1, Length: 2},
335},
336PositiveBuckets: []int64{2, 2, -2, 0},
337NegativeSpans: []histogram.Span{
338{Offset: 0, Length: 2},
339{Offset: 1, Length: 2},
340},
341NegativeBuckets: []int64{2, 2, -2, 0},
342}
343app := db.Appender(context.Background())
344for i := 0; i < 6; i++ {
345l := labels.FromStrings("__name__", "test_metric", "foo", strconv.Itoa(i))
346expL := labels.FromStrings("__name__", "test_metric", "instance", "", "foo", strconv.Itoa(i))
347var err error
348switch i {
349case 0, 3:
350_, err = app.Append(0, l, 100*60*1000, float64(i*100))
351expVec = append(expVec, promql.Sample{
352T: 100 * 60 * 1000,
353F: float64(i * 100),
354Metric: expL,
355})
356case 4:
357_, err = app.AppendHistogram(0, l, 100*60*1000, histWithoutZeroBucket.Copy(), nil)
358expVec = append(expVec, promql.Sample{
359T: 100 * 60 * 1000,
360H: histWithoutZeroBucket.ToFloat(nil),
361Metric: expL,
362})
363default:
364hist.ZeroCount++
365hist.Count++
366_, err = app.AppendHistogram(0, l, 100*60*1000, hist.Copy(), nil)
367expVec = append(expVec, promql.Sample{
368T: 100 * 60 * 1000,
369H: hist.ToFloat(nil),
370Metric: expL,
371})
372}
373require.NoError(t, err)
374}
375require.NoError(t, app.Commit())
376
377h := &Handler{
378localStorage: &dbAdapter{db},
379lookbackDelta: 5 * time.Minute,
380now: func() model.Time { return 101 * 60 * 1000 }, // 101min after epoch.
381config: &config.Config{
382GlobalConfig: config.GlobalConfig{},
383},
384}
385
386req := httptest.NewRequest(http.MethodGet, "http://example.org/federate?match[]=test_metric", nil)
387req.Header.Add("Accept", `application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited,application/openmetrics-text;version=1.0.0;q=0.8,application/openmetrics-text;version=0.0.1;q=0.75,text/plain;version=0.0.4;q=0.5,*/*;q=0.1`)
388res := httptest.NewRecorder()
389
390h.federation(res, req)
391
392require.Equal(t, http.StatusOK, res.Code)
393body, err := io.ReadAll(res.Body)
394require.NoError(t, err)
395p := textparse.NewProtobufParser(body, false, labels.NewSymbolTable())
396var actVec promql.Vector
397metricFamilies := 0
398l := labels.Labels{}
399for {
400et, err := p.Next()
401if err != nil && errors.Is(err, io.EOF) {
402break
403}
404require.NoError(t, err)
405if et == textparse.EntryHistogram || et == textparse.EntrySeries {
406p.Metric(&l)
407}
408switch et {
409case textparse.EntryHelp:
410metricFamilies++
411case textparse.EntryHistogram:
412_, parsedTimestamp, h, fh := p.Histogram()
413require.Nil(t, h)
414actVec = append(actVec, promql.Sample{
415T: *parsedTimestamp,
416H: fh,
417Metric: l,
418})
419case textparse.EntrySeries:
420_, parsedTimestamp, f := p.Series()
421actVec = append(actVec, promql.Sample{
422T: *parsedTimestamp,
423F: f,
424Metric: l,
425})
426}
427}
428
429// TODO(codesome): Once PromQL is able to set the CounterResetHint on histograms,
430// test it with switching histogram types for metric families.
431require.Equal(t, 4, metricFamilies)
432testutil.RequireEqual(t, expVec, actVec)
433}
434