grafana/pkg/promlib/querydata/response.go
ismail simsek 3fb6319d1b
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
2024-03-11 17:22:33 +01:00

202 lines
5.3 KiB
Go

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()
}