diff --git a/pkg/tsdb/loki/frame.go b/pkg/tsdb/loki/frame.go index 35314e22e65..5593c59ae57 100644 --- a/pkg/tsdb/loki/frame.go +++ b/pkg/tsdb/loki/frame.go @@ -2,6 +2,7 @@ package loki import ( "fmt" + "hash/fnv" "sort" "strings" "time" @@ -11,14 +12,42 @@ import ( // we adjust the dataframes to be the way frontend & alerting // wants them. -func adjustFrame(frame *data.Frame, query *lokiQuery) *data.Frame { +func adjustFrame(frame *data.Frame, query *lokiQuery) error { + fields := frame.Fields + + if len(fields) < 2 { + return fmt.Errorf("missing fields in frame") + } + + // metric-fields have "timefield, valuefield" + // logs-fields have "labelsfield, timefield, ..." + + secondField := fields[1] + + if secondField.Type() == data.FieldTypeFloat64 { + return adjustMetricFrame(frame, query) + } else { + return adjustLogsFrame(frame, query) + } +} + +func adjustMetricFrame(frame *data.Frame, query *lokiQuery) error { + fields := frame.Fields + // we check if the fields are of correct type + if len(fields) != 2 { + return fmt.Errorf("invalid fields in metric frame") + } + + timeField := fields[0] + valueField := fields[1] + + if (timeField.Type() != data.FieldTypeTime) || (valueField.Type() != data.FieldTypeFloat64) { + return fmt.Errorf("invalid fields in metric frame") + } + labels := getFrameLabels(frame) - timeFields, nonTimeFields := partitionFields(frame) - - isMetricFrame := nonTimeFields[0].Type() != data.FieldTypeString - - isMetricRange := isMetricFrame && query.QueryType == QueryTypeRange + isMetricRange := query.QueryType == QueryTypeRange name := formatName(labels, query) frame.Name = name @@ -33,48 +62,109 @@ func adjustFrame(frame *data.Frame, query *lokiQuery) *data.Frame { 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()) + if isMetricRange { + if timeField.Config == nil { + timeField.Config = &data.FieldConfig{} } + timeField.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 + if valueField.Config == nil { + valueField.Config = &data.FieldConfig{} + } + valueField.Config.DisplayNameFromDS = name + + return nil +} + +func adjustLogsFrame(frame *data.Frame, query *lokiQuery) error { + // we check if the fields are of correct type and length + fields := frame.Fields + if len(fields) != 3 { + return fmt.Errorf("invalid fields in logs frame") } - // for streams-dataframes, we need to send to the browser the nanosecond-precision timestamp too. + labelsField := fields[0] + timeField := fields[1] + lineField := fields[2] + + if (timeField.Type() != data.FieldTypeTime) || (lineField.Type() != data.FieldTypeString) || (labelsField.Type() != data.FieldTypeString) { + return fmt.Errorf("invalid fields in metric frame") + } + + if (timeField.Len() != lineField.Len()) || (timeField.Len() != labelsField.Len()) { + return fmt.Errorf("invalid fields in metric frame") + } + + if frame.Meta == nil { + frame.Meta = &data.FrameMeta{} + } + + frame.Meta.ExecutedQueryString = "Expr: " + query.Expr + + // we need to send to the browser the nanosecond-precision timestamp too. // usually timestamps become javascript-date-objects in the browser automatically, which only // have millisecond-precision. // so we send a separate timestamp-as-string field too. - if !isMetricFrame { - stringTimeField := makeStringTimeField(timeFields[0]) - frame.Fields = append(frame.Fields, stringTimeField) - } + stringTimeField := makeStringTimeField(timeField) - return frame + idField, err := makeIdField(stringTimeField, lineField, labelsField, frame.RefID) + if err != nil { + return err + } + frame.Fields = append(frame.Fields, stringTimeField, idField) + return nil } -func makeStringTimeField(field *data.Field) *data.Field { - length := field.Len() +func makeStringTimeField(timeField *data.Field) *data.Field { + length := timeField.Len() stringTimestamps := make([]string, length) for i := 0; i < length; i++ { - if v, ok := field.ConcreteAt(i); ok { - nsNumber := v.(time.Time).UnixNano() - stringTimestamps[i] = fmt.Sprintf("%d", nsNumber) - } + nsNumber := timeField.At(i).(time.Time).UnixNano() + stringTimestamps[i] = fmt.Sprintf("%d", nsNumber) } - return data.NewField("tsNs", field.Labels.Copy(), stringTimestamps) + return data.NewField("tsNs", timeField.Labels.Copy(), stringTimestamps) +} + +func calculateCheckSum(time string, line string, labels string) (string, error) { + input := []byte(line + "_" + labels) + hash := fnv.New32() + _, err := hash.Write(input) + if err != nil { + return "", err + } + return fmt.Sprintf("%s_%x", time, hash.Sum32()), nil +} + +func makeIdField(stringTimeField *data.Field, lineField *data.Field, labelsField *data.Field, refId string) (*data.Field, error) { + length := stringTimeField.Len() + + ids := make([]string, length) + + checksums := make(map[string]int) + + for i := 0; i < length; i++ { + time := stringTimeField.At(i).(string) + line := lineField.At(i).(string) + labels := labelsField.At(i).(string) + + sum, err := calculateCheckSum(time, line, labels) + if err != nil { + return nil, err + } + + sumCount := checksums[sum] + idSuffix := "" + if sumCount > 0 { + // we had this checksum already, we need to do something to make it unique + idSuffix = fmt.Sprintf("_%d", sumCount) + } + checksums[sum] = sumCount + 1 + + ids[i] = sum + idSuffix + "_" + refId + } + return data.NewField("id", nil, ids), nil } func formatNamePrometheusStyle(labels map[string]string) string { @@ -119,18 +209,3 @@ func getFrameLabels(frame *data.Frame) map[string]string { 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 -} diff --git a/pkg/tsdb/loki/frame_test.go b/pkg/tsdb/loki/frame_test.go index ee2d0a74637..0e4bf3ebcfc 100644 --- a/pkg/tsdb/loki/frame_test.go +++ b/pkg/tsdb/loki/frame_test.go @@ -37,7 +37,56 @@ func TestFormatName(t *testing.T) { } func TestAdjustFrame(t *testing.T) { - t.Run("response should be parsed normally", func(t *testing.T) { + t.Run("logs-frame metadata should be set correctly", func(t *testing.T) { + frame := data.NewFrame("", + data.NewField("labels", nil, []string{ + `{"level":"info"}`, + `{"level":"error"}`, + `{"level":"error"}`, + `{"level":"info"}`, + }), + data.NewField("time", nil, []time.Time{ + time.Date(2022, 1, 2, 3, 4, 5, 6, time.UTC), + time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC), + time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC), + time.Date(2022, 1, 2, 3, 6, 5, 6, time.UTC), + }), + data.NewField("line", nil, []string{"line1", "line2", "line2", "line3"}), + ) + + frame.RefID = "A" + + query := &lokiQuery{ + Expr: `{type="important"}`, + QueryType: QueryTypeRange, + } + + err := adjustFrame(frame, query) + require.NoError(t, err) + + fields := frame.Fields + + require.Equal(t, 5, len(fields)) + tsNsField := fields[3] + require.Equal(t, "tsNs", tsNsField.Name) + require.Equal(t, data.FieldTypeString, tsNsField.Type()) + require.Equal(t, 4, tsNsField.Len()) + require.Equal(t, "1641092645000000006", tsNsField.At(0)) + require.Equal(t, "1641092705000000006", tsNsField.At(1)) + require.Equal(t, "1641092705000000006", tsNsField.At(2)) + require.Equal(t, "1641092765000000006", tsNsField.At(3)) + + idField := fields[4] + require.Equal(t, "id", idField.Name) + require.Equal(t, data.FieldTypeString, idField.Type()) + require.Equal(t, 4, idField.Len()) + require.Equal(t, "1641092645000000006_a36f4e1b_A", idField.At(0)) + require.Equal(t, "1641092705000000006_1d77c9ca_A", idField.At(1)) + require.Equal(t, "1641092705000000006_1d77c9ca_1_A", idField.At(2)) + require.Equal(t, "1641092765000000006_948c1a7d_A", idField.At(3)) + }) + + t.Run("logs-frame id and string-time fields should be created", 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"} @@ -52,7 +101,8 @@ func TestAdjustFrame(t *testing.T) { Step: time.Second * 42, } - adjustFrame(frame, query) + err := adjustFrame(frame, query) + require.NoError(t, err) require.Equal(t, frame.Name, "legend Application") require.Equal(t, frame.Meta.ExecutedQueryString, "Expr: up(ALERTS)\nStep: 42s") @@ -72,7 +122,8 @@ func TestAdjustFrame(t *testing.T) { frame := data.NewFrame("test", field1, field2) frame.SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMany}) - adjustFrame(frame, query) + err := adjustFrame(frame, query) + require.NoError(t, err) // to keep the test simple, we assume the // first field is the time-field diff --git a/pkg/tsdb/loki/parse_response.go b/pkg/tsdb/loki/parse_response.go index 48eae74861e..73ed248c4d5 100644 --- a/pkg/tsdb/loki/parse_response.go +++ b/pkg/tsdb/loki/parse_response.go @@ -2,11 +2,13 @@ package loki import ( "fmt" + "sort" "time" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/loki/pkg/loghttp" "github.com/grafana/loki/pkg/logqlmodel/stats" + jsoniter "github.com/json-iterator/go" ) func parseResponse(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) { @@ -17,7 +19,10 @@ func parseResponse(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, } for _, frame := range frames { - adjustFrame(frame, query) + err = adjustFrame(frame, query) + if err != nil { + return nil, err + } } return frames, nil @@ -31,7 +36,7 @@ func lokiResponseToDataFrames(value *loghttp.QueryResponse, query *lokiQuery) (d case loghttp.Vector: return lokiVectorToDataFrames(res, query, stats), nil case loghttp.Streams: - return lokiStreamsToDataFrames(res, query, stats), nil + return lokiStreamsToDataFrames(res, query, stats) default: return nil, fmt.Errorf("resultType %T not supported{", res) } @@ -54,8 +59,8 @@ func lokiMatrixToDataFrames(matrix loghttp.Matrix, query *lokiQuery, stats []dat values = append(values, float64(k.Value)) } - timeField := data.NewField("", nil, timeVector) - valueField := data.NewField("", tags, values) + timeField := data.NewField("time", nil, timeVector) + valueField := data.NewField("value", tags, values) frame := data.NewFrame("", timeField, valueField) frame.SetMeta(&data.FrameMeta{ @@ -79,8 +84,8 @@ func lokiVectorToDataFrames(vector loghttp.Vector, query *lokiQuery, stats []dat for k, v := range v.Metric { tags[string(k)] = string(v) } - timeField := data.NewField("", nil, timeVector) - valueField := data.NewField("", tags, values) + timeField := data.NewField("time", nil, timeVector) + valueField := data.NewField("value", tags, values) frame := data.NewFrame("", timeField, valueField) frame.SetMeta(&data.FrameMeta{ @@ -93,35 +98,61 @@ func lokiVectorToDataFrames(vector loghttp.Vector, query *lokiQuery, stats []dat return frames } -func lokiStreamsToDataFrames(streams loghttp.Streams, query *lokiQuery, stats []data.QueryStat) data.Frames { - frames := data.Frames{} +// we serialize the labels as an ordered list of pairs +func labelsToString(labels data.Labels) (string, error) { + keys := make([]string, 0, len(labels)) + for k := range labels { + keys = append(keys, k) + } + sort.Strings(keys) + + labelArray := make([][2]string, 0, len(labels)) + + for _, k := range keys { + pair := [2]string{k, labels[k]} + labelArray = append(labelArray, pair) + } + + bytes, err := jsoniter.Marshal(labelArray) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +func lokiStreamsToDataFrames(streams loghttp.Streams, query *lokiQuery, stats []data.QueryStat) (data.Frames, error) { + var timeVector []time.Time + var values []string + var labelsVector []string 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 + labelsText, err := labelsToString(v.Labels.Map()) + if err != nil { + return nil, err } for _, k := range v.Entries { timeVector = append(timeVector, k.Timestamp.UTC()) values = append(values, k.Line) + labelsVector = append(labelsVector, labelsText) } - - timeField := data.NewField("", nil, timeVector) - valueField := data.NewField("", tags, values) - - frame := data.NewFrame("", timeField, valueField) - frame.SetMeta(&data.FrameMeta{ - Stats: stats, - }) - - frames = append(frames, frame) } - return frames + timeField := data.NewField("ts", nil, timeVector) + valueField := data.NewField("line", nil, values) + labelsField := data.NewField("labels", nil, labelsVector) + labelsField.Config = &data.FieldConfig{ + // we should have a native json-field-type + Custom: map[string]interface{}{"json": true}, + } + + frame := data.NewFrame("", labelsField, timeField, valueField) + frame.SetMeta(&data.FrameMeta{ + Stats: stats, + }) + + return data.Frames{frame}, nil } func parseStats(result stats.Result) []data.QueryStat { diff --git a/pkg/tsdb/loki/testdata/streams_simple.golden.txt b/pkg/tsdb/loki/testdata/streams_simple.golden.txt index 76204b3a22f..dff97cb0e40 100644 --- a/pkg/tsdb/loki/testdata/streams_simple.golden.txt +++ b/pkg/tsdb/loki/testdata/streams_simple.golden.txt @@ -111,143 +111,21 @@ Frame[0] { ], "executedQueryString": "Expr: query1" } -Name: {level="error", location="moon"} -Dimensions: 3 Fields by 1 Rows -+----------------------------------------+------------------------------------+---------------------+ -| Name: time | Name: value | Name: tsNs | -| Labels: | Labels: level=error, location=moon | Labels: | -| Type: []time.Time | Type: []string | Type: []string | -+----------------------------------------+------------------------------------+---------------------+ -| 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | -+----------------------------------------+------------------------------------+---------------------+ - - - -Frame[1] { - "stats": [ - { - "displayName": "Summary: bytes processed per second", - "unit": "Bps", - "value": 3507022 - }, - { - "displayName": "Summary: lines processed per second", - "value": 24818 - }, - { - "displayName": "Summary: total bytes processed", - "unit": "decbytes", - "value": 7772 - }, - { - "displayName": "Summary: total lines processed", - "value": 55 - }, - { - "displayName": "Summary: exec time", - "unit": "s", - "value": 0.002216125 - }, - { - "displayName": "Store: total chunks ref", - "value": 2 - }, - { - "displayName": "Store: total chunks downloaded", - "value": 3 - }, - { - "displayName": "Store: chunks download time", - "unit": "s", - "value": 0.000390958 - }, - { - "displayName": "Store: head chunk bytes", - "unit": "decbytes", - "value": 4 - }, - { - "displayName": "Store: head chunk lines", - "value": 5 - }, - { - "displayName": "Store: decompressed bytes", - "unit": "decbytes", - "value": 7772 - }, - { - "displayName": "Store: decompressed lines", - "value": 55 - }, - { - "displayName": "Store: compressed bytes", - "unit": "decbytes", - "value": 31432 - }, - { - "displayName": "Store: total duplicates", - "value": 6 - }, - { - "displayName": "Ingester: total reached", - "value": 7 - }, - { - "displayName": "Ingester: total chunks matched", - "value": 8 - }, - { - "displayName": "Ingester: total batches", - "value": 9 - }, - { - "displayName": "Ingester: total lines sent", - "value": 10 - }, - { - "displayName": "Ingester: head chunk bytes", - "unit": "decbytes", - "value": 11 - }, - { - "displayName": "Ingester: head chunk lines", - "value": 12 - }, - { - "displayName": "Ingester: decompressed bytes", - "unit": "decbytes", - "value": 13 - }, - { - "displayName": "Ingester: decompressed lines", - "value": 14 - }, - { - "displayName": "Ingester: compressed bytes", - "unit": "decbytes", - "value": 15 - }, - { - "displayName": "Ingester: total duplicates", - "value": 16 - } - ], - "executedQueryString": "Expr: query1" -} -Name: {level="info", location="moon"} -Dimensions: 3 Fields by 4 Rows -+-----------------------------------------+-----------------------------------+---------------------+ -| Name: time | Name: value | Name: tsNs | -| Labels: | Labels: level=info, location=moon | Labels: | -| Type: []time.Time | Type: []string | Type: []string | -+-----------------------------------------+-----------------------------------+---------------------+ -| 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | -| 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | -| 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | -+-----------------------------------------+-----------------------------------+---------------------+ +Name: +Dimensions: 5 Fields by 6 Rows ++-------------------------------------------+-----------------------------------------+------------------+---------------------+---------------------------------+ +| Name: labels | Name: ts | Name: line | Name: tsNs | Name: id | +| Labels: | Labels: | Labels: | Labels: | Labels: | +| Type: []string | Type: []time.Time | Type: []string | Type: []string | Type: []string | ++-------------------------------------------+-----------------------------------------+------------------+---------------------+---------------------------------+ +| [["code","one\","],["location","moonπŸŒ™"]] | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | 1645030244810757120_f5088206_ | +| [["code","\",two"],["location","moonπŸŒ™"]] | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | 1645030247027735040_46b225a4_ | +| [["code","\",two"],["location","moonπŸŒ™"]] | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | 1645030246277587968_c2a9f32b_ | +| [["code","\",two"],["location","moonπŸŒ™"]] | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | 1645030246277587968_c2a9f32b_1_ | +| [["code","\",two"],["location","moonπŸŒ™"]] | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | 1645030245539423744_86502e6_ | +| [["code","\",two"],["location","moonπŸŒ™"]] | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | 1645030244091700992_c6f5b60d_ | ++-------------------------------------------+-----------------------------------------+------------------+---------------------+---------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////AAkAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAMQGAAADAAAAbAAAACgAAAAEAAAAlPf//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC09///CAAAACwAAAAgAAAAe2xldmVsPSJlcnJvciIsIGxvY2F0aW9uPSJtb29uIn0AAAAABAAAAG5hbWUAAAAA9Pf//wgAAAA8BgAAMgYAAHsic3RhdHMiOlt7ImRpc3BsYXlOYW1lIjoiU3VtbWFyeTogYnl0ZXMgcHJvY2Vzc2VkIHBlciBzZWNvbmQiLCJ1bml0IjoiQnBzIiwidmFsdWUiOjM1MDcwMjJ9LHsiZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiBsaW5lcyBwcm9jZXNzZWQgcGVyIHNlY29uZCIsInZhbHVlIjoyNDgxOH0seyJkaXNwbGF5TmFtZSI6IlN1bW1hcnk6IHRvdGFsIGJ5dGVzIHByb2Nlc3NlZCIsInVuaXQiOiJkZWNieXRlcyIsInZhbHVlIjo3NzcyfSx7ImRpc3BsYXlOYW1lIjoiU3VtbWFyeTogdG90YWwgbGluZXMgcHJvY2Vzc2VkIiwidmFsdWUiOjU1fSx7ImRpc3BsYXlOYW1lIjoiU3VtbWFyeTogZXhlYyB0aW1lIiwidW5pdCI6InMiLCJ2YWx1ZSI6MC4wMDIyMTYxMjV9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogdG90YWwgY2h1bmtzIHJlZiIsInZhbHVlIjoyfSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IHRvdGFsIGNodW5rcyBkb3dubG9hZGVkIiwidmFsdWUiOjN9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogY2h1bmtzIGRvd25sb2FkIHRpbWUiLCJ1bml0IjoicyIsInZhbHVlIjowLjAwMDM5MDk1OH0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBoZWFkIGNodW5rIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjR9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogaGVhZCBjaHVuayBsaW5lcyIsInZhbHVlIjo1fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGRlY29tcHJlc3NlZCBieXRlcyIsInVuaXQiOiJkZWNieXRlcyIsInZhbHVlIjo3NzcyfSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGRlY29tcHJlc3NlZCBsaW5lcyIsInZhbHVlIjo1NX0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBjb21wcmVzc2VkIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjMxNDMyfSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IHRvdGFsIGR1cGxpY2F0ZXMiLCJ2YWx1ZSI6Nn0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCByZWFjaGVkIiwidmFsdWUiOjd9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogdG90YWwgY2h1bmtzIG1hdGNoZWQiLCJ2YWx1ZSI6OH0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCBiYXRjaGVzIiwidmFsdWUiOjl9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogdG90YWwgbGluZXMgc2VudCIsInZhbHVlIjoxMH0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiBoZWFkIGNodW5rIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjExfSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGhlYWQgY2h1bmsgbGluZXMiLCJ2YWx1ZSI6MTJ9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogZGVjb21wcmVzc2VkIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjEzfSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGRlY29tcHJlc3NlZCBsaW5lcyIsInZhbHVlIjoxNH0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiBjb21wcmVzc2VkIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjE1fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIGR1cGxpY2F0ZXMiLCJ2YWx1ZSI6MTZ9XSwiZXhlY3V0ZWRRdWVyeVN0cmluZyI6IkV4cHI6IHF1ZXJ5MSJ9AAAEAAAAbWV0YQAAAAADAAAAmAEAAIQAAAAEAAAAhv7//xQAAABgAAAAYAAAAAAAAAVcAAAAAgAAACwAAAAEAAAAeP7//wgAAAAQAAAABAAAAHRzTnMAAAAABAAAAG5hbWUAAAAAnP7//wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAABP///wQAAAB0c05zAAAAAAL///8UAAAA4AAAAOQAAAAAAAAF4AAAAAMAAABwAAAALAAAAAQAAAD4/v//CAAAABAAAAAFAAAAdmFsdWUAAAAEAAAAbmFtZQAAAAAc////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAXP///wgAAABIAAAAPAAAAHsiZGlzcGxheU5hbWVGcm9tRFMiOiJ7bGV2ZWw9XCJlcnJvclwiLCBsb2NhdGlvbj1cIm1vb25cIn0ifQAAAAAGAAAAY29uZmlnAAAAAAAABAAEAAQAAAAFAAAAdmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAAB0aW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAHRpbWUAAAAAAAAAAP////8IAQAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAQAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAmAAAAAEAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAgAAAAAAAAAKAAAAAAAAAATAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAABS4ukpS1BYAAAAAEAAAAGxvZyBsaW5lIGVycm9yIDEAAAAAEwAAADE2NDUwMzAyNDQ4MTA3NTcxMjAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAAQCQAAAAAAABABAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAxAYAAAMAAABsAAAAKAAAAAQAAACU9///CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAALT3//8IAAAALAAAACAAAAB7bGV2ZWw9ImVycm9yIiwgbG9jYXRpb249Im1vb24ifQAAAAAEAAAAbmFtZQAAAAD09///CAAAADwGAAAyBgAAeyJzdGF0cyI6W3siZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiBieXRlcyBwcm9jZXNzZWQgcGVyIHNlY29uZCIsInVuaXQiOiJCcHMiLCJ2YWx1ZSI6MzUwNzAyMn0seyJkaXNwbGF5TmFtZSI6IlN1bW1hcnk6IGxpbmVzIHByb2Nlc3NlZCBwZXIgc2Vjb25kIiwidmFsdWUiOjI0ODE4fSx7ImRpc3BsYXlOYW1lIjoiU3VtbWFyeTogdG90YWwgYnl0ZXMgcHJvY2Vzc2VkIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjc3NzJ9LHsiZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiB0b3RhbCBsaW5lcyBwcm9jZXNzZWQiLCJ2YWx1ZSI6NTV9LHsiZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiBleGVjIHRpbWUiLCJ1bml0IjoicyIsInZhbHVlIjowLjAwMjIxNjEyNX0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiB0b3RhbCBjaHVua3MgcmVmIiwidmFsdWUiOjJ9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogdG90YWwgY2h1bmtzIGRvd25sb2FkZWQiLCJ2YWx1ZSI6M30seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBjaHVua3MgZG93bmxvYWQgdGltZSIsInVuaXQiOiJzIiwidmFsdWUiOjAuMDAwMzkwOTU4fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGhlYWQgY2h1bmsgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6NH0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBoZWFkIGNodW5rIGxpbmVzIiwidmFsdWUiOjV9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogZGVjb21wcmVzc2VkIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjc3NzJ9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogZGVjb21wcmVzc2VkIGxpbmVzIiwidmFsdWUiOjU1fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MzE0MzJ9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogdG90YWwgZHVwbGljYXRlcyIsInZhbHVlIjo2fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIHJlYWNoZWQiLCJ2YWx1ZSI6N30seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCBjaHVua3MgbWF0Y2hlZCIsInZhbHVlIjo4fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIGJhdGNoZXMiLCJ2YWx1ZSI6OX0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCBsaW5lcyBzZW50IiwidmFsdWUiOjEwfSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGhlYWQgY2h1bmsgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MTF9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogaGVhZCBjaHVuayBsaW5lcyIsInZhbHVlIjoxMn0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiBkZWNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MTN9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogZGVjb21wcmVzc2VkIGxpbmVzIiwidmFsdWUiOjE0fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MTV9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogdG90YWwgZHVwbGljYXRlcyIsInZhbHVlIjoxNn1dLCJleGVjdXRlZFF1ZXJ5U3RyaW5nIjoiRXhwcjogcXVlcnkxIn0AAAQAAABtZXRhAAAAAAMAAACYAQAAhAAAAAQAAACG/v//FAAAAGAAAABgAAAAAAAABVwAAAACAAAALAAAAAQAAAB4/v//CAAAABAAAAAEAAAAdHNOcwAAAAAEAAAAbmFtZQAAAACc/v//CAAAAAwAAAACAAAAe30AAAYAAABsYWJlbHMAAAAAAAAE////BAAAAHRzTnMAAAAAAv///xQAAADgAAAA5AAAAAAAAAXgAAAAAwAAAHAAAAAsAAAABAAAAPj+//8IAAAAEAAAAAUAAAB2YWx1ZQAAAAQAAABuYW1lAAAAABz///8IAAAALAAAACMAAAB7ImxldmVsIjoiZXJyb3IiLCJsb2NhdGlvbiI6Im1vb24ifQAGAAAAbGFiZWxzAABc////CAAAAEgAAAA8AAAAeyJkaXNwbGF5TmFtZUZyb21EUyI6IntsZXZlbD1cImVycm9yXCIsIGxvY2F0aW9uPVwibW9vblwifSJ9AAAAAAYAAABjb25maWcAAAAAAAAEAAQABAAAAAUAAAB2YWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAHRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAdGltZQAAAAAoCQAAQVJST1cx -FRAME=QVJST1cxAAD/////+AgAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAMAGAAADAAAAaAAAACgAAAAEAAAAnPf//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC89///CAAAACgAAAAfAAAAe2xldmVsPSJpbmZvIiwgbG9jYXRpb249Im1vb24ifQAEAAAAbmFtZQAAAAD49///CAAAADwGAAAyBgAAeyJzdGF0cyI6W3siZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiBieXRlcyBwcm9jZXNzZWQgcGVyIHNlY29uZCIsInVuaXQiOiJCcHMiLCJ2YWx1ZSI6MzUwNzAyMn0seyJkaXNwbGF5TmFtZSI6IlN1bW1hcnk6IGxpbmVzIHByb2Nlc3NlZCBwZXIgc2Vjb25kIiwidmFsdWUiOjI0ODE4fSx7ImRpc3BsYXlOYW1lIjoiU3VtbWFyeTogdG90YWwgYnl0ZXMgcHJvY2Vzc2VkIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjc3NzJ9LHsiZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiB0b3RhbCBsaW5lcyBwcm9jZXNzZWQiLCJ2YWx1ZSI6NTV9LHsiZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiBleGVjIHRpbWUiLCJ1bml0IjoicyIsInZhbHVlIjowLjAwMjIxNjEyNX0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiB0b3RhbCBjaHVua3MgcmVmIiwidmFsdWUiOjJ9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogdG90YWwgY2h1bmtzIGRvd25sb2FkZWQiLCJ2YWx1ZSI6M30seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBjaHVua3MgZG93bmxvYWQgdGltZSIsInVuaXQiOiJzIiwidmFsdWUiOjAuMDAwMzkwOTU4fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGhlYWQgY2h1bmsgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6NH0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBoZWFkIGNodW5rIGxpbmVzIiwidmFsdWUiOjV9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogZGVjb21wcmVzc2VkIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjc3NzJ9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogZGVjb21wcmVzc2VkIGxpbmVzIiwidmFsdWUiOjU1fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MzE0MzJ9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogdG90YWwgZHVwbGljYXRlcyIsInZhbHVlIjo2fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIHJlYWNoZWQiLCJ2YWx1ZSI6N30seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCBjaHVua3MgbWF0Y2hlZCIsInZhbHVlIjo4fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIGJhdGNoZXMiLCJ2YWx1ZSI6OX0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCBsaW5lcyBzZW50IiwidmFsdWUiOjEwfSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGhlYWQgY2h1bmsgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MTF9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogaGVhZCBjaHVuayBsaW5lcyIsInZhbHVlIjoxMn0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiBkZWNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MTN9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogZGVjb21wcmVzc2VkIGxpbmVzIiwidmFsdWUiOjE0fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MTV9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogdG90YWwgZHVwbGljYXRlcyIsInZhbHVlIjoxNn1dLCJleGVjdXRlZFF1ZXJ5U3RyaW5nIjoiRXhwcjogcXVlcnkxIn0AAAQAAABtZXRhAAAAAAMAAACUAQAA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IlN0b3JlOiBoZWFkIGNodW5rIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjR9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogaGVhZCBjaHVuayBsaW5lcyIsInZhbHVlIjo1fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGRlY29tcHJlc3NlZCBieXRlcyIsInVuaXQiOiJkZWNieXRlcyIsInZhbHVlIjo3NzcyfSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGRlY29tcHJlc3NlZCBsaW5lcyIsInZhbHVlIjo1NX0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBjb21wcmVzc2VkIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjMxNDMyfSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IHRvdGFsIGR1cGxpY2F0ZXMiLCJ2YWx1ZSI6Nn0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCByZWFjaGVkIiwidmFsdWUiOjd9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogdG90YWwgY2h1bmtzIG1hdGNoZWQiLCJ2YWx1ZSI6OH0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCBiYXRjaGVzIiwidmFsdWUiOjl9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogdG90YWwgbGluZXMgc2VudCIsInZhbHVlIjoxMH0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiBoZWFkIGNodW5rIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjExfSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGhlYWQgY2h1bmsgbGluZXMiLCJ2YWx1ZSI6MTJ9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogZGVjb21wcmVzc2VkIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjEzfSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGRlY29tcHJlc3NlZCBsaW5lcyIsInZhbHVlIjoxNH0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiBjb21wcmVzc2VkIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjE1fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIGR1cGxpY2F0ZXMiLCJ2YWx1ZSI6MTZ9XSwiZXhlY3V0ZWRRdWVyeVN0cmluZyI6IkV4cHI6IHF1ZXJ5MSJ9AAAEAAAAbWV0YQAAAAADAAAAlAEAAIQAAAAEAAAAiv7//xQAAABgAAAAYAAAAAAAAAVcAAAAAgAAACwAAAAEAAAAfP7//wgAAAAQAAAABAAAAHRzTnMAAAAABAAAAG5hbWUAAAAAoP7//wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAACP///wQAAAB0c05zAAAAAAb///8UAAAA3AAAAOAAAAAAAAAF3AAAAAMAAABwAAAALAAAAAQAAAD8/v//CAAAABAAAAAFAAAAdmFsdWUAAAAEAAAAbmFtZQAAAAAg////CAAAACwAAAAiAAAAeyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifQAABgAAAGxhYmVscwAAYP///wgAAABEAAAAOwAAAHsiZGlzcGxheU5hbWVGcm9tRFMiOiJ7bGV2ZWw9XCJpbmZvXCIsIGxvY2F0aW9uPVwibW9vblwifSJ9AAYAAABjb25maWcAAAAAAAAEAAQABAAAAAUAAAB2YWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAHRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAdGltZQAAAAAgCQAAQVJST1cx +FRAME=QVJST1cxAAD/////IAkAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKQGAAADAAAATAAAACgAAAAEAAAAfPf//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACc9///CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAALz3//8IAAAAPAYAADIGAAB7InN0YXRzIjpbeyJkaXNwbGF5TmFtZSI6IlN1bW1hcnk6IGJ5dGVzIHByb2Nlc3NlZCBwZXIgc2Vjb25kIiwidW5pdCI6IkJwcyIsInZhbHVlIjozNTA3MDIyfSx7ImRpc3BsYXlOYW1lIjoiU3VtbWFyeTogbGluZXMgcHJvY2Vzc2VkIHBlciBzZWNvbmQiLCJ2YWx1ZSI6MjQ4MTh9LHsiZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiB0b3RhbCBieXRlcyBwcm9jZXNzZWQiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6Nzc3Mn0seyJkaXNwbGF5TmFtZSI6IlN1bW1hcnk6IHRvdGFsIGxpbmVzIHByb2Nlc3NlZCIsInZhbHVlIjo1NX0seyJkaXNwbGF5TmFtZSI6IlN1bW1hcnk6IGV4ZWMgdGltZSIsInVuaXQiOiJzIiwidmFsdWUiOjAuMDAyMjE2MTI1fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IHRvdGFsIGNodW5rcyByZWYiLCJ2YWx1ZSI6Mn0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiB0b3RhbCBjaHVua3MgZG93bmxvYWRlZCIsInZhbHVlIjozfSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGNodW5rcyBkb3dubG9hZCB0aW1lIiwidW5pdCI6InMiLCJ2YWx1ZSI6MC4wMDAzOTA5NTh9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogaGVhZCBjaHVuayBieXRlcyIsInVuaXQiOiJkZWNieXRlcyIsInZhbHVlIjo0fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGhlYWQgY2h1bmsgbGluZXMiLCJ2YWx1ZSI6NX0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBkZWNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6Nzc3Mn0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBkZWNvbXByZXNzZWQgbGluZXMiLCJ2YWx1ZSI6NTV9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogY29tcHJlc3NlZCBieXRlcyIsInVuaXQiOiJkZWNieXRlcyIsInZhbHVlIjozMTQzMn0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiB0b3RhbCBkdXBsaWNhdGVzIiwidmFsdWUiOjZ9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogdG90YWwgcmVhY2hlZCIsInZhbHVlIjo3fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIGNodW5rcyBtYXRjaGVkIiwidmFsdWUiOjh9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogdG90YWwgYmF0Y2hlcyIsInZhbHVlIjo5fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIGxpbmVzIHNlbnQiLCJ2YWx1ZSI6MTB9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogaGVhZCBjaHVuayBieXRlcyIsInVuaXQiOiJkZWNieXRlcyIsInZhbHVlIjoxMX0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiBoZWFkIGNodW5rIGxpbmVzIiwidmFsdWUiOjEyfSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGRlY29tcHJlc3NlZCBieXRlcyIsInVuaXQiOiJkZWNieXRlcyIsInZhbHVlIjoxM30seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiBkZWNvbXByZXNzZWQgbGluZXMiLCJ2YWx1ZSI6MTR9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogY29tcHJlc3NlZCBieXRlcyIsInVuaXQiOiJkZWNieXRlcyIsInZhbHVlIjoxNX0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCBkdXBsaWNhdGVzIiwidmFsdWUiOjE2fV0sImV4ZWN1dGVkUXVlcnlTdHJpbmciOiJFeHByOiBxdWVyeTEifQAABAAAAG1ldGEAAAAABQAAAKgBAAA0AQAA2AAAAFgAAAAEAAAAfv7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAABE/v//CAAAAAwAAAACAAAAaWQAAAQAAABuYW1lAAAAAAAAAAAk/v//AgAAAGlkAADO/v//FAAAAGAAAABgAAAAAAAABVwAAAACAAAALAAAAAQAAACY/v//CAAAABAAAAAEAAAAdHNOcwAAAAAEAAAAbmFtZQAAAAC8/v//CAAAAAwAAAACAAAAe30AAAYAAABsYWJlbHMAAAAAAACc/v//BAAAAHRzTnMAAAAASv///xQAAAA8AAAAPAAAAAAAAAU4AAAAAQAAAAQAAAAQ////CAAAABAAAAAEAAAAbGluZQAAAAAEAAAAbmFtZQAAAAAAAAAA9P7//wQAAABsaW5lAAAAAKL///8UAAAAOAAAAEAAAAAAAAAKQAAAAAEAAAAEAAAAaP///wgAAAAMAAAAAgAAAHRzAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAIAAAB0cwAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAIAAAACEAAAAAAAABYAAAAACAAAANAAAAAQAAADc////CAAAABAAAAAGAAAAbGFiZWxzAAAEAAAAbmFtZQAAAAAIAAwACAAEAAgAAAAIAAAAJAAAABgAAAB7ImN1c3RvbSI6eyJqc29uIjp0cnVlfX0AAAAABgAAAGNvbmZpZwAAAAAAAAQABAAEAAAABgAAAGxhYmVscwAA/////4gBAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAABAAwAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAAD4AAAABgAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAIAAAAAAAAAACAQAAAAAAACgBAAAAAAAAAAAAAAAAAAAoAQAAAAAAADAAAAAAAAAAWAEAAAAAAAAAAAAAAAAAAFgBAAAAAAAAHAAAAAAAAAB4AQAAAAAAAFsAAAAAAAAA2AEAAAAAAAAAAAAAAAAAANgBAAAAAAAAHAAAAAAAAAD4AQAAAAAAAHIAAAAAAAAAcAIAAAAAAAAAAAAAAAAAAHACAAAAAAAAHAAAAAAAAACQAgAAAAAAAK8AAAAAAAAAAAAAAAUAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAArAAAAVgAAAIEAAACsAAAA1wAAAAIBAAAAAAAAW1siY29kZSIsIm9uZVwiLCJdLFsibG9jYXRpb24iLCJtb29u8J+MmSJdXVtbImNvZGUiLCJcIix0d28iXSxbImxvY2F0aW9uIiwibW9vbvCfjJkiXV1bWyJjb2RlIiwiXCIsdHdvIl0sWyJsb2NhdGlvbiIsIm1vb27wn4yZIl1dW1siY29kZSIsIlwiLHR3byJdLFsibG9jYXRpb24iLCJtb29u8J+MmSJdXVtbImNvZGUiLCJcIix0d28iXSxbImxvY2F0aW9uIiwibW9vbvCfjJkiXV1bWyJjb2RlIiwiXCIsdHdvIl0sWyJsb2NhdGlvbiIsIm1vb27wn4yZIl1dAAAAAAAAABS4ukpS1BYAetw+S1LUFgAkJhJLUtQWACQmEktS1BYApibmSlLUFgAn3I9KUtQWAAAAABAAAAAfAAAALgAAAD0AAABMAAAAWwAAAAAAAABsb2cgbGluZSBlcnJvciAxbG9nIGxpbmUgaW5mbyAxbG9nIGxpbmUgaW5mbyAybG9nIGxpbmUgaW5mbyAybG9nIGxpbmUgaW5mbyAzbG9nIGxpbmUgaW5mbyA0AAAAAAAAAAAAEwAAACYAAAA5AAAATAAAAF8AAAByAAAAAAAAADE2NDUwMzAyNDQ4MTA3NTcxMjAxNjQ1MDMwMjQ3MDI3NzM1MDQwMTY0NTAzMDI0NjI3NzU4Nzk2ODE2NDUwMzAyNDYyNzc1ODc5NjgxNjQ1MDMwMjQ1NTM5NDIzNzQ0MTY0NTAzMDI0NDA5MTcwMDk5MgAAAAAAAAAAAAAdAAAAOgAAAFcAAAB2AAAAkgAAAK8AAAAAAAAAMTY0NTAzMDI0NDgxMDc1NzEyMF9mNTA4ODIwNl8xNjQ1MDMwMjQ3MDI3NzM1MDQwXzQ2YjIyNWE0XzE2NDUwMzAyNDYyNzc1ODc5NjhfYzJhOWYzMmJfMTY0NTAzMDI0NjI3NzU4Nzk2OF9jMmE5ZjMyYl8xXzE2NDUwMzAyNDU1Mzk0MjM3NDRfODY1MDJlNl8xNjQ1MDMwMjQ0MDkxNzAwOTkyX2M2ZjViNjBkXwAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAPAAAAAAABAABAAAAMAkAAAAAAACQAQAAAAAAAEADAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACkBgAAAwAAAEwAAAAoAAAABAAAAHz3//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAnPf//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAC89///CAAAADwGAAAyBgAAeyJzdGF0cyI6W3siZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiBieXRlcyBwcm9jZXNzZWQgcGVyIHNlY29uZCIsInVuaXQiOiJCcHMiLCJ2YWx1ZSI6MzUwNzAyMn0seyJkaXNwbGF5TmFtZSI6IlN1bW1hcnk6IGxpbmVzIHByb2Nlc3NlZCBwZXIgc2Vjb25kIiwidmFsdWUiOjI0ODE4fSx7ImRpc3BsYXlOYW1lIjoiU3VtbWFyeTogdG90YWwgYnl0ZXMgcHJvY2Vzc2VkIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjc3NzJ9LHsiZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiB0b3RhbCBsaW5lcyBwcm9jZXNzZWQiLCJ2YWx1ZSI6NTV9LHsiZGlzcGxheU5hbWUiOiJTdW1tYXJ5OiBleGVjIHRpbWUiLCJ1bml0IjoicyIsInZhbHVlIjowLjAwMjIxNjEyNX0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiB0b3RhbCBjaHVua3MgcmVmIiwidmFsdWUiOjJ9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogdG90YWwgY2h1bmtzIGRvd25sb2FkZWQiLCJ2YWx1ZSI6M30seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBjaHVua3MgZG93bmxvYWQgdGltZSIsInVuaXQiOiJzIiwidmFsdWUiOjAuMDAwMzkwOTU4fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGhlYWQgY2h1bmsgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6NH0seyJkaXNwbGF5TmFtZSI6IlN0b3JlOiBoZWFkIGNodW5rIGxpbmVzIiwidmFsdWUiOjV9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogZGVjb21wcmVzc2VkIGJ5dGVzIiwidW5pdCI6ImRlY2J5dGVzIiwidmFsdWUiOjc3NzJ9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogZGVjb21wcmVzc2VkIGxpbmVzIiwidmFsdWUiOjU1fSx7ImRpc3BsYXlOYW1lIjoiU3RvcmU6IGNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MzE0MzJ9LHsiZGlzcGxheU5hbWUiOiJTdG9yZTogdG90YWwgZHVwbGljYXRlcyIsInZhbHVlIjo2fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIHJlYWNoZWQiLCJ2YWx1ZSI6N30seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCBjaHVua3MgbWF0Y2hlZCIsInZhbHVlIjo4fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IHRvdGFsIGJhdGNoZXMiLCJ2YWx1ZSI6OX0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiB0b3RhbCBsaW5lcyBzZW50IiwidmFsdWUiOjEwfSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGhlYWQgY2h1bmsgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MTF9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogaGVhZCBjaHVuayBsaW5lcyIsInZhbHVlIjoxMn0seyJkaXNwbGF5TmFtZSI6IkluZ2VzdGVyOiBkZWNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MTN9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogZGVjb21wcmVzc2VkIGxpbmVzIiwidmFsdWUiOjE0fSx7ImRpc3BsYXlOYW1lIjoiSW5nZXN0ZXI6IGNvbXByZXNzZWQgYnl0ZXMiLCJ1bml0IjoiZGVjYnl0ZXMiLCJ2YWx1ZSI6MTV9LHsiZGlzcGxheU5hbWUiOiJJbmdlc3RlcjogdG90YWwgZHVwbGljYXRlcyIsInZhbHVlIjoxNn1dLCJleGVjdXRlZFF1ZXJ5U3RyaW5nIjoiRXhwcjogcXVlcnkxIn0AAAQAAABtZXRhAAAAAAUAAACoAQAANAEAANgAAABYAAAABAAAAH7+//8UAAAAOAAAADgAAAAAAAAFNAAAAAEAAAAEAAAARP7//wgAAAAMAAAAAgAAAGlkAAAEAAAAbmFtZQAAAAAAAAAAJP7//wIAAABpZAAAzv7//xQAAABgAAAAYAAAAAAAAAVcAAAAAgAAACwAAAAEAAAAmP7//wgAAAAQAAAABAAAAHRzTnMAAAAABAAAAG5hbWUAAAAAvP7//wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAAnP7//wQAAAB0c05zAAAAAEr///8UAAAAPAAAADwAAAAAAAAFOAAAAAEAAAAEAAAAEP///wgAAAAQAAAABAAAAGxpbmUAAAAABAAAAG5hbWUAAAAAAAAAAPT+//8EAAAAbGluZQAAAACi////FAAAADgAAABAAAAAAAAACkAAAAABAAAABAAAAGj///8IAAAADAAAAAIAAAB0cwAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwACAAAAdHMAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAACAAAAAhAAAAAAAAAWAAAAAAgAAADQAAAAEAAAA3P///wgAAAAQAAAABgAAAGxhYmVscwAABAAAAG5hbWUAAAAACAAMAAgABAAIAAAACAAAACQAAAAYAAAAeyJjdXN0b20iOnsianNvbiI6dHJ1ZX19AAAAAAYAAABjb25maWcAAAAAAAAEAAQABAAAAAYAAABsYWJlbHMAAFAJAABBUlJPVzE= diff --git a/pkg/tsdb/loki/testdata/streams_simple.json b/pkg/tsdb/loki/testdata/streams_simple.json index 8754d444ad0..71c0d4ab44b 100644 --- a/pkg/tsdb/loki/testdata/streams_simple.json +++ b/pkg/tsdb/loki/testdata/streams_simple.json @@ -5,8 +5,8 @@ "result": [ { "stream": { - "level": "error", - "location": "moon" + "code": "one\",", + "location": "moonπŸŒ™" }, "values": [ [ @@ -17,8 +17,8 @@ }, { "stream": { - "level": "info", - "location": "moon" + "code": "\",two", + "location": "moonπŸŒ™" }, "values": [ [ @@ -29,6 +29,10 @@ "1645030246277587968", "log line info 2" ], + [ + "1645030246277587968", + "log line info 2" + ], [ "1645030245539423744", "log line info 3" diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts index 6e16b129682..f4fb69f3db4 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts @@ -1,100 +1,95 @@ -import { ArrayVector, CoreApp, DataFrame, DataQueryRequest, DataQueryResponse, FieldType, toUtc } from '@grafana/data'; +import { ArrayVector, DataFrame, DataQueryResponse, FieldType } from '@grafana/data'; import { cloneDeep } from 'lodash'; import { transformBackendResult } from './backendResultTransformer'; -import { LokiQuery } from './types'; -const frame: DataFrame = { - name: 'frame1', +const LOKI_EXPR = '{level="info"} |= "thing1"'; +const inputFrame: DataFrame = { refId: 'A', meta: { - executedQueryString: 'something1', + executedQueryString: LOKI_EXPR, }, fields: [ { - name: 'Time', + name: 'time', type: FieldType.time, config: {}, - values: new ArrayVector([1645029699311, 1645029699312, 1645029699313]), + values: new ArrayVector([1645030244810, 1645030247027, 1645030246277, 1645030245539, 1645030244091]), }, { - name: 'Value', + name: 'value', + type: FieldType.string, + config: {}, + values: new ArrayVector(['line1', 'line2', 'line3', 'line4', 'line5']), + }, + { + name: 'labels', type: FieldType.string, - labels: { - level: 'error', - location: 'moon', - protocol: 'http', - }, config: { - displayNameFromDS: '{level="error", location="moon", protocol="http"}', + custom: { + json: true, + }, }, - values: new ArrayVector(['line1', 'line2', 'line3']), + values: new ArrayVector([ + `[["level", "info"],["code", "41πŸŒ™"]]`, + `[["level", "error"],["code", "41πŸŒ™"]]`, + `[["level", "error"],["code", "43πŸŒ™"]]`, + `[["level", "error"],["code", "41πŸŒ™"]]`, + `[["level", "info"],["code", "41πŸŒ™"]]`, + ]), }, { name: 'tsNs', + type: FieldType.time, + config: {}, + values: new ArrayVector([ + '1645030244810757120', + '1645030247027735040', + '1645030246277587968', + '1645030245539423744', + '1645030244091700992', + ]), + }, + { + name: 'id', type: FieldType.string, config: {}, - values: new ArrayVector(['1645029699311000500', '1645029699312000500', '1645029699313000500']), + values: new ArrayVector(['id1', 'id2', 'id3', 'id4', 'id5']), }, ], - length: 3, + length: 5, }; -function makeRequest(expr: string): DataQueryRequest { - return { - requestId: 'test1', - interval: '1s', - intervalMs: 1000, - range: { - from: toUtc('2022-02-22T13:14:15'), - to: toUtc('2022-02-22T13:15:15'), - raw: { - from: toUtc('2022-02-22T13:14:15'), - to: toUtc('2022-02-22T13:15:15'), - }, - }, - scopedVars: {}, - targets: [ - { - refId: 'A', - expr, - }, - ], - timezone: 'UTC', - app: CoreApp.Explore, - startTime: 0, - }; -} - describe('loki backendResultTransformer', () => { it('processes a logs-dataframe correctly', () => { - const response: DataQueryResponse = { data: [cloneDeep(frame)] }; - const request = makeRequest('{level="info"} |= "thing1"'); + const response: DataQueryResponse = { data: [cloneDeep(inputFrame)] }; - const expectedFrame = cloneDeep(frame); + const expectedFrame = cloneDeep(inputFrame); expectedFrame.meta = { - executedQueryString: 'something1', + ...expectedFrame.meta, preferredVisualisationType: 'logs', searchWords: ['thing1'], custom: { lokiQueryStatKey: 'Summary: total bytes processed', }, }; - expectedFrame.fields[2].type = FieldType.time; - expectedFrame.fields.push({ - name: 'id', - type: FieldType.string, - config: {}, - values: new ArrayVector([ - '6b099923-25a6-5336-96fa-c84a14b7c351_A', - '0e1b7c47-a956-5cf2-a803-d487679745bd_A', - '6f9a840c-6a00-525b-9ed4-cceea29e62af_A', - ]), - }); + expectedFrame.fields[2].type = FieldType.other; + expectedFrame.fields[2].values = new ArrayVector([ + { level: 'info', code: '41πŸŒ™' }, + { level: 'error', code: '41πŸŒ™' }, + { level: 'error', code: '43πŸŒ™' }, + { level: 'error', code: '41πŸŒ™' }, + { level: 'info', code: '41πŸŒ™' }, + ]); const expected: DataQueryResponse = { data: [expectedFrame] }; - const result = transformBackendResult(response, request); + const result = transformBackendResult(response, [ + { + refId: 'A', + expr: LOKI_EXPR, + }, + ]); expect(result).toEqual(expected); }); }); diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.ts b/public/app/plugins/datasource/loki/backendResultTransformer.ts index 2423fc81c93..ba4a6b3ebf0 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.ts @@ -1,8 +1,15 @@ -import { DataQueryRequest, DataQueryResponse, DataFrame, isDataFrame, FieldType, QueryResultMeta } from '@grafana/data'; +import { + DataQueryResponse, + DataFrame, + isDataFrame, + FieldType, + QueryResultMeta, + ArrayVector, + Labels, +} from '@grafana/data'; import { LokiQuery, LokiQueryType } from './types'; import { makeTableFrames } from './makeTableFrames'; import { formatQuery, getHighlighterExpressionsFromQuery } from './query_utils'; -import { makeIdField } from './makeIdField'; function isMetricFrame(frame: DataFrame): boolean { return frame.fields.every((field) => field.type === FieldType.time || field.type === FieldType.number); @@ -19,6 +26,12 @@ function setFrameMeta(frame: DataFrame, meta: QueryResultMeta): DataFrame { }; } +function decodeLabelsInJson(text: string): Labels { + const array: Array<[string, string]> = JSON.parse(text); + // NOTE: maybe we should go with maps, those have guaranteed ordering + return Object.fromEntries(array); +} + function processStreamFrame(frame: DataFrame, query: LokiQuery | undefined): DataFrame { const meta: QueryResultMeta = { preferredVisualisationType: 'logs', @@ -29,21 +42,36 @@ function processStreamFrame(frame: DataFrame, query: LokiQuery | undefined): Dat }, }; const newFrame = setFrameMeta(frame, meta); - const newFields = frame.fields.map((field) => { - // the nanosecond-timestamp field must have a type-time - if (field.name === 'tsNs') { - return { - ...field, - type: FieldType.time, - }; - } else { - return field; + + const newFields = newFrame.fields.map((field) => { + switch (field.name) { + case 'labels': { + // the labels, when coming from the server, are json-encoded. + // here we decode them if needed. + return field.config.custom.json + ? { + name: field.name, + type: FieldType.other, + config: field.config, + // we are parsing the labels the same way as streaming-dataframes do + values: new ArrayVector(field.values.toArray().map((text) => decodeLabelsInJson(text))), + } + : field; + } + case 'tsNs': { + // we need to switch the field-type to be `time` + return { + ...field, + type: FieldType.time, + }; + } + default: { + // no modification needed + return field; + } } }); - // we add a calculated id-field - newFields.push(makeIdField(frame)); - return { ...newFrame, fields: newFields, @@ -96,10 +124,7 @@ function groupFrames( return { streamsFrames, metricInstantFrames, metricRangeFrames }; } -export function transformBackendResult( - response: DataQueryResponse, - request: DataQueryRequest -): DataQueryResponse { +export function transformBackendResult(response: DataQueryResponse, queries: LokiQuery[]): DataQueryResponse { const { data, ...rest } = response; // in the typescript type, data is an array of basically anything. @@ -112,7 +137,7 @@ export function transformBackendResult( return d; }); - const queryMap = new Map(request.targets.map((query) => [query.refId, query])); + const queryMap = new Map(queries.map((query) => [query.refId, query])); const { streamsFrames, metricInstantFrames, metricRangeFrames } = groupFrames(dataFrames, queryMap); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 944d62bbe07..4a33807cb1e 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -161,7 +161,14 @@ export class LokiDatasource ...request, targets: request.targets.map(getNormalizedLokiQuery), }; - return super.query(fixedRequest).pipe(map((response) => transformBackendResult(response, fixedRequest))); + + if (fixedRequest.liveStreaming) { + return this.runLiveQueryThroughBackend(fixedRequest); + } else { + return super + .query(fixedRequest) + .pipe(map((response) => transformBackendResult(response, fixedRequest.targets))); + } } const filteredTargets = request.targets @@ -199,6 +206,27 @@ export class LokiDatasource return merge(...subQueries); } + runLiveQueryThroughBackend(request: DataQueryRequest): Observable { + // this only works in explore-mode, so variables don't need to be handled, + // and only for logs-queries, not metric queries + const logsQueries = request.targets.filter((query) => query.expr !== '' && !isMetricsQuery(query.expr)); + + if (logsQueries.length === 0) { + return of({ + data: [], + state: LoadingState.Done, + }); + } + + const subQueries = logsQueries.map((query) => { + const maxDataPoints = query.maxLines || this.maxLines; + // FIXME: currently we are running it through the frontend still. + return this.runLiveQuery(query, maxDataPoints); + }); + + return merge(...subQueries); + } + runInstantQuery = ( target: LokiQuery, options: DataQueryRequest, diff --git a/public/app/plugins/datasource/loki/makeIdField.test.ts b/public/app/plugins/datasource/loki/makeIdField.test.ts deleted file mode 100644 index 04ab5c10e8d..00000000000 --- a/public/app/plugins/datasource/loki/makeIdField.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ArrayVector, DataFrame, FieldType } from '@grafana/data'; -import { makeIdField } from './makeIdField'; - -function makeFrame(timestamps: number[], values: string[], timestampNss: string[], refId?: string): DataFrame { - return { - name: 'frame', - refId, - meta: { - executedQueryString: 'something1', - }, - fields: [ - { - name: 'Time', - type: FieldType.time, - config: {}, - values: new ArrayVector(timestamps), - }, - { - name: 'Value', - type: FieldType.string, - config: {}, - labels: { - foo: 'bar', - }, - values: new ArrayVector(values), - }, - { - name: 'tsNs', - type: FieldType.time, - config: {}, - values: new ArrayVector(timestampNss), - }, - ], - length: timestamps.length, - }; -} - -describe('loki makeIdField', () => { - it('should always generate unique ids for logs', () => { - const frame = makeFrame( - [1579857562021, 1579857562021, 1579857562021, 1579857562021], - [ - 't=2020-02-12T15:04:51+0000 lvl=info msg="Duplicated"', - 't=2020-02-12T15:04:51+0000 lvl=info msg="Duplicated"', - 't=2020-02-12T15:04:51+0000 lvl=info msg="Non-Duplicated"', - 't=2020-02-12T15:04:51+0000 lvl=info msg="Duplicated"', - ], - ['1579857562021616000', '1579857562021616000', '1579857562021616000', '1579857562021616000'] - ); - expect(makeIdField(frame)).toEqual({ - config: {}, - name: 'id', - type: 'string', - values: new ArrayVector([ - '75fceace-9f98-5134-b222-643fdcde2877', - '75fceace-9f98-5134-b222-643fdcde2877_1', - '4a081a89-040d-5f64-9477-a4d846ce9f6b', - '75fceace-9f98-5134-b222-643fdcde2877_2', - ]), - }); - }); - - it('should append refId to the unique ids if refId is provided', () => { - const frame = makeFrame( - [1579857562021, 1579857562021, 1579857562021, 1579857562021], - [ - 't=2020-02-12T15:04:51+0000 lvl=info msg="Duplicated"', - 't=2020-02-12T15:04:51+0000 lvl=info msg="Duplicated"', - 't=2020-02-12T15:04:51+0000 lvl=info msg="Non-Duplicated"', - 't=2020-02-12T15:04:51+0000 lvl=info msg="Duplicated"', - ], - ['1579857562021616000', '1579857562021616000', '1579857562021616000', '1579857562021616000'], - 'X' - ); - expect(makeIdField(frame)).toEqual({ - config: {}, - name: 'id', - type: 'string', - values: new ArrayVector([ - '75fceace-9f98-5134-b222-643fdcde2877_X', - '75fceace-9f98-5134-b222-643fdcde2877_1_X', - '4a081a89-040d-5f64-9477-a4d846ce9f6b_X', - '75fceace-9f98-5134-b222-643fdcde2877_2_X', - ]), - }); - }); -}); diff --git a/public/app/plugins/datasource/loki/makeIdField.ts b/public/app/plugins/datasource/loki/makeIdField.ts deleted file mode 100644 index 02550b07a62..00000000000 --- a/public/app/plugins/datasource/loki/makeIdField.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { v5 as uuidv5 } from 'uuid'; - -import { ArrayVector, DataFrame, Field, FieldType, Labels } from '@grafana/data'; - -const UUID_NAMESPACE = '6ec946da-0f49-47a8-983a-1d76d17e7c92'; - -function createUid(text: string, usedUids: Map, refId?: string): string { - const id = uuidv5(text, UUID_NAMESPACE); - - // check how many times have we seen this id before, - // set the count to zero, if never. - const count = usedUids.get(id) ?? 0; - - // if we have seen this id before, we need to make - // it unique by appending the seen-count - // (starts with 1, and goes up) - const uniqueId = count > 0 ? `${id}_${count}` : id; - - // we increment the counter for this id, to be used when we are called the next time - usedUids.set(id, count + 1); - - // we add refId to the end, if it is available - return refId !== undefined ? `${uniqueId}_${refId}` : uniqueId; -} - -export function makeIdField(frame: DataFrame): Field { - const allLabels: Labels = {}; - - // collect labels from every field - frame.fields.forEach((field) => { - Object.assign(allLabels, field.labels); - }); - - const labelsString = Object.entries(allLabels) - .map(([key, val]) => `${key}="${val}"`) - .sort() - .join(''); - - const usedUids = new Map(); - - const { length } = frame; - - const uids: string[] = new Array(length); - - // we need to go through the dataframe "row by row" - for (let i = 0; i < length; i++) { - const row = frame.fields.map((f) => String(f.values.get(i))); - const text = `${labelsString}_${row.join('_')}`; - const uid = createUid(text, usedUids, frame.refId); - uids[i] = uid; - } - - return { name: 'id', type: FieldType.string, config: {}, values: new ArrayVector(uids) }; -}