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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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: ")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user