mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
loki: backend mode: support all query types (#45619)
* loki: backend mode: support all query types * loki: backend: adjust vector-parsing field-names * loki: backend: no interval for streams-dataframes * loki: backend: enable more query types * better variable name * removed unnecessary code * improve frame-processing * more unit tests * improved code Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * remove unused code Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * simplify code Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * lint fix Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
This commit is contained in:
parent
a578cf0f7c
commit
1cad35ea67
@ -28,16 +28,41 @@ func newLokiAPI(client *http.Client, url string, log log.Logger) *LokiAPI {
|
|||||||
func makeRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*http.Request, error) {
|
func makeRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*http.Request, error) {
|
||||||
qs := url.Values{}
|
qs := url.Values{}
|
||||||
qs.Set("query", query.Expr)
|
qs.Set("query", query.Expr)
|
||||||
qs.Set("step", query.Step.String())
|
|
||||||
qs.Set("start", strconv.FormatInt(query.Start.UnixNano(), 10))
|
// MaxLines defaults to zero when not received,
|
||||||
qs.Set("end", strconv.FormatInt(query.End.UnixNano(), 10))
|
// and Loki does not like limit=0, even when it is not needed
|
||||||
|
// (for example for metric queries), so we
|
||||||
|
// only send it when it's set
|
||||||
|
if query.MaxLines > 0 {
|
||||||
|
qs.Set("limit", fmt.Sprintf("%d", query.MaxLines))
|
||||||
|
}
|
||||||
|
|
||||||
lokiUrl, err := url.Parse(lokiDsUrl)
|
lokiUrl, err := url.Parse(lokiDsUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lokiUrl.Path = "/loki/api/v1/query_range"
|
switch query.QueryType {
|
||||||
|
case QueryTypeRange:
|
||||||
|
{
|
||||||
|
qs.Set("start", strconv.FormatInt(query.Start.UnixNano(), 10))
|
||||||
|
qs.Set("end", strconv.FormatInt(query.End.UnixNano(), 10))
|
||||||
|
// NOTE: technically for streams-producing queries `step`
|
||||||
|
// is ignored, so it would be nicer to not send it in such cases,
|
||||||
|
// but we cannot detect that situation, so we always send it.
|
||||||
|
// it should not break anything.
|
||||||
|
qs.Set("step", query.Step.String())
|
||||||
|
lokiUrl.Path = "/loki/api/v1/query_range"
|
||||||
|
}
|
||||||
|
case QueryTypeInstant:
|
||||||
|
{
|
||||||
|
qs.Set("time", strconv.FormatInt(query.End.UnixNano(), 10))
|
||||||
|
lokiUrl.Path = "/loki/api/v1/query"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid QueryType: %v", query.QueryType)
|
||||||
|
}
|
||||||
|
|
||||||
lokiUrl.RawQuery = qs.Encode()
|
lokiUrl.RawQuery = qs.Encode()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", lokiUrl.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", lokiUrl.String(), nil)
|
||||||
@ -51,11 +76,10 @@ func makeRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*http.
|
|||||||
// so it is not a regression.
|
// so it is not a regression.
|
||||||
// twe need to have that when we migrate to backend-queries.
|
// twe need to have that when we migrate to backend-queries.
|
||||||
//
|
//
|
||||||
// 2. we will have to send a custom http header based on the VolumeQuery prop
|
|
||||||
// (again, not needed for the alerting scenario)
|
if query.VolumeQuery {
|
||||||
// if query.VolumeQuery {
|
req.Header.Set("X-Query-Tags", "Source=logvolhist")
|
||||||
// req.Header.Set("X-Query-Tags", "Source=logvolhist")
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
@ -103,7 +127,7 @@ func makeLokiError(body io.ReadCloser) error {
|
|||||||
return fmt.Errorf("%v", errorMessage)
|
return fmt.Errorf("%v", errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *LokiAPI) QueryRange(ctx context.Context, query lokiQuery) (*loghttp.QueryResponse, error) {
|
func (api *LokiAPI) Query(ctx context.Context, query lokiQuery) (*loghttp.QueryResponse, error) {
|
||||||
req, err := makeRequest(ctx, api.url, query)
|
req, err := makeRequest(ctx, api.url, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
113
pkg/tsdb/loki/frame.go
Normal file
113
pkg/tsdb/loki/frame.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package loki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
// we adjust the dataframes to be the way frontend & alerting
|
||||||
|
// wants them.
|
||||||
|
func adjustFrame(frame *data.Frame, query *lokiQuery) *data.Frame {
|
||||||
|
labels := getFrameLabels(frame)
|
||||||
|
|
||||||
|
timeFields, nonTimeFields := partitionFields(frame)
|
||||||
|
|
||||||
|
isMetricFrame := nonTimeFields[0].Type() != data.FieldTypeString
|
||||||
|
|
||||||
|
isMetricRange := isMetricFrame && query.QueryType == QueryTypeRange
|
||||||
|
|
||||||
|
name := formatName(labels, query)
|
||||||
|
frame.Name = name
|
||||||
|
|
||||||
|
if frame.Meta == nil {
|
||||||
|
frame.Meta = &data.FrameMeta{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMetricRange {
|
||||||
|
frame.Meta.ExecutedQueryString = "Expr: " + query.Expr + "\n" + "Step: " + query.Step.String()
|
||||||
|
} else {
|
||||||
|
frame.Meta.ExecutedQueryString = "Expr: " + query.Expr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range timeFields {
|
||||||
|
field.Name = "time"
|
||||||
|
|
||||||
|
if isMetricRange {
|
||||||
|
if field.Config == nil {
|
||||||
|
field.Config = &data.FieldConfig{}
|
||||||
|
}
|
||||||
|
field.Config.Interval = float64(query.Step.Milliseconds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range nonTimeFields {
|
||||||
|
field.Name = "value"
|
||||||
|
if field.Config == nil {
|
||||||
|
field.Config = &data.FieldConfig{}
|
||||||
|
}
|
||||||
|
field.Config.DisplayNameFromDS = name
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatNamePrometheusStyle(labels map[string]string) string {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
for k, v := range labels {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s=%q", k, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(parts)
|
||||||
|
|
||||||
|
return fmt.Sprintf("{%s}", strings.Join(parts, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
//If legend (using of name or pattern instead of time series name) is used, use that name/pattern for formatting
|
||||||
|
func formatName(labels map[string]string, query *lokiQuery) string {
|
||||||
|
if query.LegendFormat == "" {
|
||||||
|
return formatNamePrometheusStyle(labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := legendFormat.ReplaceAllFunc([]byte(query.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{}
|
||||||
|
})
|
||||||
|
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFrameLabels(frame *data.Frame) map[string]string {
|
||||||
|
labels := make(map[string]string)
|
||||||
|
|
||||||
|
for _, field := range frame.Fields {
|
||||||
|
for k, v := range field.Labels {
|
||||||
|
labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
func partitionFields(frame *data.Frame) ([]*data.Field, []*data.Field) {
|
||||||
|
var timeFields []*data.Field
|
||||||
|
var nonTimeFields []*data.Field
|
||||||
|
|
||||||
|
for _, field := range frame.Fields {
|
||||||
|
if field.Type() == data.FieldTypeTime {
|
||||||
|
timeFields = append(timeFields, field)
|
||||||
|
} else {
|
||||||
|
nonTimeFields = append(nonTimeFields, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeFields, nonTimeFields
|
||||||
|
}
|
87
pkg/tsdb/loki/frame_test.go
Normal file
87
pkg/tsdb/loki/frame_test.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package loki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatName(t *testing.T) {
|
||||||
|
t.Run("converting metric name", func(t *testing.T) {
|
||||||
|
metric := map[string]string{
|
||||||
|
"app": "backend",
|
||||||
|
"device": "mobile",
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &lokiQuery{
|
||||||
|
LegendFormat: "legend {{app}} {{ device }} {{broken}}",
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, "legend backend mobile ", formatName(metric, query))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("build full series name", func(t *testing.T) {
|
||||||
|
metric := map[string]string{
|
||||||
|
"app": "backend",
|
||||||
|
"device": "mobile",
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &lokiQuery{
|
||||||
|
LegendFormat: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, `{app="backend", device="mobile"}`, formatName(metric, query))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdjustFrame(t *testing.T) {
|
||||||
|
t.Run("response should be parsed normally", func(t *testing.T) {
|
||||||
|
field1 := data.NewField("", nil, make([]time.Time, 0))
|
||||||
|
field2 := data.NewField("", nil, make([]float64, 0))
|
||||||
|
field2.Labels = data.Labels{"app": "Application", "tag2": "tag2"}
|
||||||
|
|
||||||
|
frame := data.NewFrame("test", field1, field2)
|
||||||
|
frame.SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMany})
|
||||||
|
|
||||||
|
query := &lokiQuery{
|
||||||
|
Expr: "up(ALERTS)",
|
||||||
|
QueryType: QueryTypeRange,
|
||||||
|
LegendFormat: "legend {{app}}",
|
||||||
|
Step: time.Second * 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustFrame(frame, query)
|
||||||
|
|
||||||
|
require.Equal(t, frame.Name, "legend Application")
|
||||||
|
require.Equal(t, frame.Meta.ExecutedQueryString, "Expr: up(ALERTS)\nStep: 42s")
|
||||||
|
require.Equal(t, frame.Fields[0].Config.Interval, float64(42000))
|
||||||
|
require.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "legend Application")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should set interval-attribute in response", func(t *testing.T) {
|
||||||
|
query := &lokiQuery{
|
||||||
|
Step: time.Second * 42,
|
||||||
|
QueryType: QueryTypeRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
field1 := data.NewField("", nil, make([]time.Time, 0))
|
||||||
|
field2 := data.NewField("", nil, make([]float64, 0))
|
||||||
|
|
||||||
|
frame := data.NewFrame("test", field1, field2)
|
||||||
|
frame.SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMany})
|
||||||
|
|
||||||
|
adjustFrame(frame, query)
|
||||||
|
|
||||||
|
// to keep the test simple, we assume the
|
||||||
|
// first field is the time-field
|
||||||
|
timeField := frame.Fields[0]
|
||||||
|
require.NotNil(t, timeField)
|
||||||
|
require.Equal(t, data.FieldTypeTime, timeField.Type())
|
||||||
|
|
||||||
|
timeFieldConfig := timeField.Config
|
||||||
|
require.NotNil(t, timeFieldConfig)
|
||||||
|
require.Equal(t, float64(42000), timeFieldConfig.Interval)
|
||||||
|
})
|
||||||
|
}
|
@ -22,17 +22,27 @@ import (
|
|||||||
// but i wanted to test for all of them, to be sure.
|
// but i wanted to test for all of them, to be sure.
|
||||||
|
|
||||||
func TestSuccessResponse(t *testing.T) {
|
func TestSuccessResponse(t *testing.T) {
|
||||||
|
matrixQuery := lokiQuery{Expr: "up(ALERTS)", Step: time.Second * 42, QueryType: QueryTypeRange}
|
||||||
|
vectorQuery := lokiQuery{Expr: "query1", QueryType: QueryTypeInstant}
|
||||||
|
streamsQuery := lokiQuery{Expr: "query1", QueryType: QueryTypeRange}
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
filepath string
|
filepath string
|
||||||
|
query lokiQuery
|
||||||
}{
|
}{
|
||||||
{name: "parse a simple matrix response", filepath: "matrix_simple"},
|
{name: "parse a simple matrix response", filepath: "matrix_simple", query: matrixQuery},
|
||||||
{name: "parse a matrix response with a time-gap in the middle", filepath: "matrix_gap"},
|
{name: "parse a matrix response with a time-gap in the middle", filepath: "matrix_gap", query: matrixQuery},
|
||||||
// you can produce NaN by having a metric query and add ` / 0` to the end
|
// you can produce NaN by having a metric query and add ` / 0` to the end
|
||||||
{name: "parse a matrix response with NaN", filepath: "matrix_nan"},
|
{name: "parse a matrix response with NaN", filepath: "matrix_nan", query: matrixQuery},
|
||||||
// you can produce Infinity by using `quantile_over_time(42,` (value larger than 1)
|
// you can produce Infinity by using `quantile_over_time(42,` (value larger than 1)
|
||||||
{name: "parse a matrix response with Infinity", filepath: "matrix_inf"},
|
{name: "parse a matrix response with Infinity", filepath: "matrix_inf", query: matrixQuery},
|
||||||
{name: "parse a matrix response with very small step value", filepath: "matrix_small_step"},
|
{name: "parse a matrix response with very small step value", filepath: "matrix_small_step", query: matrixQuery},
|
||||||
|
|
||||||
|
{name: "parse a simple vector response", filepath: "vector_simple", query: vectorQuery},
|
||||||
|
{name: "parse a vector response with special values", filepath: "vector_special_values", query: vectorQuery},
|
||||||
|
|
||||||
|
{name: "parse a simple streams response", filepath: "streams_simple", query: streamsQuery},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tt {
|
for _, test := range tt {
|
||||||
@ -43,7 +53,7 @@ func TestSuccessResponse(t *testing.T) {
|
|||||||
bytes, err := os.ReadFile(responseFileName)
|
bytes, err := os.ReadFile(responseFileName)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
frames, err := runQuery(context.Background(), makeMockedAPI(200, "application/json", bytes), &lokiQuery{Expr: "up(ALERTS)", Step: time.Second * 42})
|
frames, err := runQuery(context.Background(), makeMockedAPI(200, "application/json", bytes), &test.query)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dr := &backend.DataResponse{
|
dr := &backend.DataResponse{
|
||||||
@ -103,7 +113,7 @@ func TestErrorResponse(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tt {
|
for _, test := range tt {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
frames, err := runQuery(context.Background(), makeMockedAPI(400, test.contentType, test.body), &lokiQuery{})
|
frames, err := runQuery(context.Background(), makeMockedAPI(400, test.contentType, test.body), &lokiQuery{QueryType: QueryTypeRange})
|
||||||
|
|
||||||
require.Len(t, frames, 0)
|
require.Len(t, frames, 0)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
@ -5,8 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||||
@ -15,10 +13,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/loki/pkg/loghttp"
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
|
||||||
"github.com/prometheus/common/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@ -44,13 +39,15 @@ type datasourceInfo struct {
|
|||||||
URL string
|
URL string
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryModel struct {
|
type QueryJSONModel struct {
|
||||||
QueryType string `json:"queryType"`
|
QueryType string `json:"queryType"`
|
||||||
Expr string `json:"expr"`
|
Expr string `json:"expr"`
|
||||||
LegendFormat string `json:"legendFormat"`
|
LegendFormat string `json:"legendFormat"`
|
||||||
Interval string `json:"interval"`
|
Interval string `json:"interval"`
|
||||||
IntervalMS int `json:"intervalMS"`
|
IntervalMS int `json:"intervalMS"`
|
||||||
Resolution int64 `json:"resolution"`
|
Resolution int64 `json:"resolution"`
|
||||||
|
MaxLines int `json:"maxLines"`
|
||||||
|
VolumeQuery bool `json:"volumeQuery"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
|
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||||
@ -111,67 +108,9 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//If legend (using of name or pattern instead of time series name) is used, use that name/pattern for formatting
|
|
||||||
func formatLegend(metric model.Metric, query *lokiQuery) string {
|
|
||||||
if query.LegendFormat == "" {
|
|
||||||
return metric.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
result := legendFormat.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
|
|
||||||
labelName := strings.Replace(string(in), "{{", "", 1)
|
|
||||||
labelName = strings.Replace(labelName, "}}", "", 1)
|
|
||||||
labelName = strings.TrimSpace(labelName)
|
|
||||||
if val, exists := metric[model.LabelName(labelName)]; exists {
|
|
||||||
return []byte(val)
|
|
||||||
}
|
|
||||||
return []byte{}
|
|
||||||
})
|
|
||||||
|
|
||||||
return string(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseResponse(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) {
|
|
||||||
frames := data.Frames{}
|
|
||||||
|
|
||||||
//We are currently processing only matrix results (for alerting)
|
|
||||||
matrix, ok := value.Data.Result.(loghttp.Matrix)
|
|
||||||
if !ok {
|
|
||||||
return frames, fmt.Errorf("unsupported result format: %q", value.Data.ResultType)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range matrix {
|
|
||||||
name := formatLegend(v.Metric, query)
|
|
||||||
tags := make(map[string]string, len(v.Metric))
|
|
||||||
timeVector := make([]time.Time, 0, len(v.Values))
|
|
||||||
values := make([]float64, 0, len(v.Values))
|
|
||||||
|
|
||||||
for k, v := range v.Metric {
|
|
||||||
tags[string(k)] = string(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, k := range v.Values {
|
|
||||||
timeVector = append(timeVector, k.Timestamp.Time().UTC())
|
|
||||||
values = append(values, float64(k.Value))
|
|
||||||
}
|
|
||||||
|
|
||||||
timeField := data.NewField("time", nil, timeVector)
|
|
||||||
timeField.Config = &data.FieldConfig{Interval: float64(query.Step.Milliseconds())}
|
|
||||||
valueField := data.NewField("value", tags, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: name})
|
|
||||||
|
|
||||||
frame := data.NewFrame(name, timeField, valueField)
|
|
||||||
frame.SetMeta(&data.FrameMeta{
|
|
||||||
ExecutedQueryString: "Expr: " + query.Expr + "\n" + "Step: " + query.Step.String(),
|
|
||||||
})
|
|
||||||
|
|
||||||
frames = append(frames, frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
return frames, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// we extracted this part of the functionality to make it easy to unit-test it
|
// we extracted this part of the functionality to make it easy to unit-test it
|
||||||
func runQuery(ctx context.Context, api *LokiAPI, query *lokiQuery) (data.Frames, error) {
|
func runQuery(ctx context.Context, api *LokiAPI, query *lokiQuery) (data.Frames, error) {
|
||||||
value, err := api.QueryRange(ctx, *query)
|
value, err := api.Query(ctx, *query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data.Frames{}, err
|
return data.Frames{}, err
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package loki
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -52,10 +53,25 @@ func interpolateVariables(expr string, interval time.Duration, timeRange time.Du
|
|||||||
return expr
|
return expr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseQueryType(jsonValue string) (QueryType, error) {
|
||||||
|
switch jsonValue {
|
||||||
|
case "instant":
|
||||||
|
return QueryTypeInstant, nil
|
||||||
|
case "range":
|
||||||
|
return QueryTypeRange, nil
|
||||||
|
case "":
|
||||||
|
// there are older queries stored in alerting that did not have queryType,
|
||||||
|
// those were range-queries
|
||||||
|
return QueryTypeRange, nil
|
||||||
|
default:
|
||||||
|
return QueryTypeRange, fmt.Errorf("invalid queryType: %s", jsonValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parseQuery(queryContext *backend.QueryDataRequest) ([]*lokiQuery, error) {
|
func parseQuery(queryContext *backend.QueryDataRequest) ([]*lokiQuery, error) {
|
||||||
qs := []*lokiQuery{}
|
qs := []*lokiQuery{}
|
||||||
for _, query := range queryContext.Queries {
|
for _, query := range queryContext.Queries {
|
||||||
model := &QueryModel{}
|
model := &QueryJSONModel{}
|
||||||
err := json.Unmarshal(query.JSON, model)
|
err := json.Unmarshal(query.JSON, model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -76,13 +92,21 @@ func parseQuery(queryContext *backend.QueryDataRequest) ([]*lokiQuery, error) {
|
|||||||
|
|
||||||
expr := interpolateVariables(model.Expr, interval, timeRange)
|
expr := interpolateVariables(model.Expr, interval, timeRange)
|
||||||
|
|
||||||
|
queryType, err := parseQueryType(model.QueryType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
qs = append(qs, &lokiQuery{
|
qs = append(qs, &lokiQuery{
|
||||||
Expr: expr,
|
Expr: expr,
|
||||||
|
QueryType: queryType,
|
||||||
Step: step,
|
Step: step,
|
||||||
|
MaxLines: model.MaxLines,
|
||||||
LegendFormat: model.LegendFormat,
|
LegendFormat: model.LegendFormat,
|
||||||
Start: start,
|
Start: start,
|
||||||
End: end,
|
End: end,
|
||||||
RefID: query.RefID,
|
RefID: query.RefID,
|
||||||
|
VolumeQuery: model.VolumeQuery,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
114
pkg/tsdb/loki/parse_response.go
Normal file
114
pkg/tsdb/loki/parse_response.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package loki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/grafana/loki/pkg/loghttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseResponse(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) {
|
||||||
|
frames, err := lokiResponseToDataFrames(value, query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, frame := range frames {
|
||||||
|
adjustFrame(frame, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lokiResponseToDataFrames(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) {
|
||||||
|
switch res := value.Data.Result.(type) {
|
||||||
|
case loghttp.Matrix:
|
||||||
|
return lokiMatrixToDataFrames(res, query), nil
|
||||||
|
case loghttp.Vector:
|
||||||
|
return lokiVectorToDataFrames(res, query), nil
|
||||||
|
case loghttp.Streams:
|
||||||
|
return lokiStreamsToDataFrames(res, query), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("resultType %T not supported{", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lokiMatrixToDataFrames(matrix loghttp.Matrix, query *lokiQuery) data.Frames {
|
||||||
|
frames := data.Frames{}
|
||||||
|
|
||||||
|
for _, v := range matrix {
|
||||||
|
tags := make(map[string]string, len(v.Metric))
|
||||||
|
timeVector := make([]time.Time, 0, len(v.Values))
|
||||||
|
values := make([]float64, 0, len(v.Values))
|
||||||
|
|
||||||
|
for k, v := range v.Metric {
|
||||||
|
tags[string(k)] = string(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range v.Values {
|
||||||
|
timeVector = append(timeVector, k.Timestamp.Time().UTC())
|
||||||
|
values = append(values, float64(k.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
timeField := data.NewField("", nil, timeVector)
|
||||||
|
valueField := data.NewField("", tags, values)
|
||||||
|
|
||||||
|
frame := data.NewFrame("", timeField, valueField)
|
||||||
|
|
||||||
|
frames = append(frames, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
func lokiVectorToDataFrames(vector loghttp.Vector, query *lokiQuery) data.Frames {
|
||||||
|
frames := data.Frames{}
|
||||||
|
|
||||||
|
for _, v := range vector {
|
||||||
|
tags := make(map[string]string, len(v.Metric))
|
||||||
|
timeVector := []time.Time{v.Timestamp.Time().UTC()}
|
||||||
|
values := []float64{float64(v.Value)}
|
||||||
|
|
||||||
|
for k, v := range v.Metric {
|
||||||
|
tags[string(k)] = string(v)
|
||||||
|
}
|
||||||
|
timeField := data.NewField("", nil, timeVector)
|
||||||
|
valueField := data.NewField("", tags, values)
|
||||||
|
|
||||||
|
frame := data.NewFrame("", timeField, valueField)
|
||||||
|
|
||||||
|
frames = append(frames, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
func lokiStreamsToDataFrames(streams loghttp.Streams, query *lokiQuery) data.Frames {
|
||||||
|
frames := data.Frames{}
|
||||||
|
|
||||||
|
for _, v := range streams {
|
||||||
|
tags := make(map[string]string, len(v.Labels))
|
||||||
|
timeVector := make([]time.Time, 0, len(v.Entries))
|
||||||
|
values := make([]string, 0, len(v.Entries))
|
||||||
|
|
||||||
|
for k, v := range v.Labels {
|
||||||
|
tags[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range v.Entries {
|
||||||
|
timeVector = append(timeVector, k.Timestamp.UTC())
|
||||||
|
values = append(values, k.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeField := data.NewField("", nil, timeVector)
|
||||||
|
valueField := data.NewField("", tags, values)
|
||||||
|
|
||||||
|
frame := data.NewFrame("", timeField, valueField)
|
||||||
|
|
||||||
|
frames = append(frames, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames
|
||||||
|
}
|
@ -11,45 +11,15 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoki(t *testing.T) {
|
|
||||||
t.Run("converting metric name", func(t *testing.T) {
|
|
||||||
metric := map[p.LabelName]p.LabelValue{
|
|
||||||
p.LabelName("app"): p.LabelValue("backend"),
|
|
||||||
p.LabelName("device"): p.LabelValue("mobile"),
|
|
||||||
}
|
|
||||||
|
|
||||||
query := &lokiQuery{
|
|
||||||
LegendFormat: "legend {{app}} {{ device }} {{broken}}",
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(t, "legend backend mobile ", formatLegend(metric, query))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("build full series name", func(t *testing.T) {
|
|
||||||
metric := map[p.LabelName]p.LabelValue{
|
|
||||||
p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"),
|
|
||||||
p.LabelName("app"): p.LabelValue("backend"),
|
|
||||||
p.LabelName("device"): p.LabelValue("mobile"),
|
|
||||||
}
|
|
||||||
|
|
||||||
query := &lokiQuery{
|
|
||||||
LegendFormat: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(t, `http_request_total{app="backend", device="mobile"}`, formatLegend(metric, query))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseResponse(t *testing.T) {
|
func TestParseResponse(t *testing.T) {
|
||||||
t.Run("value is not of type matrix", func(t *testing.T) {
|
t.Run("value is not of supported type", func(t *testing.T) {
|
||||||
queryRes := data.Frames{}
|
|
||||||
value := loghttp.QueryResponse{
|
value := loghttp.QueryResponse{
|
||||||
Data: loghttp.QueryResponseData{
|
Data: loghttp.QueryResponseData{
|
||||||
Result: loghttp.Vector{},
|
Result: loghttp.Scalar{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
res, err := parseResponse(&value, nil)
|
res, err := parseResponse(&value, nil)
|
||||||
require.Equal(t, queryRes, res)
|
require.Equal(t, len(res), 0)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -74,6 +44,7 @@ func TestParseResponse(t *testing.T) {
|
|||||||
|
|
||||||
query := &lokiQuery{
|
query := &lokiQuery{
|
||||||
Expr: "up(ALERTS)",
|
Expr: "up(ALERTS)",
|
||||||
|
QueryType: QueryTypeRange,
|
||||||
LegendFormat: "legend {{app}}",
|
LegendFormat: "legend {{app}}",
|
||||||
Step: time.Second * 42,
|
Step: time.Second * 42,
|
||||||
}
|
}
|
||||||
@ -117,7 +88,8 @@ func TestParseResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := &lokiQuery{
|
query := &lokiQuery{
|
||||||
Step: time.Second * 42,
|
Step: time.Second * 42,
|
||||||
|
QueryType: QueryTypeRange,
|
||||||
}
|
}
|
||||||
|
|
||||||
frames, err := parseResponse(&value, query)
|
frames, err := parseResponse(&value, query)
|
37
pkg/tsdb/loki/testdata/streams_simple.golden.txt
vendored
Normal file
37
pkg/tsdb/loki/testdata/streams_simple.golden.txt
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
|
||||||
|
Frame[0] {
|
||||||
|
"executedQueryString": "Expr: query1"
|
||||||
|
}
|
||||||
|
Name: {level="error", location="moon"}
|
||||||
|
Dimensions: 2 Fields by 1 Rows
|
||||||
|
+----------------------------------------+------------------------------------+
|
||||||
|
| Name: time | Name: value |
|
||||||
|
| Labels: | Labels: level=error, location=moon |
|
||||||
|
| Type: []time.Time | Type: []string |
|
||||||
|
+----------------------------------------+------------------------------------+
|
||||||
|
| 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 |
|
||||||
|
+----------------------------------------+------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Frame[1] {
|
||||||
|
"executedQueryString": "Expr: query1"
|
||||||
|
}
|
||||||
|
Name: {level="info", location="moon"}
|
||||||
|
Dimensions: 2 Fields by 4 Rows
|
||||||
|
+-----------------------------------------+-----------------------------------+
|
||||||
|
| Name: time | Name: value |
|
||||||
|
| Labels: | Labels: level=info, location=moon |
|
||||||
|
| Type: []time.Time | Type: []string |
|
||||||
|
+-----------------------------------------+-----------------------------------+
|
||||||
|
| 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 |
|
||||||
|
| 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 |
|
||||||
|
| 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 |
|
||||||
|
| 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 |
|
||||||
|
+-----------------------------------------+-----------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
====== TEST DATA RESPONSE (arrow base64) ======
|
||||||
|
FRAME=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
|
||||||
|
FRAME=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
|
76
pkg/tsdb/loki/testdata/streams_simple.json
vendored
Normal file
76
pkg/tsdb/loki/testdata/streams_simple.json
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"resultType": "streams",
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"stream": {
|
||||||
|
"level": "error",
|
||||||
|
"location": "moon"
|
||||||
|
},
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
"1645030244810757120",
|
||||||
|
"log line error 1"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stream": {
|
||||||
|
"level": "info",
|
||||||
|
"location": "moon"
|
||||||
|
},
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
"1645030247027735040",
|
||||||
|
"log line info 1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1645030246277587968",
|
||||||
|
"log line info 2"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1645030245539423744",
|
||||||
|
"log line info 3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1645030244091700992",
|
||||||
|
"log line info 4"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stats": {
|
||||||
|
"summary": {
|
||||||
|
"bytesProcessedPerSecond": 3507022,
|
||||||
|
"linesProcessedPerSecond": 24818,
|
||||||
|
"totalBytesProcessed": 7772,
|
||||||
|
"totalLinesProcessed": 55,
|
||||||
|
"execTime": 0.002216125
|
||||||
|
},
|
||||||
|
"store": {
|
||||||
|
"totalChunksRef": 2,
|
||||||
|
"totalChunksDownloaded": 2,
|
||||||
|
"chunksDownloadTime": 0.000390958,
|
||||||
|
"headChunkBytes": 0,
|
||||||
|
"headChunkLines": 0,
|
||||||
|
"decompressedBytes": 7772,
|
||||||
|
"decompressedLines": 55,
|
||||||
|
"compressedBytes": 31432,
|
||||||
|
"totalDuplicates": 0
|
||||||
|
},
|
||||||
|
"ingester": {
|
||||||
|
"totalReached": 0,
|
||||||
|
"totalChunksMatched": 0,
|
||||||
|
"totalBatches": 0,
|
||||||
|
"totalLinesSent": 0,
|
||||||
|
"headChunkBytes": 0,
|
||||||
|
"headChunkLines": 0,
|
||||||
|
"decompressedBytes": 0,
|
||||||
|
"decompressedLines": 0,
|
||||||
|
"compressedBytes": 0,
|
||||||
|
"totalDuplicates": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
pkg/tsdb/loki/testdata/vector_simple.golden.txt
vendored
Normal file
34
pkg/tsdb/loki/testdata/vector_simple.golden.txt
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
|
||||||
|
Frame[0] {
|
||||||
|
"executedQueryString": "Expr: query1"
|
||||||
|
}
|
||||||
|
Name: {level="error", location="moon"}
|
||||||
|
Dimensions: 2 Fields by 1 Rows
|
||||||
|
+-------------------------------+------------------------------------+
|
||||||
|
| Name: time | Name: value |
|
||||||
|
| Labels: | Labels: level=error, location=moon |
|
||||||
|
| Type: []time.Time | Type: []float64 |
|
||||||
|
+-------------------------------+------------------------------------+
|
||||||
|
| 2022-02-16 16:41:39 +0000 UTC | 23 |
|
||||||
|
+-------------------------------+------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Frame[1] {
|
||||||
|
"executedQueryString": "Expr: query1"
|
||||||
|
}
|
||||||
|
Name: {level="info", location="moon"}
|
||||||
|
Dimensions: 2 Fields by 1 Rows
|
||||||
|
+-------------------------------+-----------------------------------+
|
||||||
|
| Name: time | Name: value |
|
||||||
|
| Labels: | Labels: level=info, location=moon |
|
||||||
|
| Type: []time.Time | Type: []float64 |
|
||||||
|
+-------------------------------+-----------------------------------+
|
||||||
|
| 2022-02-16 16:41:39 +0000 UTC | 47 |
|
||||||
|
+-------------------------------+-----------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
====== TEST DATA RESPONSE (arrow base64) ======
|
||||||
|
FRAME=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
|
||||||
|
FRAME=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
|
16
pkg/tsdb/loki/testdata/vector_simple.json
vendored
Normal file
16
pkg/tsdb/loki/testdata/vector_simple.json
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"resultType": "vector",
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"metric": { "level": "error", "location": "moon"},
|
||||||
|
"value": [1645029699, "23"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": { "level": "info", "location": "moon" },
|
||||||
|
"value": [1645029699, "47"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
50
pkg/tsdb/loki/testdata/vector_special_values.golden.txt
vendored
Normal file
50
pkg/tsdb/loki/testdata/vector_special_values.golden.txt
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
|
||||||
|
Frame[0] {
|
||||||
|
"executedQueryString": "Expr: query1"
|
||||||
|
}
|
||||||
|
Name: {level="error", location="moon"}
|
||||||
|
Dimensions: 2 Fields by 1 Rows
|
||||||
|
+-------------------------------+------------------------------------+
|
||||||
|
| Name: time | Name: value |
|
||||||
|
| Labels: | Labels: level=error, location=moon |
|
||||||
|
| Type: []time.Time | Type: []float64 |
|
||||||
|
+-------------------------------+------------------------------------+
|
||||||
|
| 2022-02-16 16:41:39 +0000 UTC | +Inf |
|
||||||
|
+-------------------------------+------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Frame[1] {
|
||||||
|
"executedQueryString": "Expr: query1"
|
||||||
|
}
|
||||||
|
Name: {level="info", location="moon"}
|
||||||
|
Dimensions: 2 Fields by 1 Rows
|
||||||
|
+-------------------------------+-----------------------------------+
|
||||||
|
| Name: time | Name: value |
|
||||||
|
| Labels: | Labels: level=info, location=moon |
|
||||||
|
| Type: []time.Time | Type: []float64 |
|
||||||
|
+-------------------------------+-----------------------------------+
|
||||||
|
| 2022-02-16 16:41:39 +0000 UTC | -Inf |
|
||||||
|
+-------------------------------+-----------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Frame[2] {
|
||||||
|
"executedQueryString": "Expr: query1"
|
||||||
|
}
|
||||||
|
Name: {level="debug", location="moon"}
|
||||||
|
Dimensions: 2 Fields by 1 Rows
|
||||||
|
+-------------------------------+------------------------------------+
|
||||||
|
| Name: time | Name: value |
|
||||||
|
| Labels: | Labels: level=debug, location=moon |
|
||||||
|
| Type: []time.Time | Type: []float64 |
|
||||||
|
+-------------------------------+------------------------------------+
|
||||||
|
| 2022-02-16 16:41:39 +0000 UTC | NaN |
|
||||||
|
+-------------------------------+------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
====== TEST DATA RESPONSE (arrow base64) ======
|
||||||
|
FRAME=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
|
||||||
|
FRAME=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
|
||||||
|
FRAME=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
|
20
pkg/tsdb/loki/testdata/vector_special_values.json
vendored
Normal file
20
pkg/tsdb/loki/testdata/vector_special_values.json
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"resultType": "vector",
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"metric": { "level": "error", "location": "moon"},
|
||||||
|
"value": [1645029699, "+Inf"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": { "level": "info", "location": "moon" },
|
||||||
|
"value": [1645029699, "-Inf"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": { "level": "debug", "location": "moon" },
|
||||||
|
"value": [1645029699, "NaN"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,21 @@ package loki
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
type QueryType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
QueryTypeRange QueryType = "range"
|
||||||
|
QueryTypeInstant QueryType = "instant"
|
||||||
|
)
|
||||||
|
|
||||||
type lokiQuery struct {
|
type lokiQuery struct {
|
||||||
Expr string
|
Expr string
|
||||||
|
QueryType QueryType
|
||||||
Step time.Duration
|
Step time.Duration
|
||||||
|
MaxLines int
|
||||||
LegendFormat string
|
LegendFormat string
|
||||||
Start time.Time
|
Start time.Time
|
||||||
End time.Time
|
End time.Time
|
||||||
RefID string
|
RefID string
|
||||||
|
VolumeQuery bool
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
import { DataQueryRequest, DataQueryResponse, DataFrame, isDataFrame, FieldType, QueryResultMeta } from '@grafana/data';
|
||||||
|
import { LokiQuery, LokiQueryType } from './types';
|
||||||
|
import { makeTableFrames } from './makeTableFrames';
|
||||||
|
import { formatQuery, getHighlighterExpressionsFromQuery } from './query_utils';
|
||||||
|
|
||||||
|
function isMetricFrame(frame: DataFrame): boolean {
|
||||||
|
return frame.fields.every((field) => field.type === FieldType.time || field.type === FieldType.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a new frame, with meta merged with it's original meta
|
||||||
|
function setFrameMeta(frame: DataFrame, meta: QueryResultMeta): DataFrame {
|
||||||
|
const { meta: oldMeta, ...rest } = frame;
|
||||||
|
// meta maybe be undefined, we need to handle that
|
||||||
|
const newMeta = { ...oldMeta, ...meta };
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
meta: newMeta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function processStreamsFrames(frames: DataFrame[], queryMap: Map<string, LokiQuery>): DataFrame[] {
|
||||||
|
return frames.map((frame) => {
|
||||||
|
const query = frame.refId !== undefined ? queryMap.get(frame.refId) : undefined;
|
||||||
|
const meta: QueryResultMeta = {
|
||||||
|
preferredVisualisationType: 'logs',
|
||||||
|
searchWords: query !== undefined ? getHighlighterExpressionsFromQuery(formatQuery(query.expr)) : undefined,
|
||||||
|
};
|
||||||
|
return setFrameMeta(frame, meta);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processMetricInstantFrames(frames: DataFrame[]): DataFrame[] {
|
||||||
|
return frames.length > 0 ? makeTableFrames(frames) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function processMetricRangeFrames(frames: DataFrame[]): DataFrame[] {
|
||||||
|
const meta: QueryResultMeta = { preferredVisualisationType: 'graph' };
|
||||||
|
return frames.map((frame) => setFrameMeta(frame, meta));
|
||||||
|
}
|
||||||
|
|
||||||
|
// we split the frames into 3 groups, because we will handle
|
||||||
|
// each group slightly differently
|
||||||
|
function groupFrames(
|
||||||
|
frames: DataFrame[],
|
||||||
|
queryMap: Map<string, LokiQuery>
|
||||||
|
): {
|
||||||
|
streamsFrames: DataFrame[];
|
||||||
|
metricInstantFrames: DataFrame[];
|
||||||
|
metricRangeFrames: DataFrame[];
|
||||||
|
} {
|
||||||
|
const streamsFrames: DataFrame[] = [];
|
||||||
|
const metricInstantFrames: DataFrame[] = [];
|
||||||
|
const metricRangeFrames: DataFrame[] = [];
|
||||||
|
|
||||||
|
frames.forEach((frame) => {
|
||||||
|
if (!isMetricFrame(frame)) {
|
||||||
|
streamsFrames.push(frame);
|
||||||
|
} else {
|
||||||
|
const isInstantFrame = frame.refId != null && queryMap.get(frame.refId)?.queryType === LokiQueryType.Instant;
|
||||||
|
if (isInstantFrame) {
|
||||||
|
metricInstantFrames.push(frame);
|
||||||
|
} else {
|
||||||
|
metricRangeFrames.push(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { streamsFrames, metricInstantFrames, metricRangeFrames };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformBackendResult(
|
||||||
|
response: DataQueryResponse,
|
||||||
|
request: DataQueryRequest<LokiQuery>
|
||||||
|
): DataQueryResponse {
|
||||||
|
const { data, ...rest } = response;
|
||||||
|
|
||||||
|
// in the typescript type, data is an array of basically anything.
|
||||||
|
// we do know that they have to be dataframes, so we make a quick check,
|
||||||
|
// this way we can be sure, and also typescript is happy.
|
||||||
|
const dataFrames = data.map((d) => {
|
||||||
|
if (!isDataFrame(d)) {
|
||||||
|
throw new Error('transformation only supports dataframe responses');
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryMap = new Map(request.targets.map((query) => [query.refId, query]));
|
||||||
|
|
||||||
|
const { streamsFrames, metricInstantFrames, metricRangeFrames } = groupFrames(dataFrames, queryMap);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
data: [
|
||||||
|
...processMetricRangeFrames(metricRangeFrames),
|
||||||
|
...processMetricInstantFrames(metricInstantFrames),
|
||||||
|
...processStreamsFrames(streamsFrames, queryMap),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
@ -44,6 +44,7 @@ import {
|
|||||||
lokiStreamsToDataFrames,
|
lokiStreamsToDataFrames,
|
||||||
processRangeQueryResponse,
|
processRangeQueryResponse,
|
||||||
} from './result_transformer';
|
} from './result_transformer';
|
||||||
|
import { transformBackendResult } from './backendResultTransformer';
|
||||||
import { addParsedLabelToQuery, getNormalizedLokiQuery, queryHasPipeParser } from './query_utils';
|
import { addParsedLabelToQuery, getNormalizedLokiQuery, queryHasPipeParser } from './query_utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -153,19 +154,7 @@ export class LokiDatasource
|
|||||||
...this.getRangeScopedVars(request.range),
|
...this.getRangeScopedVars(request.range),
|
||||||
};
|
};
|
||||||
|
|
||||||
// if all these are true, run query through backend:
|
const shouldRunBackendQuery = config.featureToggles.lokiBackendMode && request.app === CoreApp.Explore;
|
||||||
// - feature-flag is enabled
|
|
||||||
// - we are in explore-mode
|
|
||||||
// - for every query it is true that:
|
|
||||||
// - query is range query
|
|
||||||
// - and query is metric query
|
|
||||||
// - and query is not a log-volume-query (those need a custom http header)
|
|
||||||
const shouldRunBackendQuery =
|
|
||||||
config.featureToggles.lokiBackendMode &&
|
|
||||||
request.app === CoreApp.Explore &&
|
|
||||||
request.targets.every(
|
|
||||||
(query) => query.queryType === LokiQueryType.Range && isMetricsQuery(query.expr) && !query.volumeQuery
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldRunBackendQuery) {
|
if (shouldRunBackendQuery) {
|
||||||
// we "fix" the loki queries to have `.queryType` and not have `.instant` and `.range`
|
// we "fix" the loki queries to have `.queryType` and not have `.instant` and `.range`
|
||||||
@ -173,7 +162,7 @@ export class LokiDatasource
|
|||||||
...request,
|
...request,
|
||||||
targets: request.targets.map(getNormalizedLokiQuery),
|
targets: request.targets.map(getNormalizedLokiQuery),
|
||||||
};
|
};
|
||||||
return super.query(fixedRequest);
|
return super.query(fixedRequest).pipe(map((response) => transformBackendResult(response, fixedRequest)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredTargets = request.targets
|
const filteredTargets = request.targets
|
||||||
|
146
public/app/plugins/datasource/loki/makeTableFrames.test.ts
Normal file
146
public/app/plugins/datasource/loki/makeTableFrames.test.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { ArrayVector, DataFrame, FieldType } from '@grafana/data';
|
||||||
|
import { makeTableFrames } from './makeTableFrames';
|
||||||
|
|
||||||
|
const frame1: DataFrame = {
|
||||||
|
name: 'frame1',
|
||||||
|
refId: 'A',
|
||||||
|
meta: {
|
||||||
|
executedQueryString: 'something1',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'Time',
|
||||||
|
type: FieldType.time,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1645029699311]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Value',
|
||||||
|
type: FieldType.number,
|
||||||
|
labels: {
|
||||||
|
level: 'error',
|
||||||
|
location: 'moon',
|
||||||
|
protocol: 'http',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
displayNameFromDS: '{level="error", location="moon", protocol="http"}',
|
||||||
|
},
|
||||||
|
values: new ArrayVector([23]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const frame2: DataFrame = {
|
||||||
|
name: 'frame1',
|
||||||
|
refId: 'A',
|
||||||
|
meta: {
|
||||||
|
executedQueryString: 'something1',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'Time',
|
||||||
|
type: FieldType.time,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1645029699311]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Value',
|
||||||
|
type: FieldType.number,
|
||||||
|
labels: {
|
||||||
|
level: 'info',
|
||||||
|
location: 'moon',
|
||||||
|
protocol: 'http',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
displayNameFromDS: '{level="info", location="moon", protocol="http"}',
|
||||||
|
},
|
||||||
|
values: new ArrayVector([45]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const frame3: DataFrame = {
|
||||||
|
name: 'frame1',
|
||||||
|
refId: 'B',
|
||||||
|
meta: {
|
||||||
|
executedQueryString: 'something1',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'Time',
|
||||||
|
type: FieldType.time,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1645029699311]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Value',
|
||||||
|
type: FieldType.number,
|
||||||
|
labels: {
|
||||||
|
level: 'error',
|
||||||
|
location: 'moon',
|
||||||
|
protocol: 'http',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
displayNameFromDS: '{level="error", location="moon", protocol="http"}',
|
||||||
|
},
|
||||||
|
values: new ArrayVector([72]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const outputSingle = [
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{ config: {}, name: 'Time', type: 'time', values: new ArrayVector([1645029699311]) },
|
||||||
|
{ config: { filterable: true }, name: 'level', type: 'string', values: new ArrayVector(['error']) },
|
||||||
|
{ config: { filterable: true }, name: 'location', type: 'string', values: new ArrayVector(['moon']) },
|
||||||
|
{ config: { filterable: true }, name: 'protocol', type: 'string', values: new ArrayVector(['http']) },
|
||||||
|
{ config: {}, name: 'Value #A', type: 'number', values: new ArrayVector([23]) },
|
||||||
|
],
|
||||||
|
length: 1,
|
||||||
|
meta: { preferredVisualisationType: 'table' },
|
||||||
|
refId: 'A',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const outputMulti = [
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{ config: {}, name: 'Time', type: 'time', values: new ArrayVector([1645029699311, 1645029699311]) },
|
||||||
|
{ config: { filterable: true }, name: 'level', type: 'string', values: new ArrayVector(['error', 'info']) },
|
||||||
|
{ config: { filterable: true }, name: 'location', type: 'string', values: new ArrayVector(['moon', 'moon']) },
|
||||||
|
{ config: { filterable: true }, name: 'protocol', type: 'string', values: new ArrayVector(['http', 'http']) },
|
||||||
|
{ config: {}, name: 'Value #A', type: 'number', values: new ArrayVector([23, 45]) },
|
||||||
|
],
|
||||||
|
length: 2,
|
||||||
|
meta: { preferredVisualisationType: 'table' },
|
||||||
|
refId: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{ config: {}, name: 'Time', type: 'time', values: new ArrayVector([1645029699311]) },
|
||||||
|
{ config: { filterable: true }, name: 'level', type: 'string', values: new ArrayVector(['error']) },
|
||||||
|
{ config: { filterable: true }, name: 'location', type: 'string', values: new ArrayVector(['moon']) },
|
||||||
|
{ config: { filterable: true }, name: 'protocol', type: 'string', values: new ArrayVector(['http']) },
|
||||||
|
{ config: {}, name: 'Value #B', type: 'number', values: new ArrayVector([72]) },
|
||||||
|
],
|
||||||
|
length: 1,
|
||||||
|
meta: { preferredVisualisationType: 'table' },
|
||||||
|
refId: 'B',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('loki backendResultTransformer', () => {
|
||||||
|
it('converts a single instant metric dataframe to table dataframe', () => {
|
||||||
|
const result = makeTableFrames([frame1]);
|
||||||
|
expect(result).toEqual(outputSingle);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts 3 instant metric dataframes into 2 tables', () => {
|
||||||
|
const result = makeTableFrames([frame1, frame2, frame3]);
|
||||||
|
expect(result).toEqual(outputMulti);
|
||||||
|
});
|
||||||
|
});
|
75
public/app/plugins/datasource/loki/makeTableFrames.ts
Normal file
75
public/app/plugins/datasource/loki/makeTableFrames.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { DataFrame, Field, FieldType, ArrayVector } from '@grafana/data';
|
||||||
|
import { groupBy } from 'lodash';
|
||||||
|
|
||||||
|
export function makeTableFrames(instantMetricFrames: DataFrame[]): DataFrame[] {
|
||||||
|
// first we remove frames that have no refId
|
||||||
|
// (we will group them by refId, so we need it to be set)
|
||||||
|
const framesWithRefId = instantMetricFrames.filter((f) => f.refId !== undefined);
|
||||||
|
|
||||||
|
const framesByRefId = groupBy(framesWithRefId, (frame) => frame.refId);
|
||||||
|
|
||||||
|
return Object.entries(framesByRefId).map(([refId, frames]) => makeTableFrame(frames, refId));
|
||||||
|
}
|
||||||
|
|
||||||
|
type NumberField = Field<number, ArrayVector<number>>;
|
||||||
|
type StringField = Field<string, ArrayVector<string>>;
|
||||||
|
|
||||||
|
function makeTableFrame(instantMetricFrames: DataFrame[], refId: string): DataFrame {
|
||||||
|
const tableTimeField: NumberField = { name: 'Time', config: {}, values: new ArrayVector(), type: FieldType.time };
|
||||||
|
const tableValueField: NumberField = {
|
||||||
|
name: `Value #${refId}`,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector(),
|
||||||
|
type: FieldType.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort metric labels, create columns for them and record their index
|
||||||
|
const allLabelNames = new Set(
|
||||||
|
instantMetricFrames.map((frame) => frame.fields.map((field) => Object.keys(field.labels ?? {})).flat()).flat()
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedLabelNames = Array.from(allLabelNames).sort();
|
||||||
|
|
||||||
|
const labelFields: StringField[] = sortedLabelNames.map((labelName) => ({
|
||||||
|
name: labelName,
|
||||||
|
config: { filterable: true },
|
||||||
|
values: new ArrayVector(),
|
||||||
|
type: FieldType.string,
|
||||||
|
}));
|
||||||
|
|
||||||
|
instantMetricFrames.forEach((frame) => {
|
||||||
|
const timeField = frame.fields.find((field) => field.type === FieldType.time);
|
||||||
|
const valueField = frame.fields.find((field) => field.type === FieldType.number);
|
||||||
|
if (timeField == null || valueField == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeArray = timeField.values.toArray();
|
||||||
|
const valueArray = valueField.values.toArray();
|
||||||
|
|
||||||
|
for (let x of timeArray) {
|
||||||
|
tableTimeField.values.add(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x of valueArray) {
|
||||||
|
tableValueField.values.add(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = valueField.labels ?? {};
|
||||||
|
|
||||||
|
for (let f of labelFields) {
|
||||||
|
const text = labels[f.name] ?? '';
|
||||||
|
// we insert the labels as many times as we have values
|
||||||
|
for (let i = 0; i < valueArray.length; i++) {
|
||||||
|
f.values.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields: [tableTimeField, ...labelFields, tableValueField],
|
||||||
|
refId,
|
||||||
|
meta: { preferredVisualisationType: 'table' },
|
||||||
|
length: tableTimeField.values.length,
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user