2024-11-07 12:24:44 +00:00
|
|
|
package apptest
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2024-11-26 18:03:56 +00:00
|
|
|
"net/url"
|
2024-11-20 15:30:55 +00:00
|
|
|
"slices"
|
2024-11-07 12:24:44 +00:00
|
|
|
"strconv"
|
2024-11-20 15:30:55 +00:00
|
|
|
"strings"
|
2024-11-07 12:24:44 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
2024-11-21 18:39:17 +00:00
|
|
|
|
|
|
|
pb "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
2024-11-07 12:24:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// PrometheusQuerier contains methods available to Prometheus-like HTTP API for Querying
|
|
|
|
type PrometheusQuerier interface {
|
2024-11-26 18:03:56 +00:00
|
|
|
PrometheusAPIV1Export(t *testing.T, query string, opts QueryOpts) *PrometheusAPIV1QueryResponse
|
|
|
|
PrometheusAPIV1Query(t *testing.T, query string, opts QueryOpts) *PrometheusAPIV1QueryResponse
|
|
|
|
PrometheusAPIV1QueryRange(t *testing.T, query string, opts QueryOpts) *PrometheusAPIV1QueryResponse
|
2024-11-07 12:24:44 +00:00
|
|
|
PrometheusAPIV1Series(t *testing.T, matchQuery string, opts QueryOpts) *PrometheusAPIV1SeriesResponse
|
|
|
|
}
|
|
|
|
|
|
|
|
// PrometheusWriter contains methods available to Prometheus-like HTTP API for Writing new data
|
|
|
|
type PrometheusWriter interface {
|
2024-11-21 18:39:17 +00:00
|
|
|
PrometheusAPIV1Write(t *testing.T, records []pb.TimeSeries, opts QueryOpts)
|
2024-11-07 12:24:44 +00:00
|
|
|
PrometheusAPIV1ImportPrometheus(t *testing.T, records []string, opts QueryOpts)
|
|
|
|
}
|
|
|
|
|
2024-11-20 15:30:55 +00:00
|
|
|
// StorageFlusher defines a method that forces the flushing of data inserted
|
|
|
|
// into the storage, so it becomes available for searching immediately.
|
|
|
|
type StorageFlusher interface {
|
|
|
|
ForceFlush(t *testing.T)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PrometheusWriteQuerier encompasses the methods for writing, flushing and
|
|
|
|
// querying the data.
|
|
|
|
type PrometheusWriteQuerier interface {
|
|
|
|
PrometheusWriter
|
|
|
|
PrometheusQuerier
|
|
|
|
StorageFlusher
|
|
|
|
}
|
|
|
|
|
2024-11-07 12:24:44 +00:00
|
|
|
// QueryOpts contains various params used for querying or ingesting data
|
|
|
|
type QueryOpts struct {
|
2024-11-26 18:03:56 +00:00
|
|
|
Tenant string
|
|
|
|
Timeout string
|
|
|
|
Start string
|
|
|
|
End string
|
|
|
|
Time string
|
|
|
|
Step string
|
|
|
|
ExtraFilters []string
|
|
|
|
ExtraLabels []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (qos *QueryOpts) asURLValues() url.Values {
|
|
|
|
uv := make(url.Values)
|
|
|
|
addNonEmpty := func(name string, values ...string) {
|
|
|
|
for _, value := range values {
|
|
|
|
if len(value) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
uv.Add(name, value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
addNonEmpty("start", qos.Start)
|
|
|
|
addNonEmpty("end", qos.End)
|
|
|
|
addNonEmpty("time", qos.Time)
|
|
|
|
addNonEmpty("step", qos.Step)
|
|
|
|
addNonEmpty("timeout", qos.Timeout)
|
|
|
|
addNonEmpty("extra_label", qos.ExtraLabels...)
|
|
|
|
addNonEmpty("extra_filters", qos.ExtraFilters...)
|
|
|
|
|
|
|
|
return uv
|
|
|
|
}
|
|
|
|
|
|
|
|
// getTenant returns tenant with optional default value
|
|
|
|
func (qos *QueryOpts) getTenant() string {
|
|
|
|
if qos.Tenant == "" {
|
|
|
|
return "0"
|
|
|
|
}
|
|
|
|
return qos.Tenant
|
2024-11-07 12:24:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// PrometheusAPIV1QueryResponse is an inmemory representation of the
|
|
|
|
// /prometheus/api/v1/query or /prometheus/api/v1/query_range response.
|
|
|
|
type PrometheusAPIV1QueryResponse struct {
|
|
|
|
Status string
|
|
|
|
Data *QueryData
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewPrometheusAPIV1QueryResponse is a test helper function that creates a new
|
|
|
|
// instance of PrometheusAPIV1QueryResponse by unmarshalling a json string.
|
|
|
|
func NewPrometheusAPIV1QueryResponse(t *testing.T, s string) *PrometheusAPIV1QueryResponse {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
res := &PrometheusAPIV1QueryResponse{}
|
|
|
|
if err := json.Unmarshal([]byte(s), res); err != nil {
|
2024-11-26 18:03:56 +00:00
|
|
|
t.Fatalf("could not unmarshal query response data=\n%s\n: %v", string(s), err)
|
2024-11-07 12:24:44 +00:00
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
// QueryData holds the query result along with its type.
|
|
|
|
type QueryData struct {
|
|
|
|
ResultType string
|
|
|
|
Result []*QueryResult
|
|
|
|
}
|
|
|
|
|
|
|
|
// QueryResult holds the metric name (in the form of label name-value
|
|
|
|
// collection) and its samples.
|
|
|
|
//
|
|
|
|
// Sample or Samples field is set for /prometheus/api/v1/query or
|
|
|
|
// /prometheus/api/v1/query_range response respectively.
|
|
|
|
type QueryResult struct {
|
|
|
|
Metric map[string]string
|
|
|
|
Sample *Sample `json:"value"`
|
|
|
|
Samples []*Sample `json:"values"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sample is a timeseries value at a given timestamp.
|
|
|
|
type Sample struct {
|
|
|
|
Timestamp int64
|
|
|
|
Value float64
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewSample is a test helper function that creates a new sample out of time in
|
|
|
|
// RFC3339 format and a value.
|
|
|
|
func NewSample(t *testing.T, timeStr string, value float64) *Sample {
|
|
|
|
parsedTime, err := time.Parse(time.RFC3339, timeStr)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("could not parse RFC3339 time %q: %v", timeStr, err)
|
|
|
|
}
|
|
|
|
return &Sample{parsedTime.Unix(), value}
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalJSON populates the sample fields from a JSON string.
|
|
|
|
func (s *Sample) UnmarshalJSON(b []byte) error {
|
|
|
|
var (
|
2024-11-26 18:03:56 +00:00
|
|
|
ts float64
|
2024-11-07 12:24:44 +00:00
|
|
|
v string
|
|
|
|
)
|
|
|
|
raw := []any{&ts, &v}
|
|
|
|
if err := json.Unmarshal(b, &raw); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if got, want := len(raw), 2; got != want {
|
|
|
|
return fmt.Errorf("unexpected number of fields: got %d, want %d (raw sample: %s)", got, want, string(b))
|
|
|
|
}
|
2024-11-26 18:03:56 +00:00
|
|
|
s.Timestamp = int64(ts)
|
2024-11-07 12:24:44 +00:00
|
|
|
var err error
|
|
|
|
s.Value, err = strconv.ParseFloat(v, 64)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not parse sample value %q: %w", v, err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PrometheusAPIV1SeriesResponse is an inmemory representation of the
|
|
|
|
// /prometheus/api/v1/series response.
|
|
|
|
type PrometheusAPIV1SeriesResponse struct {
|
|
|
|
Status string
|
|
|
|
IsPartial bool
|
|
|
|
Data []map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewPrometheusAPIV1SeriesResponse is a test helper function that creates a new
|
|
|
|
// instance of PrometheusAPIV1SeriesResponse by unmarshalling a json string.
|
|
|
|
func NewPrometheusAPIV1SeriesResponse(t *testing.T, s string) *PrometheusAPIV1SeriesResponse {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
res := &PrometheusAPIV1SeriesResponse{}
|
|
|
|
if err := json.Unmarshal([]byte(s), res); err != nil {
|
2024-11-26 18:03:56 +00:00
|
|
|
t.Fatalf("could not unmarshal series response data:\n%s\n err: %v", string(s), err)
|
2024-11-07 12:24:44 +00:00
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
2024-11-20 15:30:55 +00:00
|
|
|
|
|
|
|
// Sort sorts the response data.
|
|
|
|
func (r *PrometheusAPIV1SeriesResponse) Sort() {
|
|
|
|
str := func(m map[string]string) string {
|
|
|
|
s := []string{}
|
|
|
|
for k, v := range m {
|
|
|
|
s = append(s, k+v)
|
|
|
|
}
|
|
|
|
slices.Sort(s)
|
|
|
|
return strings.Join(s, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
slices.SortFunc(r.Data, func(a, b map[string]string) int {
|
|
|
|
return strings.Compare(str(a), str(b))
|
|
|
|
})
|
|
|
|
}
|