Skip to content

Commit

Permalink
Merge branch 'main' into issue-66/query-builder
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG.md
  • Loading branch information
Loori-R committed Sep 16, 2023
2 parents 9c6ab3a + 23671d7 commit 1be3ef7
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 77 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* FEATURE: add datasource settings for limiting the number of metrics during discovery. The proper limits should protect users from slowing down the browser when datasource returns big amounts of discovered metrics in response. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/82).

* BUGFIX: correctly handle custom query parameters in annotation queries. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/95)
* BUGFIX: fix the duplication of labels in the legend when using expressions. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/93)
* BUGFIX: fix the loading of metrics in the `metrics browser`. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/98)
* BUGFIX: fix an issue where `metricsql` functions were not properly processed. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/66)

## [v0.3.0](https://github.com/VictoriaMetrics/grafana-datasource/releases/tag/v0.3.0)
Expand Down
6 changes: 4 additions & 2 deletions pkg/plugin/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,14 @@ func (d *Datasource) query(ctx context.Context, query backend.DataQuery) backend
return newResponseError(err, backend.StatusInternal)
}

legend := q.parseLegend()
frames, err := r.getDataFrames(legend)
frames, err := r.getDataFrames()
if err != nil {
err = fmt.Errorf("failed to prepare data from reponse: %w", err)
return newResponseError(err, backend.StatusInternal)
}
for i := range frames {
q.addMetadataToMultiFrame(frames[i])
}

return backend.DataResponse{Frames: frames}
}
Expand Down
78 changes: 73 additions & 5 deletions pkg/plugin/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import (
"fmt"
"net/url"
"path"
"regexp"
"sort"
"strconv"
"strings"
"time"

"github.com/grafana/grafana-plugin-sdk-go/data"
)

const (
instantQueryPath = "/api/v1/query"
rangeQueryPath = "/api/v1/query_range"
legendFormatAuto = "__auto"
metricsName = "__name__"
)

// Query represents backend query object
Expand Down Expand Up @@ -119,13 +125,75 @@ func (q *Query) queryRangeURL(expr string, step time.Duration, queryParams url.V
return q.url.String()
}

var legendReplacer = strings.NewReplacer("{{", "", "}}", "")
var legendReplacer = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)

