mirror of
https://github.com/hanzoai/metrics.git
synced 2026-10-30 20:03:39 +00:00
The import aliases may complicate maintenance of the code in the long term if they aren't used consistently, e.g. if one file imports the apptest under the default name while the other file imports the apptest under the "at" name. The aliases also complicate grepping the code by apptest.* or prompbmarshal.* .
370 lines
14 KiB
Go
370 lines
14 KiB
Go
package tests
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/google/go-cmp/cmp"
|
||
"github.com/google/go-cmp/cmp/cmpopts"
|
||
|
||
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||
)
|
||
|
||
// Data used in examples in
|
||
// https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query and
|
||
// https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query
|
||
var docData = []string{
|
||
"foo_bar 1.00 1652169600000", // 2022-05-10T08:00:00Z
|
||
"foo_bar 2.00 1652169660000", // 2022-05-10T08:01:00Z
|
||
"foo_bar 3.00 1652169720000", // 2022-05-10T08:02:00Z
|
||
"foo_bar 5.00 1652169840000", // 2022-05-10T08:04:00Z, one point missed
|
||
"foo_bar 5.50 1652169960000", // 2022-05-10T08:06:00Z, one point missed
|
||
"foo_bar 5.50 1652170020000", // 2022-05-10T08:07:00Z
|
||
"foo_bar 4.00 1652170080000", // 2022-05-10T08:08:00Z
|
||
"foo_bar 3.50 1652170260000", // 2022-05-10T08:11:00Z, two points missed
|
||
"foo_bar 3.25 1652170320000", // 2022-05-10T08:12:00Z
|
||
"foo_bar 3.00 1652170380000", // 2022-05-10T08:13:00Z
|
||
"foo_bar 2.00 1652170440000", // 2022-05-10T08:14:00Z
|
||
"foo_bar 1.00 1652170500000", // 2022-05-10T08:15:00Z
|
||
"foo_bar 4.00 1652170560000", // 2022-05-10T08:16:00Z
|
||
}
|
||
|
||
// TestSingleKeyConceptsQuery verifies cases from https://docs.victoriametrics.com/victoriametrics/keyconcepts/#query-data
|
||
// for vm-single.
|
||
func TestSingleKeyConceptsQuery(t *testing.T) {
|
||
tc := apptest.NewTestCase(t)
|
||
defer tc.Stop()
|
||
|
||
sut := tc.MustStartDefaultVmsingle()
|
||
|
||
testKeyConceptsQueryData(t, sut)
|
||
}
|
||
|
||
// TestClusterKeyConceptsQueryData verifies cases from https://docs.victoriametrics.com/victoriametrics/keyconcepts/#query-data
|
||
// for vm-cluster.
|
||
func TestClusterKeyConceptsQueryData(t *testing.T) {
|
||
tc := apptest.NewTestCase(t)
|
||
defer tc.Stop()
|
||
|
||
sut := tc.MustStartDefaultCluster()
|
||
|
||
testKeyConceptsQueryData(t, sut)
|
||
}
|
||
|
||
// testKeyConceptsQueryData verifies cases from https://docs.victoriametrics.com/victoriametrics/keyconcepts/#query-data
|
||
func testKeyConceptsQueryData(t *testing.T, sut apptest.PrometheusWriteQuerier) {
|
||
|
||
// Insert example data from documentation.
|
||
sut.PrometheusAPIV1ImportPrometheus(t, docData, apptest.QueryOpts{})
|
||
sut.ForceFlush(t)
|
||
|
||
testInstantQuery(t, sut)
|
||
testRangeQuery(t, sut)
|
||
testRangeQueryIsEquivalentToManyInstantQueries(t, sut)
|
||
}
|
||
|
||
// testInstantQuery verifies the statements made in the `Instant query` section
|
||
// of the VictoriaMetrics documentation. See:
|
||
// https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query
|
||
func testInstantQuery(t *testing.T, q apptest.PrometheusQuerier) {
|
||
// Get the value of the foo_bar time series at 2022-05-10T08:03:00Z with the
|
||
// step of 5m and timeout 5s. There is no sample at exactly this timestamp.
|
||
// Therefore, VictoriaMetrics will search for the nearest sample within the
|
||
// [time-5m..time] interval.
|
||
got := q.PrometheusAPIV1Query(t, "foo_bar", apptest.QueryOpts{Time: "2022-05-10T08:03:00.000Z", Step: "5m"})
|
||
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data":{"result":[{"metric":{"__name__":"foo_bar"},"value":[1652169780,"3"]}]}}`)
|
||
opt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
|
||
if diff := cmp.Diff(want, got, opt); diff != "" {
|
||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||
}
|
||
|
||
// Get the value of the foo_bar time series at 2022-05-10T08:18:00Z with the
|
||
// step of 1m and timeout 5s. There is no sample at this timestamp.
|
||
// Therefore, VictoriaMetrics will search for the nearest sample within the
|
||
// [time-1m..time] interval. Since the nearest sample is 2m away and the
|
||
// step is 1m, then the VictoriaMetrics must return empty response.
|
||
got = q.PrometheusAPIV1Query(t, "foo_bar", apptest.QueryOpts{Time: "2022-05-10T08:18:00.000Z", Step: "1m"})
|
||
if len(got.Data.Result) > 0 {
|
||
t.Errorf("unexpected response: got non-empty result, want empty result:\n%v", got)
|
||
}
|
||
}
|
||
|
||
// testRangeQuery verifies the statements made in the `Range query` section of
|
||
// the VictoriaMetrics documentation. See:
|
||
// https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query
|
||
func testRangeQuery(t *testing.T, q apptest.PrometheusQuerier) {
|
||
f := func(start, end, step string, wantSamples []*apptest.Sample) {
|
||
t.Helper()
|
||
|
||
got := q.PrometheusAPIV1QueryRange(t, "foo_bar", apptest.QueryOpts{Start: start, End: end, Step: step})
|
||
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "foo_bar"}, "values": []}]}}`)
|
||
want.Data.Result[0].Samples = wantSamples
|
||
opt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
|
||
if diff := cmp.Diff(want, got, opt); diff != "" {
|
||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||
}
|
||
}
|
||
|
||
// Verify the statement that the query result for
|
||
// [2022-05-10T07:59:00Z..2022-05-10T08:17:00Z] time range and 1m step will
|
||
// contain 17 points.
|
||
f("2022-05-10T07:59:00.000Z", "2022-05-10T08:17:00.000Z", "1m", []*apptest.Sample{
|
||
// Sample for 2022-05-10T07:59:00Z is missing because the time series has
|
||
// samples only starting from 8:00.
|
||
apptest.NewSample(t, "2022-05-10T08:00:00Z", 1),
|
||
apptest.NewSample(t, "2022-05-10T08:01:00Z", 2),
|
||
apptest.NewSample(t, "2022-05-10T08:02:00Z", 3),
|
||
apptest.NewSample(t, "2022-05-10T08:03:00Z", 3),
|
||
apptest.NewSample(t, "2022-05-10T08:04:00Z", 5),
|
||
apptest.NewSample(t, "2022-05-10T08:05:00Z", 5),
|
||
apptest.NewSample(t, "2022-05-10T08:06:00Z", 5.5),
|
||
apptest.NewSample(t, "2022-05-10T08:07:00Z", 5.5),
|
||
apptest.NewSample(t, "2022-05-10T08:08:00Z", 4),
|
||
apptest.NewSample(t, "2022-05-10T08:09:00Z", 4),
|
||
// Sample for 2022-05-10T08:10:00Z is missing because there is no sample
|
||
// within the [8:10 - 1m .. 8:10] interval.
|
||
apptest.NewSample(t, "2022-05-10T08:11:00Z", 3.5),
|
||
apptest.NewSample(t, "2022-05-10T08:12:00Z", 3.25),
|
||
apptest.NewSample(t, "2022-05-10T08:13:00Z", 3),
|
||
apptest.NewSample(t, "2022-05-10T08:14:00Z", 2),
|
||
apptest.NewSample(t, "2022-05-10T08:15:00Z", 1),
|
||
apptest.NewSample(t, "2022-05-10T08:16:00Z", 4),
|
||
apptest.NewSample(t, "2022-05-10T08:17:00Z", 4),
|
||
})
|
||
|
||
// Verify the statement that a query is executed at start, start+step,
|
||
// start+2*step, …, step+N*step timestamps, where N is the whole number
|
||
// of steps that fit between start and end.
|
||
f("2022-05-10T08:00:01.000Z", "2022-05-10T08:02:00.000Z", "1m", []*apptest.Sample{
|
||
apptest.NewSample(t, "2022-05-10T08:00:01Z", 1),
|
||
apptest.NewSample(t, "2022-05-10T08:01:01Z", 2),
|
||
})
|
||
|
||
// Verify the statement that a query is executed at start, start+step,
|
||
// start+2*step, …, end timestamps, when end = start + N*step.
|
||
f("2022-05-10T08:00:00.000Z", "2022-05-10T08:02:00.000Z", "1m", []*apptest.Sample{
|
||
apptest.NewSample(t, "2022-05-10T08:00:00Z", 1),
|
||
apptest.NewSample(t, "2022-05-10T08:01:00Z", 2),
|
||
apptest.NewSample(t, "2022-05-10T08:02:00Z", 3),
|
||
})
|
||
|
||
// If the step isn’t set, then it defaults to 5m (5 minutes).
|
||
f("2022-05-10T07:59:00.000Z", "2022-05-10T08:17:00.000Z", "", []*apptest.Sample{
|
||
// Sample for 2022-05-10T07:59:00Z is missing because the time series has
|
||
// samples only starting from 8:00.
|
||
apptest.NewSample(t, "2022-05-10T08:04:00Z", 5),
|
||
apptest.NewSample(t, "2022-05-10T08:09:00Z", 4),
|
||
apptest.NewSample(t, "2022-05-10T08:14:00Z", 2),
|
||
})
|
||
}
|
||
|
||
// testRangeQueryIsEquivalentToManyInstantQueries verifies the statement made in
|
||
// the `Range query` section of the VictoriaMetrics documentation that a range
|
||
// query is actually an instant query executed 1 + (start-end)/step times on the
|
||
// time range from start to end. The only difference is that instant queries
|
||
// will not produce ephemeral points.
|
||
//
|
||
// See: https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query
|
||
func testRangeQueryIsEquivalentToManyInstantQueries(t *testing.T, q apptest.PrometheusQuerier) {
|
||
f := func(timestamp string, want *apptest.Sample) {
|
||
t.Helper()
|
||
|
||
gotInstant := q.PrometheusAPIV1Query(t, "foo_bar", apptest.QueryOpts{Time: timestamp, Step: "1m"})
|
||
if want == nil {
|
||
if got, want := len(gotInstant.Data.Result), 0; got != want {
|
||
t.Errorf("unexpected instant result size: got %d, want %d", got, want)
|
||
}
|
||
} else {
|
||
got := gotInstant.Data.Result[0].Sample
|
||
if diff := cmp.Diff(want, got); diff != "" {
|
||
t.Errorf("unexpected instant sample (-want, +got):\n%s", diff)
|
||
}
|
||
}
|
||
}
|
||
|
||
rangeRes := q.PrometheusAPIV1QueryRange(t, "foo_bar", apptest.QueryOpts{
|
||
Start: "2022-05-10T07:59:00.000Z",
|
||
End: "2022-05-10T08:17:00.000Z",
|
||
Step: "1m",
|
||
})
|
||
rangeSamples := rangeRes.Data.Result[0].Samples
|
||
|
||
f("2022-05-10T07:59:00.000Z", nil)
|
||
f("2022-05-10T08:00:00.000Z", rangeSamples[0])
|
||
f("2022-05-10T08:01:00.000Z", rangeSamples[1])
|
||
f("2022-05-10T08:02:00.000Z", rangeSamples[2])
|
||
f("2022-05-10T08:03:00.000Z", nil)
|
||
f("2022-05-10T08:04:00.000Z", rangeSamples[4])
|
||
f("2022-05-10T08:05:00.000Z", nil)
|
||
f("2022-05-10T08:06:00.000Z", rangeSamples[6])
|
||
f("2022-05-10T08:07:00.000Z", rangeSamples[7])
|
||
f("2022-05-10T08:08:00.000Z", rangeSamples[8])
|
||
f("2022-05-10T08:09:00.000Z", nil)
|
||
f("2022-05-10T08:10:00.000Z", nil)
|
||
f("2022-05-10T08:11:00.000Z", rangeSamples[10])
|
||
f("2022-05-10T08:12:00.000Z", rangeSamples[11])
|
||
f("2022-05-10T08:13:00.000Z", rangeSamples[12])
|
||
f("2022-05-10T08:14:00.000Z", rangeSamples[13])
|
||
f("2022-05-10T08:15:00.000Z", rangeSamples[14])
|
||
f("2022-05-10T08:16:00.000Z", rangeSamples[15])
|
||
f("2022-05-10T08:17:00.000Z", nil)
|
||
}
|
||
|
||
func TestSingleMillisecondPrecisionInInstantQueries(t *testing.T) {
|
||
tc := apptest.NewTestCase(t)
|
||
defer tc.Stop()
|
||
|
||
sut := tc.MustStartDefaultVmsingle()
|
||
|
||
testMillisecondPrecisionInInstantQueries(tc, sut)
|
||
}
|
||
|
||
func TestClusterMillisecondPrecisionInInstantQueries(t *testing.T) {
|
||
tc := apptest.NewTestCase(t)
|
||
defer tc.Stop()
|
||
|
||
sut := tc.MustStartDefaultCluster()
|
||
|
||
testMillisecondPrecisionInInstantQueries(tc, sut)
|
||
}
|
||
|
||
func testMillisecondPrecisionInInstantQueries(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier) {
|
||
t := tc.T()
|
||
|
||
type opts struct {
|
||
query string
|
||
qtime string
|
||
step string
|
||
wantMetric map[string]string
|
||
wantSample *apptest.Sample
|
||
wantSamples []*apptest.Sample
|
||
}
|
||
f := func(sut apptest.PrometheusQuerier, opts *opts) {
|
||
t.Helper()
|
||
wantResult := []*apptest.QueryResult{}
|
||
if opts.wantMetric != nil && (opts.wantSample != nil || len(opts.wantSamples) > 0) {
|
||
wantResult = append(wantResult, &apptest.QueryResult{
|
||
Metric: opts.wantMetric,
|
||
Sample: opts.wantSample,
|
||
Samples: opts.wantSamples,
|
||
})
|
||
}
|
||
tc.Assert(&apptest.AssertOptions{
|
||
Msg: "unexpected /api/v1/query response",
|
||
Got: func() any {
|
||
return sut.PrometheusAPIV1Query(t, opts.query, apptest.QueryOpts{
|
||
Time: opts.qtime,
|
||
Step: opts.step,
|
||
})
|
||
},
|
||
Want: &apptest.PrometheusAPIV1QueryResponse{Data: &apptest.QueryData{Result: wantResult}},
|
||
CmpOpts: []cmp.Option{
|
||
cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
|
||
},
|
||
})
|
||
}
|
||
|
||
sut.PrometheusAPIV1ImportPrometheus(t, []string{
|
||
`series1{label="foo"} 10 1707123456700`, // 2024-02-05T08:57:36.700Z
|
||
`series1{label="foo"} 20 1707123456800`, // 2024-02-05T08:57:36.800Z
|
||
}, apptest.QueryOpts{})
|
||
sut.ForceFlush(t)
|
||
|
||
// Verify that both points were created correctly. Fetch both points by
|
||
// passing a duration into the instant query: the difference between the two
|
||
// timestamps, plus 1ms (to include both points), so 101ms:
|
||
f(sut, &opts{
|
||
query: "series1[101ms]",
|
||
qtime: "1707123456800", // 2024-02-05T08:57:36.800Z
|
||
wantMetric: map[string]string{"__name__": "series1", "label": "foo"},
|
||
wantSamples: []*apptest.Sample{
|
||
{Timestamp: 1707123456700, Value: 10},
|
||
{Timestamp: 1707123456800, Value: 20},
|
||
},
|
||
})
|
||
|
||
// Search the first point at its own timestamp with step 1ms.
|
||
f(sut, &opts{
|
||
query: "series1",
|
||
qtime: "1707123456700", // 2024-02-05T08:57:36.700Z
|
||
step: "1ms",
|
||
wantMetric: map[string]string{"__name__": "series1", "label": "foo"},
|
||
wantSample: &apptest.Sample{Timestamp: 1707123456700, Value: 10},
|
||
})
|
||
|
||
// Search the first point at 1ms past its own timestamp.
|
||
// Expect empty result because the search interval (qtime-step, qtime]
|
||
// excludes the timestamp of the first point.
|
||
f(sut, &opts{
|
||
query: "series1",
|
||
qtime: "1707123456701", // 2024-02-05T08:57:36.701Z
|
||
step: "1ms",
|
||
})
|
||
|
||
// Fetch the last point at its timestamp with step 1ms.
|
||
f(sut, &opts{
|
||
query: "series1",
|
||
qtime: "1707123456800", // 2024-02-05T08:57:36.800Z
|
||
step: "1ms",
|
||
wantMetric: map[string]string{"__name__": "series1", "label": "foo"},
|
||
wantSample: &apptest.Sample{Timestamp: 1707123456800, Value: 20},
|
||
})
|
||
|
||
// Fetch the last point at its timestamp with step 1ms.
|
||
// Expect empty result because the search interval (qtime-step, qtime]
|
||
// excludes the timestamp of the first point.
|
||
f(sut, &opts{
|
||
query: "series1",
|
||
qtime: "1707123456801", // 2024-02-05T08:57:36.801Z
|
||
step: "1ms",
|
||
// wantMetric: map[string]string{"__name__": "series1", "label": "foo"},
|
||
// wantSample: &apptest.Sample{Timestamp: 1707123456801, Value: 20},
|
||
})
|
||
|
||
// Insert samples with different dates. The difference in ms between the two
|
||
// timestamps is 4236579304.
|
||
sut.PrometheusAPIV1ImportPrometheus(t, []string{
|
||
`series2{label="foo"} 10 1638564958042`, // 2021-12-03T20:55:58.042Z
|
||
`series2{label="foo"} 20 1642801537346`, // 2022-01-21T21:45:37.346Z
|
||
}, apptest.QueryOpts{})
|
||
sut.ForceFlush(t)
|
||
|
||
// Both Prometheus and VictoriaMetrics exclude the leftmost millisecond,
|
||
// thus the following queries must return only one sample.
|
||
f(sut, &opts{
|
||
query: "series2[4236579304ms]",
|
||
qtime: "1642801537346",
|
||
step: "1ms",
|
||
wantMetric: map[string]string{"__name__": "series2", "label": "foo"},
|
||
wantSamples: []*apptest.Sample{
|
||
{Timestamp: 1642801537346, Value: 20},
|
||
},
|
||
})
|
||
f(sut, &opts{
|
||
query: "count_over_time(series2[4236579304ms])",
|
||
qtime: "1642801537346", // 2022-01-21T21:45:37.346Z
|
||
step: "1ms",
|
||
wantMetric: map[string]string{"label": "foo"},
|
||
wantSample: &apptest.Sample{Timestamp: 1642801537346, Value: 1},
|
||
})
|
||
|
||
// Adding 1ms to the duration (4236579305ms) causes queries to return 2
|
||
// samples.
|
||
f(sut, &opts{
|
||
query: "series2[4236579305ms]",
|
||
qtime: "1642801537346",
|
||
step: "1ms",
|
||
wantMetric: map[string]string{"__name__": "series2", "label": "foo"},
|
||
wantSamples: []*apptest.Sample{
|
||
{Timestamp: 1638564958042, Value: 10}, // 2021-12-03T20:55:58.042Z
|
||
{Timestamp: 1642801537346, Value: 20},
|
||
},
|
||
})
|
||
f(sut, &opts{
|
||
query: "count_over_time(series2[4236579305ms])",
|
||
qtime: "1642801537346", // 2022-01-21T21:45:37.346Z
|
||
step: "1ms",
|
||
wantMetric: map[string]string{"label": "foo"},
|
||
wantSample: &apptest.Sample{Timestamp: 1642801537346, Value: 2},
|
||
})
|
||
}
|