Prometheus: Introduce prometheus backend library (#83952)

* Move files to prometheus-library

* refactor core prometheus to use prometheus-library

* modify client transport options

* mock

* have a type

* import aliases

* rename

* call the right method

* remove unrelated test from the library

* update codeowners

* go work sync

* update go.work.sum

* make swagger-clean && make openapi3-gen

* add promlib to makefile

* remove clilogger

* Export the function

* update unit test

* add prometheus_test.go

* fix mock type

* use mapUtil from grafana-plugin-sdk-go
This commit is contained in:
ismail simsek
2024-03-11 17:22:33 +01:00
committed by GitHub
parent cfc7ea92da
commit 3fb6319d1b
114 changed files with 672 additions and 340 deletions
+87
View File
@@ -0,0 +1,87 @@
package exemplar
import (
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
var _ data.Framer = (*Framer)(nil)
type Framer struct {
frames data.Frames
sampler Sampler
labelTracker LabelTracker
meta *data.FrameMeta
refID string
}
func NewFramer(sampler Sampler, labelTracker LabelTracker) *Framer {
return &Framer{
frames: data.Frames{},
sampler: sampler,
labelTracker: labelTracker,
}
}
func (f *Framer) SetMeta(meta *data.FrameMeta) {
f.meta = meta
}
func (f *Framer) SetRefID(refID string) {
f.refID = refID
}
func (f *Framer) AddFrame(frame *data.Frame) {
f.frames = append(f.frames, frame)
}
func (f *Framer) Frames() (data.Frames, error) {
exemplars := f.sampler.Sample()
f.sampler.Reset()
if len(exemplars) == 0 {
return f.frames, nil
}
// the new exemplar frame will be a single frame in long format
// with a timestamp, metric value, and one or more label fields
exemplarFrame := data.NewFrame("exemplar")
exemplarFrame.RefID = f.refID
exemplarFrame.Meta = f.meta
// init the fields for the new exemplar frame
timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, make([]time.Time, 0, len(exemplars)))
valueField := data.NewField(data.TimeSeriesValueFieldName, nil, make([]float64, 0, len(exemplars)))
exemplarFrame.Fields = append(exemplarFrame.Fields, timeField, valueField)
labelNames := f.labelTracker.GetNames()
exemplarLabels := make(map[string]string, len(labelNames))
for _, labelName := range labelNames {
exemplarFrame.Fields = append(exemplarFrame.Fields, data.NewField(labelName, nil, make([]string, 0, len(exemplars))))
}
// add the sampled exemplars to the new exemplar frame
for _, b := range exemplars {
// Fill labels map with default values
for _, n := range labelNames {
exemplarLabels[n] = b.SeriesLabels[n]
}
// Enter corresponding label values from exemplar fields
for _, bf := range b.Fields {
if _, exists := exemplarLabels[bf.Name]; exists {
exemplarLabels[bf.Name] = bf.CopyAt(b.RowIdx).(string)
}
}
timeField.Append(b.Timestamp)
valueField.Append(b.Value)
for i, labelName := range labelNames {
colIdx := i + 2 // +2 to skip time and value fields
exemplarFrame.Fields[colIdx].Append(exemplarLabels[labelName])
}
}
f.frames = append(f.frames, exemplarFrame)
return f.frames, nil
}
+52
View File
@@ -0,0 +1,52 @@
package exemplar
import (
"sort"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
var _ LabelTracker = (*labelTracker)(nil)
type LabelTracker interface {
Add(labels map[string]string)
AddFields(fields []*data.Field)
GetNames() []string
}
type labelTracker struct {
labelSet map[string]struct{}
}
func NewLabelTracker() LabelTracker {
return &labelTracker{
labelSet: map[string]struct{}{},
}
}
// Add saves label names that haven't been seen before
// so that they can be used to build the label fields in the exemplar frame
func (l *labelTracker) Add(labels map[string]string) {
for k := range labels {
l.labelSet[k] = struct{}{}
}
}
// AddFields saves field names so that they can be used to build the label fields in the exemplar frame.
func (l *labelTracker) AddFields(fields []*data.Field) {
for _, f := range fields {
l.labelSet[f.Name] = struct{}{}
}
}
// GetNames returns sorted unique label names
func (l *labelTracker) GetNames() []string {
labelNames := make([]string, 0, len(l.labelSet))
for k := range l.labelSet {
labelNames = append(labelNames, k)
}
sort.SliceStable(labelNames, func(i, j int) bool {
return labelNames[i] < labelNames[j]
})
return labelNames
}
+46
View File
@@ -0,0 +1,46 @@
package exemplar
import (
"sort"
"time"
"github.com/grafana/grafana/pkg/promlib/models"
)
type Sampler interface {
Add(models.Exemplar)
SetStep(time.Duration)
Sample() []models.Exemplar
Reset()
}
var _ Sampler = (*NoOpSampler)(nil)
type NoOpSampler struct {
exemplars []models.Exemplar
}
func NewNoOpSampler() Sampler {
return &NoOpSampler{
exemplars: []models.Exemplar{},
}
}
func (e *NoOpSampler) Add(ex models.Exemplar) {
e.exemplars = append(e.exemplars, ex)
}
func (e *NoOpSampler) SetStep(time.Duration) {
// noop
}
func (e *NoOpSampler) Sample() []models.Exemplar {
sort.SliceStable(e.exemplars, func(i, j int) bool {
return e.exemplars[i].Timestamp.Before(e.exemplars[j].Timestamp)
})
return e.exemplars
}
func (e *NoOpSampler) Reset() {
e.exemplars = []models.Exemplar{}
}
@@ -0,0 +1,93 @@
package exemplar
import (
"math"
"sort"
"time"
"github.com/grafana/grafana/pkg/promlib/models"
)
type StandardDeviationSampler struct {
step time.Duration
buckets map[time.Time][]models.Exemplar
count int
mean float64
m2 float64
}
func NewStandardDeviationSampler() Sampler {
return &StandardDeviationSampler{
buckets: map[time.Time][]models.Exemplar{},
}
}
func (e *StandardDeviationSampler) SetStep(step time.Duration) {
e.step = step
}
func (e *StandardDeviationSampler) Add(ex models.Exemplar) {
bucketTs := models.AlignTimeRange(ex.Timestamp, e.step, 0)
e.updateAggregations(ex.Value)
if _, exists := e.buckets[bucketTs]; !exists {
e.buckets[bucketTs] = []models.Exemplar{ex}
return
}
e.buckets[bucketTs] = append(e.buckets[bucketTs], ex)
}
// updateAggregations uses Welford's online algorithm for calculating the mean and variance
// https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
func (e *StandardDeviationSampler) updateAggregations(val float64) {
e.count++
delta := val - e.mean
e.mean += delta / float64(e.count)
delta2 := val - e.mean
e.m2 += delta * delta2
}
// standardDeviation calculates the amount of variation in the data
// https://en.wikipedia.org/wiki/Standard_deviation
func (e *StandardDeviationSampler) standardDeviation() float64 {
if e.count < 2 {
return 0
}
return math.Sqrt(e.m2 / float64(e.count-1))
}
func (e *StandardDeviationSampler) Sample() []models.Exemplar {
exemplars := make([]models.Exemplar, 0, len(e.buckets))
for _, b := range e.buckets {
// sort by value in descending order
sort.SliceStable(b, func(i, j int) bool {
return b[i].Value > b[j].Value
})
sampled := []models.Exemplar{}
for _, ex := range b {
if len(sampled) == 0 {
sampled = append(sampled, ex)
continue
}
// only sample values at least 2 standard deviation distance to previously taken value
prev := sampled[len(sampled)-1]
if e.standardDeviation() != 0.0 && prev.Value-ex.Value > e.standardDeviation()*2.0 {
sampled = append(sampled, ex)
}
}
exemplars = append(exemplars, sampled...)
}
sort.SliceStable(exemplars, func(i, j int) bool {
return exemplars[i].Timestamp.Before(exemplars[j].Timestamp)
})
return exemplars
}
func (e *StandardDeviationSampler) Reset() {
e.step = 0
e.buckets = map[time.Time][]models.Exemplar{}
e.count = 0
e.mean = 0
e.m2 = 0
}
@@ -0,0 +1,28 @@
package exemplar_test
import (
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/grafana/grafana/pkg/promlib/models"
"github.com/grafana/grafana/pkg/promlib/querydata/exemplar"
)
func TestStdDevSampler(t *testing.T) {
sampler := exemplar.NewStandardDeviationSampler().(*exemplar.StandardDeviationSampler)
t.Run("standard deviation sampler", func(t *testing.T) {
tr := models.TimeRange{
Start: time.Unix(0, 0),
End: time.Unix(100000, 0),
}
ex := generateTestExemplars(tr)
sampler.SetStep(600 * time.Second)
for i := 0; i < len(ex); i++ {
sampler.Add(ex[i])
}
framer := exemplar.NewFramer(sampler, exemplar.NewLabelTracker())
experimental.CheckGoldenJSONFramer(t, "testdata", "stddev_sampler", framer, update)
})
}
@@ -0,0 +1,45 @@
package exemplar_test
import (
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/grafana/grafana/pkg/promlib/models"
"github.com/grafana/grafana/pkg/promlib/querydata/exemplar"
)
const update = true
func TestNoOpSampler(t *testing.T) {
sampler := exemplar.NewNoOpSampler().(*exemplar.NoOpSampler)
t.Run("no-op sampler", func(t *testing.T) {
tr := models.TimeRange{
Start: time.Unix(0, 0),
End: time.Unix(2000, 0),
}
ex := generateTestExemplars(tr)
for i := 0; i < len(ex); i++ {
sampler.Add(ex[i])
}
framer := exemplar.NewFramer(sampler, exemplar.NewLabelTracker())
experimental.CheckGoldenJSONFramer(t, "testdata", "noop_sampler", framer, update)
})
}
func generateTestExemplars(tr models.TimeRange) []models.Exemplar {
exemplars := []models.Exemplar{}
next := tr.Start.UTC()
for {
if next.Equal(tr.End) || next.After(tr.End) {
break
}
exemplars = append(exemplars, models.Exemplar{
Timestamp: next,
Value: float64(next.Unix()),
})
next = next.Add(time.Second).UTC()
}
return exemplars
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,392 @@
// 🌟 This was machine generated. Do not edit. 🌟
//
// Frame[0]
// Name: exemplar
// Dimensions: 2 Fields by 167 Rows
// +-------------------------------+-----------------+
// | Name: Time | Name: Value |
// | Labels: | Labels: |
// | Type: []time.Time | Type: []float64 |
// +-------------------------------+-----------------+
// | 1970-01-01 00:09:59 +0000 UTC | 599 |
// | 1970-01-01 00:19:59 +0000 UTC | 1199 |
// | 1970-01-01 00:29:59 +0000 UTC | 1799 |
// | 1970-01-01 00:39:59 +0000 UTC | 2399 |
// | 1970-01-01 00:49:59 +0000 UTC | 2999 |
// | 1970-01-01 00:59:59 +0000 UTC | 3599 |
// | 1970-01-01 01:09:59 +0000 UTC | 4199 |
// | 1970-01-01 01:19:59 +0000 UTC | 4799 |
// | 1970-01-01 01:29:59 +0000 UTC | 5399 |
// | ... | ... |
// +-------------------------------+-----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
{
"status": 200,
"frames": [
{
"schema": {
"name": "exemplar",
"fields": [
{
"name": "Time",
"type": "time",
"typeInfo": {
"frame": "time.Time"
}
},
{
"name": "Value",
"type": "number",
"typeInfo": {
"frame": "float64"
}
}
]
},
"data": {
"values": [
[
599000,
1199000,
1799000,
2399000,
2999000,
3599000,
4199000,
4799000,
5399000,
5999000,
6599000,
7199000,
7799000,
8399000,
8999000,
9599000,
10199000,
10799000,
11399000,
11999000,
12599000,
13199000,
13799000,
14399000,
14999000,
15599000,
16199000,
16799000,
17399000,
17999000,
18599000,
19199000,
19799000,
20399000,
20999000,
21599000,
22199000,
22799000,
23399000,
23999000,
24599000,
25199000,
25799000,
26399000,
26999000,
27599000,
28199000,
28799000,
29399000,
29999000,
30599000,
31199000,
31799000,
32399000,
32999000,
33599000,
34199000,
34799000,
35399000,
35999000,
36599000,
37199000,
37799000,
38399000,
38999000,
39599000,
40199000,
40799000,
41399000,
41999000,
42599000,
43199000,
43799000,
44399000,
44999000,
45599000,
46199000,
46799000,
47399000,
47999000,
48599000,
49199000,
49799000,
50399000,
50999000,
51599000,
52199000,
52799000,
53399000,
53999000,
54599000,
55199000,
55799000,
56399000,
56999000,
57599000,
58199000,
58799000,
59399000,
59999000,
60599000,
61199000,
61799000,
62399000,
62999000,
63599000,
64199000,
64799000,
65399000,
65999000,
66599000,
67199000,
67799000,
68399000,
68999000,
69599000,
70199000,
70799000,
71399000,
71999000,
72599000,
73199000,
73799000,
74399000,
74999000,
75599000,
76199000,
76799000,
77399000,
77999000,
78599000,
79199000,
79799000,
80399000,
80999000,
81599000,
82199000,
82799000,
83399000,
83999000,
84599000,
85199000,
85799000,
86399000,
86999000,
87599000,
88199000,
88799000,
89399000,
89999000,
90599000,
91199000,
91799000,
92399000,
92999000,
93599000,
94199000,
94799000,
95399000,
95999000,
96599000,
97199000,
97799000,
98399000,
98999000,
99599000,
99999000
],
[
599,
1199,
1799,
2399,
2999,
3599,
4199,
4799,
5399,
5999,
6599,
7199,
7799,
8399,
8999,
9599,
10199,
10799,
11399,
11999,
12599,
13199,
13799,
14399,
14999,
15599,
16199,
16799,
17399,
17999,
18599,
19199,
19799,
20399,
20999,
21599,
22199,
22799,
23399,
23999,
24599,
25199,
25799,
26399,
26999,
27599,
28199,
28799,
29399,
29999,
30599,
31199,
31799,
32399,
32999,
33599,
34199,
34799,
35399,
35999,
36599,
37199,
37799,
38399,
38999,
39599,
40199,
40799,
41399,
41999,
42599,
43199,
43799,
44399,
44999,
45599,
46199,
46799,
47399,
47999,
48599,
49199,
49799,
50399,
50999,
51599,
52199,
52799,
53399,
53999,
54599,
55199,
55799,
56399,
56999,
57599,
58199,
58799,
59399,
59999,
60599,
61199,
61799,
62399,
62999,
63599,
64199,
64799,
65399,
65999,
66599,
67199,
67799,
68399,
68999,
69599,
70199,
70799,
71399,
71999,
72599,
73199,
73799,
74399,
74999,
75599,
76199,
76799,
77399,
77999,
78599,
79199,
79799,
80399,
80999,
81599,
82199,
82799,
83399,
83999,
84599,
85199,
85799,
86399,
86999,
87599,
88199,
88799,
89399,
89999,
90599,
91199,
91799,
92399,
92999,
93599,
94199,
94799,
95399,
95999,
96599,
97199,
97799,
98399,
98999,
99599,
99999
]
]
}
}
]
}
+153
View File
@@ -0,0 +1,153 @@
package querydata_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/promlib/models"
)
// when memory-profiling this benchmark, these commands are recommended:
// - go test -benchmem -run=^$ -bench ^BenchmarkExemplarJson$ github.com/grafana/grafana/pkg/promlib/querydata -memprofile memprofile.out -count 6 | tee old.txt
// - go tool pprof -http=localhost:6061 memprofile.out
func BenchmarkExemplarJson(b *testing.B) {
queryFileName := filepath.Join("../testdata", "exemplar.query.json")
query, err := loadStoredQuery(queryFileName)
require.NoError(b, err)
responseFileName := filepath.Join("../testdata", "exemplar.result.json")
// nolint:gosec
// We can ignore the gosec G304 warning since this is a test file
responseBytes, err := os.ReadFile(responseFileName)
require.NoError(b, err)
tCtx, err := setup()
require.NoError(b, err)
b.ResetTimer()
for n := 0; n < b.N; n++ {
res := http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(responseBytes)),
}
tCtx.httpProvider.setResponse(&res)
resp, err := tCtx.queryData.Execute(context.Background(), query)
require.NoError(b, err)
for _, r := range resp.Responses {
require.NoError(b, r.Error)
}
}
}
var resp *backend.QueryDataResponse
// when memory-profiling this benchmark, these commands are recommended:
// - go test -benchmem -run=^$ -bench ^BenchmarkRangeJson$ github.com/grafana/grafana/pkg/promlib/querydata -memprofile memprofile.out -count 6 | tee old.txt
// - go tool pprof -http=localhost:6061 memprofile.out
// - benchstat old.txt new.txt
func BenchmarkRangeJson(b *testing.B) {
var (
r *backend.QueryDataResponse
err error
)
body, q := createJsonTestData(1642000000, 1, 300, 400)
tCtx, err := setup()
require.NoError(b, err)
b.ResetTimer()
for n := 0; n < b.N; n++ {
res := http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(body)),
}
tCtx.httpProvider.setResponse(&res)
r, err = tCtx.queryData.Execute(context.Background(), q)
require.NoError(b, err)
}
resp = r
}
const nanRate = 0.002
// we build the JSON file from strings,
// it was easier to write it this way.
func makeJsonTestMetric(index int) string {
return fmt.Sprintf(`{"server":"main","category":"maintenance","case":"%v"}`, index)
}
// return a value between -100 and +100, sometimes NaN, in string
func makeJsonTestValue(r *rand.Rand) string {
if r.Float64() < nanRate {
return "NaN"
} else {
return fmt.Sprintf("%f", (r.Float64()*200)-100)
}
}
// create one time-series
func makeJsonTestSeries(start int64, step int64, timestampCount int, r *rand.Rand, seriesIndex int) string {
var values []string
for i := 0; i < timestampCount; i++ {
// create out of order timestamps to test sorting
if seriesIndex == 0 && i%2 == 0 {
continue
}
value := fmt.Sprintf(`[%d,"%v"]`, start+(int64(i)*step), makeJsonTestValue(r))
values = append(values, value)
}
return fmt.Sprintf(`{"metric":%v,"values":[%v]}`, makeJsonTestMetric(seriesIndex), strings.Join(values, ","))
}
func createJsonTestData(start int64, step int64, timestampCount int, seriesCount int) ([]byte, *backend.QueryDataRequest) {
// we use random numbers as values, but they have to be the same numbers
// every time we call this, so we create a random source.
r := rand.New(rand.NewSource(42))
var allSeries []string
for i := 0; i < seriesCount; i++ {
allSeries = append(allSeries, makeJsonTestSeries(start, step, timestampCount, r, i))
}
bytes := []byte(fmt.Sprintf(`{"status":"success","data":{"resultType":"matrix","result":[%v]}}`, strings.Join(allSeries, ",")))
qm := models.QueryModel{
PrometheusQueryProperties: models.PrometheusQueryProperties{
Range: true,
Expr: "test",
},
}
data, err := json.Marshal(&qm)
if err != nil {
panic(err)
}
res := backend.QueryDataRequest{
Queries: []backend.DataQuery{
{
RefID: "A",
TimeRange: backend.TimeRange{
From: time.Unix(start, 0),
To: time.Unix(start+((int64(timestampCount)-1)*step), 0),
},
Interval: time.Second * time.Duration(step),
JSON: data,
},
},
}
return bytes, &res
}
+153
View File
@@ -0,0 +1,153 @@
package querydata_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/promlib/models"
)
var update = true
func TestRangeResponses(t *testing.T) {
tt := []struct {
name string
filepath string
}{
{name: "parse a simple matrix response", filepath: "range_simple"},
{name: "parse a simple matrix response with value missing steps", filepath: "range_missing"},
{name: "parse a matrix response with Infinity", filepath: "range_infinity"},
{name: "parse a matrix response with NaN", filepath: "range_nan"},
{name: "parse a response with legendFormat __auto", filepath: "range_auto"},
}
for _, test := range tt {
queryFileName := filepath.Join("../testdata", test.filepath+".query.json")
responseFileName := filepath.Join("../testdata", test.filepath+".result.json")
goldenFileName := test.filepath + ".result.golden"
t.Run(test.name, goldenScenario(test.name, queryFileName, responseFileName, goldenFileName))
}
}
func TestExemplarResponses(t *testing.T) {
tt := []struct {
name string
filepath string
}{
{name: "parse an exemplar response", filepath: "exemplar"},
}
for _, test := range tt {
queryFileName := filepath.Join("../testdata", test.filepath+".query.json")
responseFileName := filepath.Join("../testdata", test.filepath+".result.json")
goldenFileName := test.filepath + ".result.golden"
t.Run(test.name, goldenScenario(test.name, queryFileName, responseFileName, goldenFileName))
}
}
func goldenScenario(name, queryFileName, responseFileName, goldenFileName string) func(t *testing.T) {
return func(t *testing.T) {
query, err := loadStoredQuery(queryFileName)
require.NoError(t, err)
//nolint:gosec
responseBytes, err := os.ReadFile(responseFileName)
require.NoError(t, err)
result, err := runQuery(responseBytes, query)
require.NoError(t, err)
require.Len(t, result.Responses, 1)
dr, found := result.Responses["A"]
require.True(t, found)
experimental.CheckGoldenJSONResponse(t, "../testdata", goldenFileName, &dr, update)
}
}
// we store the prometheus query data in a json file, here is some minimal code
// to be able to read it back. unfortunately we cannot use the models.Query
// struct here, because it has `time.time` and `time.duration` fields that
// cannot be unmarshalled from JSON automatically.
type storedPrometheusQuery struct {
RefId string
RangeQuery bool
ExemplarQuery bool
Start int64
End int64
Step int64
Expr string
LegendFormat string
}
func loadStoredQuery(fileName string) (*backend.QueryDataRequest, error) {
//nolint:gosec
bytes, err := os.ReadFile(fileName)
if err != nil {
return nil, err
}
var sq storedPrometheusQuery
err = json.Unmarshal(bytes, &sq)
if err != nil {
return nil, err
}
qm := models.QueryModel{
PrometheusQueryProperties: models.PrometheusQueryProperties{
Range: sq.RangeQuery,
Exemplar: sq.ExemplarQuery,
Expr: sq.Expr,
LegendFormat: sq.LegendFormat,
},
CommonQueryProperties: models.CommonQueryProperties{
IntervalMs: sq.Step * 1000,
},
Interval: fmt.Sprintf("%ds", sq.Step),
}
data, err := json.Marshal(&qm)
if err != nil {
return nil, err
}
return &backend.QueryDataRequest{
Queries: []backend.DataQuery{
{
TimeRange: backend.TimeRange{
From: time.Unix(sq.Start, 0),
To: time.Unix(sq.End, 0),
},
RefID: sq.RefId,
Interval: time.Second * time.Duration(sq.Step),
JSON: json.RawMessage(data),
},
},
}, nil
}
func runQuery(response []byte, q *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
tCtx, err := setup()
if err != nil {
return nil, err
}
res := &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(response)),
}
tCtx.httpProvider.setResponse(res)
return tCtx.queryData.Execute(context.Background(), q)
}
+228
View File
@@ -0,0 +1,228 @@
package querydata
import (
"context"
"fmt"
"net/http"
"regexp"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/data/utils/maputil"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
"github.com/grafana/grafana/pkg/promlib/client"
"github.com/grafana/grafana/pkg/promlib/intervalv2"
"github.com/grafana/grafana/pkg/promlib/models"
"github.com/grafana/grafana/pkg/promlib/querydata/exemplar"
"github.com/grafana/grafana/pkg/promlib/utils"
)
const legendFormatAuto = "__auto"
var legendFormatRegexp = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
type ExemplarEvent struct {
Time time.Time
Value float64
Labels map[string]string
}
// QueryData handles querying but different from buffered package uses a custom client instead of default Go Prom
// client.
type QueryData struct {
intervalCalculator intervalv2.Calculator
tracer trace.Tracer
client *client.Client
log log.Logger
ID int64
URL string
TimeInterval string
exemplarSampler func() exemplar.Sampler
}
func New(
httpClient *http.Client,
settings backend.DataSourceInstanceSettings,
plog log.Logger,
) (*QueryData, error) {
jsonData, err := utils.GetJsonData(settings)
if err != nil {
return nil, err
}
httpMethod, _ := maputil.GetStringOptional(jsonData, "httpMethod")
timeInterval, err := maputil.GetStringOptional(jsonData, "timeInterval")
if err != nil {
return nil, err
}
if httpMethod == "" {
httpMethod = http.MethodPost
}
promClient := client.NewClient(httpClient, httpMethod, settings.URL)
// standard deviation sampler is the default for backwards compatibility
exemplarSampler := exemplar.NewStandardDeviationSampler
return &QueryData{
intervalCalculator: intervalv2.NewCalculator(),
tracer: tracing.DefaultTracer(),
log: plog,
client: promClient,
TimeInterval: timeInterval,
ID: settings.ID,
URL: settings.URL,
exemplarSampler: exemplarSampler,
}, nil
}
func (s *QueryData) Execute(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
fromAlert := req.Headers["FromAlert"] == "true"
result := backend.QueryDataResponse{
Responses: backend.Responses{},
}
cfg := backend.GrafanaConfigFromContext(ctx)
hasPromQLScopeFeatureFlag := cfg.FeatureToggles().IsEnabled("promQLScope")
hasPrometheusDataplaneFeatureFlag := cfg.FeatureToggles().IsEnabled("prometheusDataplane")
for _, q := range req.Queries {
query, err := models.Parse(q, s.TimeInterval, s.intervalCalculator, fromAlert, hasPromQLScopeFeatureFlag)
if err != nil {
return &result, err
}
r := s.fetch(ctx, s.client, query, hasPrometheusDataplaneFeatureFlag)
if r == nil {
s.log.FromContext(ctx).Debug("Received nil response from runQuery", "query", query.Expr)
continue
}
result.Responses[q.RefID] = *r
}
return &result, nil
}
func (s *QueryData) fetch(ctx context.Context, client *client.Client, q *models.Query, enablePrometheusDataplane bool) *backend.DataResponse {
traceCtx, end := s.trace(ctx, q)
defer end()
logger := s.log.FromContext(traceCtx)
logger.Debug("Sending query", "start", q.Start, "end", q.End, "step", q.Step, "query", q.Expr)
dr := &backend.DataResponse{
Frames: data.Frames{},
Error: nil,
}
if q.InstantQuery {
res := s.instantQuery(traceCtx, client, q, enablePrometheusDataplane)
dr.Error = res.Error
dr.Frames = res.Frames
dr.Status = res.Status
}
if q.RangeQuery {
res := s.rangeQuery(traceCtx, client, q, enablePrometheusDataplane)
if res.Error != nil {
if dr.Error == nil {
dr.Error = res.Error
} else {
dr.Error = fmt.Errorf("%v %w", dr.Error, res.Error)
}
// When both instant and range are true, we may overwrite the status code.
// To fix this (and other things) they should come in separate http requests.
dr.Status = res.Status
}
dr.Frames = append(dr.Frames, res.Frames...)
}
if q.ExemplarQuery {
res := s.exemplarQuery(traceCtx, client, q, enablePrometheusDataplane)
if res.Error != nil {
// If exemplar query returns error, we want to only log it and
// continue with other results processing
logger.Error("Exemplar query failed", "query", q.Expr, "err", res.Error)
}
dr.Frames = append(dr.Frames, res.Frames...)
}
return dr
}
func (s *QueryData) rangeQuery(ctx context.Context, c *client.Client, q *models.Query, enablePrometheusDataplaneFlag bool) backend.DataResponse {
res, err := c.QueryRange(ctx, q)
if err != nil {
return backend.DataResponse{
Error: err,
Status: backend.StatusBadGateway,
}
}
defer func() {
err := res.Body.Close()
if err != nil {
s.log.Warn("Failed to close query range response body", "error", err)
}
}()
return s.parseResponse(ctx, q, res, enablePrometheusDataplaneFlag)
}
func (s *QueryData) instantQuery(ctx context.Context, c *client.Client, q *models.Query, enablePrometheusDataplaneFlag bool) backend.DataResponse {
res, err := c.QueryInstant(ctx, q)
if err != nil {
return backend.DataResponse{
Error: err,
Status: backend.StatusBadGateway,
}
}
// This is only for health check fall back scenario
if res.StatusCode != 200 && q.RefId == "__healthcheck__" {
return backend.DataResponse{
Error: fmt.Errorf(res.Status),
}
}
defer func() {
err := res.Body.Close()
if err != nil {
s.log.Warn("Failed to close response body", "error", err)
}
}()
return s.parseResponse(ctx, q, res, enablePrometheusDataplaneFlag)
}
func (s *QueryData) exemplarQuery(ctx context.Context, c *client.Client, q *models.Query, enablePrometheusDataplaneFlag bool) backend.DataResponse {
res, err := c.QueryExemplars(ctx, q)
if err != nil {
return backend.DataResponse{
Error: err,
}
}
defer func() {
err := res.Body.Close()
if err != nil {
s.log.Warn("Failed to close response body", "error", err)
}
}()
return s.parseResponse(ctx, q, res, enablePrometheusDataplaneFlag)
}
func (s *QueryData) trace(ctx context.Context, q *models.Query) (context.Context, func()) {
return utils.StartTrace(ctx, s.tracer, "datasource.prometheus",
attribute.String("expr", q.Expr),
attribute.Int64("start_unixnano", q.Start.UnixNano()),
attribute.Int64("stop_unixnano", q.End.UnixNano()),
)
}
+486
View File
@@ -0,0 +1,486 @@
package querydata_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"testing"
"time"
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
p "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/promlib/client"
"github.com/grafana/grafana/pkg/promlib/models"
"github.com/grafana/grafana/pkg/promlib/querydata"
)
func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
t.Run("exemplars response should be sampled and parsed normally", func(t *testing.T) {
t.Skip()
exemplars := []apiv1.ExemplarQueryResult{
{
SeriesLabels: p.LabelSet{
"__name__": "tns_request_duration_seconds_bucket",
"instance": "app:80",
"job": "tns/app",
},
Exemplars: []apiv1.Exemplar{
{
Labels: p.LabelSet{"traceID": "test1"},
Value: 0.003535405,
Timestamp: p.TimeFromUnixNano(time.Now().Add(-2 * time.Minute).UnixNano()),
},
{
Labels: p.LabelSet{"traceID": "test2"},
Value: 0.005555605,
Timestamp: p.TimeFromUnixNano(time.Now().Add(-4 * time.Minute).UnixNano()),
},
{
Labels: p.LabelSet{"traceID": "test3"},
Value: 0.007545445,
Timestamp: p.TimeFromUnixNano(time.Now().Add(-6 * time.Minute).UnixNano()),
},
{
Labels: p.LabelSet{"traceID": "test4"},
Value: 0.009545445,
Timestamp: p.TimeFromUnixNano(time.Now().Add(-7 * time.Minute).UnixNano()),
},
},
},
}
tctx, err := setup()
require.NoError(t, err)
qm := models.QueryModel{
UtcOffsetSec: 0,
PrometheusQueryProperties: models.PrometheusQueryProperties{
LegendFormat: "legend {{app}}",
Exemplar: true,
},
}
b, err := json.Marshal(&qm)
require.NoError(t, err)
query := backend.DataQuery{
RefID: "A",
JSON: b,
}
res, err := execute(tctx, query, exemplars)
require.NoError(t, err)
// Test fields
require.Len(t, res, 1)
// require.Equal(t, res[0].Name, "exemplar")
require.Equal(t, res[0].Fields[0].Name, "Time")
require.Equal(t, res[0].Fields[1].Name, "Value")
require.Len(t, res[0].Fields, 6)
// Test correct values (sampled to 2)
require.Equal(t, res[0].Fields[1].Len(), 2)
require.Equal(t, res[0].Fields[1].At(0), 0.009545445)
require.Equal(t, res[0].Fields[1].At(1), 0.003535405)
})
t.Run("matrix response should be parsed normally", func(t *testing.T) {
values := []p.SamplePair{
{Value: 1, Timestamp: 1000},
{Value: 2, Timestamp: 2000},
{Value: 3, Timestamp: 3000},
{Value: 4, Timestamp: 4000},
{Value: 5, Timestamp: 5000},
}
result := queryResult{
Type: p.ValMatrix,
Result: p.Matrix{
&p.SampleStream{
Metric: p.Metric{"app": "Application", "tag2": "tag2"},
Values: values,
},
},
}
qm := models.QueryModel{
UtcOffsetSec: 0,
PrometheusQueryProperties: models.PrometheusQueryProperties{
Range: true,
LegendFormat: "legend {{app}}",
},
}
b, err := json.Marshal(&qm)
require.NoError(t, err)
query := backend.DataQuery{
TimeRange: backend.TimeRange{
From: time.Unix(1, 0).UTC(),
To: time.Unix(5, 0).UTC(),
},
JSON: b,
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result)
require.NoError(t, err)
require.Len(t, res, 1)
require.Len(t, res[0].Fields, 2)
require.Len(t, res[0].Fields[0].Labels, 0)
require.Equal(t, "Time", res[0].Fields[0].Name)
require.Len(t, res[0].Fields[1].Labels, 2)
require.Equal(t, "app=Application, tag2=tag2", res[0].Fields[1].Labels.String())
require.Equal(t, "legend Application", res[0].Name)
// Ensure the timestamps are UTC zoned
testValue := res[0].Fields[0].At(0)
require.Equal(t, "UTC", testValue.(time.Time).Location().String())
})
t.Run("matrix response with missed data points should be parsed correctly", func(t *testing.T) {
values := []p.SamplePair{
{Value: 1, Timestamp: 1000},
{Value: 4, Timestamp: 4000},
}
result := queryResult{
Type: p.ValMatrix,
Result: p.Matrix{
&p.SampleStream{
Metric: p.Metric{"app": "Application", "tag2": "tag2"},
Values: values,
},
},
}
qm := models.QueryModel{
UtcOffsetSec: 0,
PrometheusQueryProperties: models.PrometheusQueryProperties{
Range: true,
LegendFormat: "",
},
}
b, err := json.Marshal(&qm)
require.NoError(t, err)
query := backend.DataQuery{
TimeRange: backend.TimeRange{
From: time.Unix(1, 0).UTC(),
To: time.Unix(4, 0).UTC(),
},
JSON: b,
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result)
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, res[0].Fields[0].Len(), 2)
require.Equal(t, time.Unix(1, 0).UTC(), res[0].Fields[0].At(0))
require.Equal(t, time.Unix(4, 0).UTC(), res[0].Fields[0].At(1))
require.Equal(t, res[0].Fields[1].Len(), 2)
require.Equal(t, float64(1), res[0].Fields[1].At(0).(float64))
require.Equal(t, float64(4), res[0].Fields[1].At(1).(float64))
})
t.Run("matrix response with from alerting missed data points should be parsed correctly", func(t *testing.T) {
values := []p.SamplePair{
{Value: 1, Timestamp: 1000},
{Value: 4, Timestamp: 4000},
}
result := queryResult{
Type: p.ValMatrix,
Result: p.Matrix{
&p.SampleStream{
Metric: p.Metric{"app": "Application", "tag2": "tag2"},
Values: values,
},
},
}
qm := models.QueryModel{
UtcOffsetSec: 0,
PrometheusQueryProperties: models.PrometheusQueryProperties{
Range: true,
LegendFormat: "",
},
}
b, err := json.Marshal(&qm)
require.NoError(t, err)
query := backend.DataQuery{
TimeRange: backend.TimeRange{
From: time.Unix(1, 0).UTC(),
To: time.Unix(4, 0).UTC(),
},
JSON: b,
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result)
require.NoError(t, err)
require.Len(t, res, 1)
require.Len(t, res[0].Fields, 2)
require.Len(t, res[0].Fields[0].Labels, 0)
require.Equal(t, res[0].Fields[0].Name, "Time")
require.Len(t, res[0].Fields[1].Labels, 2)
require.Equal(t, res[0].Fields[1].Labels.String(), "app=Application, tag2=tag2")
require.Equal(t, "{app=\"Application\", tag2=\"tag2\"}", res[0].Name)
})
t.Run("matrix response with NaN value should be changed to null", func(t *testing.T) {
result := queryResult{
Type: p.ValMatrix,
Result: p.Matrix{
&p.SampleStream{
Metric: p.Metric{"app": "Application"},
Values: []p.SamplePair{
{Value: p.SampleValue(math.NaN()), Timestamp: 1000},
},
},
},
}
qm := models.QueryModel{
UtcOffsetSec: 0,
PrometheusQueryProperties: models.PrometheusQueryProperties{
Range: true,
LegendFormat: "",
},
}
b, err := json.Marshal(&qm)
require.NoError(t, err)
query := backend.DataQuery{
TimeRange: backend.TimeRange{
From: time.Unix(1, 0).UTC(),
To: time.Unix(4, 0).UTC(),
},
JSON: b,
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result)
require.NoError(t, err)
require.Equal(t, "{app=\"Application\"}", res[0].Name)
require.True(t, math.IsNaN(res[0].Fields[1].At(0).(float64)))
})
t.Run("vector response should be parsed normally", func(t *testing.T) {
qr := queryResult{
Type: p.ValVector,
Result: p.Vector{
&p.Sample{
Metric: p.Metric{"app": "Application", "tag2": "tag2"},
Value: 1,
Timestamp: 123,
},
},
}
qm := models.QueryModel{
UtcOffsetSec: 0,
PrometheusQueryProperties: models.PrometheusQueryProperties{
Instant: true,
LegendFormat: "legend {{app}}",
},
}
b, err := json.Marshal(&qm)
require.NoError(t, err)
query := backend.DataQuery{
JSON: b,
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, qr)
require.NoError(t, err)
require.Len(t, res, 1)
require.Len(t, res[0].Fields, 2)
require.Len(t, res[0].Fields[0].Labels, 0)
require.Equal(t, res[0].Fields[0].Name, "Time")
require.Equal(t, res[0].Fields[0].Name, "Time")
require.Len(t, res[0].Fields[1].Labels, 2)
require.Equal(t, res[0].Fields[1].Labels.String(), "app=Application, tag2=tag2")
require.Equal(t, "legend Application", res[0].Name)
// Ensure the timestamps are UTC zoned
testValue := res[0].Fields[0].At(0)
require.Equal(t, "UTC", testValue.(time.Time).Location().String())
require.Equal(t, int64(123), testValue.(time.Time).UnixMilli())
})
t.Run("scalar response should be parsed normally", func(t *testing.T) {
t.Skip("TODO: implement scalar responses")
qr := queryResult{
Type: p.ValScalar,
Result: &p.Scalar{
Value: 1,
Timestamp: 123,
},
}
qm := models.QueryModel{
UtcOffsetSec: 0,
PrometheusQueryProperties: models.PrometheusQueryProperties{
Instant: true,
LegendFormat: "",
},
}
b, err := json.Marshal(&qm)
require.NoError(t, err)
query := backend.DataQuery{
JSON: b,
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, qr)
require.NoError(t, err)
require.Len(t, res, 1)
require.Len(t, res[0].Fields, 2)
require.Len(t, res[0].Fields[0].Labels, 0)
require.Equal(t, res[0].Fields[0].Name, "Time")
require.Equal(t, "1", res[0].Fields[1].Name)
// Ensure the timestamps are UTC zoned
testValue := res[0].Fields[0].At(0)
require.Equal(t, "UTC", testValue.(time.Time).Location().String())
require.Equal(t, int64(123), testValue.(time.Time).UnixMilli())
})
}
type queryResult struct {
Type p.ValueType `json:"resultType"`
Result any `json:"result"`
}
func executeWithHeaders(tctx *testContext, query backend.DataQuery, qr any, headers map[string]string) (data.Frames, error) {
req := backend.QueryDataRequest{
Queries: []backend.DataQuery{query},
Headers: headers,
}
promRes, err := toAPIResponse(qr)
defer func() {
if err := promRes.Body.Close(); err != nil {
fmt.Println(fmt.Errorf("response body close error: %v", err))
}
}()
if err != nil {
return nil, err
}
tctx.httpProvider.setResponse(promRes)
res, err := tctx.queryData.Execute(context.Background(), &req)
if err != nil {
return nil, err
}
return res.Responses[req.Queries[0].RefID].Frames, nil
}
func execute(tctx *testContext, query backend.DataQuery, qr any) (data.Frames, error) {
return executeWithHeaders(tctx, query, qr, map[string]string{})
}
type apiResponse struct {
Status string `json:"status"`
Data json.RawMessage `json:"data"`
}
func toAPIResponse(d any) (*http.Response, error) {
b, err := json.Marshal(d)
if err != nil {
return nil, err
}
res := apiResponse{
Status: "success",
Data: json.RawMessage(b),
}
raw, err := json.Marshal(&res)
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(raw)),
}, nil
}
type testContext struct {
httpProvider *fakeHttpClientProvider
queryData *querydata.QueryData
}
func setup() (*testContext, error) {
httpProvider := &fakeHttpClientProvider{
opts: httpclient.Options{
Timeouts: &httpclient.DefaultTimeoutOptions,
},
res: &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader([]byte(`{}`))),
},
}
settings := backend.DataSourceInstanceSettings{
URL: "http://localhost:9090",
JSONData: json.RawMessage(`{"timeInterval": "15s"}`),
}
opts, err := client.CreateTransportOptions(context.Background(), settings, log.New())
if err != nil {
return nil, err
}
httpClient, err := httpProvider.New(*opts)
if err != nil {
return nil, err
}
queryData, _ := querydata.New(httpClient, settings, log.New())
return &testContext{
httpProvider: httpProvider,
queryData: queryData,
}, nil
}
type fakeHttpClientProvider struct {
httpclient.Provider
opts httpclient.Options
req *http.Request
res *http.Response
}
func (p *fakeHttpClientProvider) New(opts ...httpclient.Options) (*http.Client, error) {
p.opts = opts[0]
c, err := httpclient.New(opts[0])
if err != nil {
return nil, err
}
c.Transport = p
return c, nil
}
func (p *fakeHttpClientProvider) GetTransport(opts ...httpclient.Options) (http.RoundTripper, error) {
p.opts = opts[0]
return http.DefaultTransport, nil
}
func (p *fakeHttpClientProvider) setResponse(res *http.Response) {
p.res = res
}
func (p *fakeHttpClientProvider) RoundTrip(req *http.Request) (*http.Response, error) {
p.req = req
return p.res, nil
}
+201
View File
@@ -0,0 +1,201 @@
package querydata
import (
"context"
"fmt"
"net/http"
"sort"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
jsoniter "github.com/json-iterator/go"
"github.com/grafana/grafana/pkg/promlib/converter"
"github.com/grafana/grafana/pkg/promlib/models"
"github.com/grafana/grafana/pkg/promlib/querydata/exemplar"
"github.com/grafana/grafana/pkg/promlib/utils"
)
func (s *QueryData) parseResponse(ctx context.Context, q *models.Query, res *http.Response, enablePrometheusDataplaneFlag bool) backend.DataResponse {
defer func() {
if err := res.Body.Close(); err != nil {
s.log.FromContext(ctx).Error("Failed to close response body", "err", err)
}
}()
ctx, endSpan := utils.StartTrace(ctx, s.tracer, "datasource.prometheus.parseResponse")
defer endSpan()
iter := jsoniter.Parse(jsoniter.ConfigDefault, res.Body, 1024)
r := converter.ReadPrometheusStyleResult(iter, converter.Options{
Dataplane: enablePrometheusDataplaneFlag,
})
r.Status = backend.Status(res.StatusCode)
// Add frame to attach metadata
if len(r.Frames) == 0 && !q.ExemplarQuery {
r.Frames = append(r.Frames, data.NewFrame(""))
}
// The ExecutedQueryString can be viewed in QueryInspector in UI
for i, frame := range r.Frames {
addMetadataToMultiFrame(q, frame, enablePrometheusDataplaneFlag)
if i == 0 {
frame.Meta.ExecutedQueryString = executedQueryString(q)
}
}
if r.Error == nil {
r = s.processExemplars(ctx, q, r)
}
return r
}
func (s *QueryData) processExemplars(ctx context.Context, q *models.Query, dr backend.DataResponse) backend.DataResponse {
_, endSpan := utils.StartTrace(ctx, s.tracer, "datasource.prometheus.processExemplars")
defer endSpan()
sampler := s.exemplarSampler()
labelTracker := exemplar.NewLabelTracker()
// we are moving from a multi-frame response returned
// by the converter to a single exemplar frame,
// so we need to build a new frame array with the
// old exemplar frames filtered out
framer := exemplar.NewFramer(sampler, labelTracker)
for _, frame := range dr.Frames {
// we don't need to process non-exemplar frames
// so they can be added to the response
if !isExemplarFrame(frame) {
framer.AddFrame(frame)
continue
}
// copy the current exemplar frame metadata
framer.SetMeta(frame.Meta)
framer.SetRefID(frame.RefID)
step := time.Duration(frame.Fields[0].Config.Interval) * time.Millisecond
sampler.SetStep(step)
seriesLabels := getSeriesLabels(frame)
labelTracker.Add(seriesLabels)
labelTracker.AddFields(frame.Fields[2:])
for rowIdx := 0; rowIdx < frame.Fields[0].Len(); rowIdx++ {
ts := frame.CopyAt(0, rowIdx).(time.Time)
val := frame.CopyAt(1, rowIdx).(float64)
ex := models.Exemplar{
RowIdx: rowIdx,
Fields: frame.Fields[2:],
Value: val,
Timestamp: ts,
SeriesLabels: seriesLabels,
}
sampler.Add(ex)
}
}
frames, err := framer.Frames()
return backend.DataResponse{
Frames: frames,
Error: err,
}
}
func addMetadataToMultiFrame(q *models.Query, frame *data.Frame, enableDataplane bool) {
if frame.Meta == nil {
frame.Meta = &data.FrameMeta{}
}
if len(frame.Fields) < 2 {
return
}
frame.Fields[0].Config = &data.FieldConfig{Interval: float64(q.Step.Milliseconds())}
customName := getName(q, frame.Fields[1])
if customName != "" {
frame.Fields[1].Config = &data.FieldConfig{DisplayNameFromDS: customName}
}
if enableDataplane {
valueField := frame.Fields[1]
if n, ok := valueField.Labels["__name__"]; ok {
valueField.Name = n
}
} else {
frame.Name = customName
}
}
// this is based on the logic from the String() function in github.com/prometheus/common/model.go
func metricNameFromLabels(f *data.Field) string {
labels := f.Labels
metricName, hasName := labels["__name__"]
numLabels := len(labels) - 1
if !hasName {
numLabels = len(labels)
}
labelStrings := make([]string, 0, numLabels)
for label, value := range labels {
if label != "__name__" {
labelStrings = append(labelStrings, fmt.Sprintf("%s=%q", label, value))
}
}
switch numLabels {
case 0:
if hasName {
return metricName
}
return "{}"
default:
sort.Strings(labelStrings)
return fmt.Sprintf("%s{%s}", metricName, strings.Join(labelStrings, ", "))
}
}
func executedQueryString(q *models.Query) string {
return "Expr: " + q.Expr + "\n" + "Step: " + q.Step.String()
}
func getName(q *models.Query, field *data.Field) string {
labels := field.Labels
legend := metricNameFromLabels(field)
if q.LegendFormat == legendFormatAuto {
if len(labels) > 0 {
legend = ""
}
} else if q.LegendFormat != "" {
result := legendFormatRegexp.ReplaceAllFunc([]byte(q.LegendFormat), func(in []byte) []byte {
labelName := strings.Replace(string(in), "{{", "", 1)
labelName = strings.Replace(labelName, "}}", "", 1)
labelName = strings.TrimSpace(labelName)
if val, exists := labels[labelName]; exists {
return []byte(val)
}
return []byte{}
})
legend = string(result)
}
// If legend is empty brackets, use query expression
if legend == "{}" {
return q.Expr
}
return legend
}
func isExemplarFrame(frame *data.Frame) bool {
rt := models.ResultTypeFromFrame(frame)
return rt == models.ResultTypeExemplar
}
func getSeriesLabels(frame *data.Frame) data.Labels {
// series labels are stored on the value field (index 1)
return frame.Fields[1].Labels.Copy()
}
+58
View File
@@ -0,0 +1,58 @@
package querydata
import (
"bytes"
"context"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/promlib/models"
"github.com/grafana/grafana/pkg/promlib/querydata/exemplar"
)
func TestQueryData_parseResponse(t *testing.T) {
qd := QueryData{exemplarSampler: exemplar.NewStandardDeviationSampler}
t.Run("resultType is before result the field must parsed normally", func(t *testing.T) {
resBody := `{"data":{"resultType":"vector", "result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}`
res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))}
result := qd.parseResponse(context.Background(), &models.Query{}, res, false)
assert.Nil(t, result.Error)
assert.Len(t, result.Frames, 1)
})
t.Run("resultType is after the result field must parsed normally", func(t *testing.T) {
resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}],"resultType":"vector"},"status":"success"}`
res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))}
result := qd.parseResponse(context.Background(), &models.Query{}, res, false)
assert.Nil(t, result.Error)
assert.Len(t, result.Frames, 1)
})
t.Run("no resultType is existed in the data", func(t *testing.T) {
resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}`
res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))}
result := qd.parseResponse(context.Background(), &models.Query{}, res, false)
assert.Error(t, result.Error)
assert.Equal(t, result.Error.Error(), "no resultType found")
})
t.Run("resultType is set as empty string before result", func(t *testing.T) {
resBody := `{"data":{"resultType":"", "result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}`
res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))}
result := qd.parseResponse(context.Background(), &models.Query{}, res, false)
assert.Error(t, result.Error)
assert.Equal(t, result.Error.Error(), "unknown result type: ")
})
t.Run("resultType is set as empty string after result", func(t *testing.T) {
resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}],"resultType":""},"status":"success"}`
res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))}
result := qd.parseResponse(context.Background(), &models.Query{}, res, false)
assert.Error(t, result.Error)
assert.Equal(t, result.Error.Error(), "unknown result type: ")
})
}