func (q *Query) parseLegend() string {
legend := legendReplacer.Replace(q.LegendFormat)
func (q *Query) parseLegend(labels data.Labels) string {

if legend == "{}" {
switch {
case q.LegendFormat == legendFormatAuto:
return q.Expr
case q.LegendFormat != "":
result := legendReplacer.ReplaceAllStringFunc(q.LegendFormat, func(in string) string {
labelName := strings.Replace(in, "{{", "", 1)
labelName = strings.Replace(labelName, "}}", "", 1)
labelName = strings.TrimSpace(labelName)
if val, ok := labels[labelName]; ok {
return val
}
return ""
})
if result == "" {
return q.Expr
}
return result
default:
// If legend is empty brackets, use query expression
legend := labelsToString(labels)
if legend == "{}" {
return q.Expr
}
return legend
}
}

func (q *Query) addMetadataToMultiFrame(frame *data.Frame) {
if len(frame.Fields) < 2 {
return
}
return legend

customName := q.parseLegend(frame.Fields[1].Labels)
if customName != "" {
frame.Fields[1].Config = &data.FieldConfig{DisplayNameFromDS: customName}
}

frame.Name = customName
}

func labelsToString(labels data.Labels) string {
if labels == nil {
return "{}"
}

var labelStrings []string
for label, value := range labels {
if label == metricsName {
continue
}
labelStrings = append(labelStrings, fmt.Sprintf("%s=%q", label, value))
}

var metricName string
mn, ok := labels[metricsName]
if ok {
metricName = mn
}

if len(labelStrings) < 1 {
return metricName
}

sort.Strings(labelStrings)
lbs := strings.Join(labelStrings, ",")

return fmt.Sprintf("%s{%s}", metricName, lbs)
}
168 changes: 168 additions & 0 deletions pkg/plugin/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"net/url"
"testing"
"time"

"github.com/grafana/grafana-plugin-sdk-go/data"
)

func TestQuery_getQueryURL(t *testing.T) {
Expand Down Expand Up @@ -180,3 +182,169 @@ func getTimeRage() TimeRange {
to := time.Unix(1670226793, 0)
return TimeRange{From: from, To: to}
}

func Test_labelsToString(t *testing.T) {
tests := []struct {
name string
labels data.Labels
want string
}{
{
name: "empty labels",
labels: nil,
want: "{}",
},
{
name: "set of labels",
labels: data.Labels{
"job": "vmstorage-maas",
"instance": "127.0.0.1",
},
want: `{instance="127.0.0.1",job="vmstorage-maas"}`,
},
{
name: "has name label",
labels: data.Labels{
"__name__": "vm_http_requests_total",
"job": "vmstorage-maas",
"instance": "127.0.0.1",
},
want: `vm_http_requests_total{instance="127.0.0.1",job="vmstorage-maas"}`,
},
{
name: "name label not from the start",
labels: data.Labels{
"job": "vmstorage-maas",
"__name__": "vm_http_requests_total",
"instance": "127.0.0.1",
},
want: `vm_http_requests_total{instance="127.0.0.1",job="vmstorage-maas"}`,
},
{
name: "has only name label",
labels: data.Labels{
"__name__": "vm_http_requests_total",
},
want: `vm_http_requests_total`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := labelsToString(tt.labels); got != tt.want {
t.Errorf("metricsFromLabels() = %v, want %v", got, tt.want)
}
})
}
}

func TestQuery_parseLegend1(t *testing.T) {
tests := []struct {
name string
legendFormat string
expr string
labels data.Labels
want string
}{
{
name: "empty labels and legend format no expression",
legendFormat: "",
labels: nil,
expr: "",
want: "",
},
{
name: "empty labels and legend format has expression",
legendFormat: "",
labels: nil,
expr: "sum(vm_http_request_total)",
want: "sum(vm_http_request_total)",
},
{
name: "empty labels and legend auto has expression",
legendFormat: "__auto",
labels: nil,
expr: "sum(vm_http_request_total)",
want: "sum(vm_http_request_total)",
},
{
name: "empty labels and legend auto has expression",
legendFormat: "{{job}}",
labels: nil,
expr: "sum(vm_http_request_total)",
want: "sum(vm_http_request_total)",
},
{
name: "empty labels and legend with metric name",
legendFormat: "{{__name__}}",
labels: nil,
expr: "sum(vm_http_request_total)",
want: "sum(vm_http_request_total)",
},
{
name: "has labels and legend auto has expression",
legendFormat: "__auto",
labels: data.Labels{
"job": "vmstorage-maas",
},
expr: "sum(vm_http_request_total)",
want: "sum(vm_http_request_total)",
},
{
name: "has labels and legend auto has expression",
legendFormat: "{{job}}",
labels: data.Labels{
"job": "vmstorage-maas",
},
expr: "sum(vm_http_request_total)",
want: "vmstorage-maas",
},
{
name: "do not have label",
legendFormat: "{{job}}",
labels: data.Labels{
"instance": "127.0.0.1",
},
expr: "sum(vm_http_request_total)",
want: "sum(vm_http_request_total)",
},
{
name: "has complex label",
legendFormat: "{{job}} {{instance}}",
labels: data.Labels{
"job": "vmstorage-maas",
"instance": "127.0.0.1",
},
expr: "sum(vm_http_request_total)",
want: "vmstorage-maas 127.0.0.1",
},
{
name: "auto label and only name present",
legendFormat: "__auto",
labels: data.Labels{
"__name__": "vm_http_request_total",
},
expr: "sum(vm_http_request_total)",
want: "sum(vm_http_request_total)",
},
{
name: "use just name in legend format",
legendFormat: "{{__name__}}",
labels: data.Labels{
"__name__": "vm_http_request_total",
},
expr: "sum(vm_http_request_total)",
want: "vm_http_request_total",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := &Query{
LegendFormat: tt.legendFormat,
Expr: tt.expr,
}
if got := q.parseLegend(tt.labels); got != tt.want {
t.Errorf("parseLegend() = %v, want %v", got, tt.want)
}
})
}
}
36 changes: 14 additions & 22 deletions pkg/plugin/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,18 @@ type promInstant struct {
Result []Result `json:"result"`
}

func (pi promInstant) dataframes(label string) (data.Frames, error) {
func (pi promInstant) dataframes() (data.Frames, error) {
frames := make(data.Frames, len(pi.Result))
for i, res := range pi.Result {
f, err := strconv.ParseFloat(res.Value[1].(string), 64)
if err != nil {
return nil, fmt.Errorf("metric %v, unable to parse float64 from %s: %w", res, res.Value[1], err)
}
if val, ok := res.Labels[label]; ok {
label = val
}

ts := time.Unix(int64(res.Value[0].(float64)), 0)
frames[i] = data.NewFrame(label,
data.NewField("time", nil, []time.Time{ts}),
data.NewField("values", data.Labels(res.Labels), []float64{f}))
frames[i] = data.NewFrame("",
data.NewField(data.TimeSeriesTimeFieldName, nil, []time.Time{ts}),
data.NewField(data.TimeSeriesValueFieldName, data.Labels(res.Labels), []float64{f}))
}

return frames, nil
Expand All @@ -68,7 +65,7 @@ type promRange struct {
Result []Result `json:"result"`
}

func (pr promRange) dataframes(label string) (data.Frames, error) {
func (pr promRange) dataframes() (data.Frames, error) {
frames := make(data.Frames, len(pr.Result))
for i, res := range pr.Result {
timestamps := make([]time.Time, len(res.Values))
Expand All @@ -91,13 +88,9 @@ func (pr promRange) dataframes(label string) (data.Frames, error) {
return nil, fmt.Errorf("metric %v contains no values", res)
}

if val, ok := res.Labels[label]; ok {
label = val
}

frames[i] = data.NewFrame(label,
data.NewField("time", nil, timestamps),
data.NewField("values", data.Labels(res.Labels), values))
frames[i] = data.NewFrame("",
data.NewField(data.TimeSeriesTimeFieldName, nil, timestamps),
data.NewField(data.TimeSeriesValueFieldName, data.Labels(res.Labels), values))
}

return frames, nil
Expand All @@ -111,30 +104,29 @@ func (ps promScalar) dataframes() (data.Frames, error) {
if err != nil {
return nil, fmt.Errorf("metric %v, unable to parse float64 from %s: %w", ps, ps[1], err)
}
label := fmt.Sprintf("%g", f)

frames = append(frames,
data.NewFrame(label,
data.NewField("time", nil, []time.Time{time.Unix(int64(ps[0].(float64)), 0)}),
data.NewField("value", nil, []float64{f})))
data.NewFrame("",
data.NewField(data.TimeSeriesTimeFieldName, nil, []time.Time{time.Unix(int64(ps[0].(float64)), 0)}),
data.NewField(data.TimeSeriesValueFieldName, nil, []float64{f})))

return frames, nil
}

func (r *Response) getDataFrames(label string) (data.Frames, error) {
func (r *Response) getDataFrames() (data.Frames, error) {
switch r.Data.ResultType {
case vector:
var pi promInstant
if err := json.Unmarshal(r.Data.Result, &pi.Result); err != nil {
return nil, fmt.Errorf("umarshal err %s; \n %#v", err, string(r.Data.Result))
}
return pi.dataframes(label)
return pi.dataframes()
case matrix:
var pr promRange
if err := json.Unmarshal(r.Data.Result, &pr.Result); err != nil {
return nil, fmt.Errorf("umarshal err %s; \n %#v", err, string(r.Data.Result))
}
return pr.dataframes(label)
return pr.dataframes()
case scalar:
var ps promScalar
if err := json.Unmarshal(r.Data.Result, &ps); err != nil {
Expand Down
Loading

0 comments on commit 1be3ef7

Please sign in to comment.