2022-02-25 02:14:17 -06:00
|
|
|
package loki
|
|
|
|
|
|
|
|
import (
|
2022-05-03 01:03:25 -05:00
|
|
|
"encoding/json"
|
2022-02-25 02:14:17 -06:00
|
|
|
"fmt"
|
2022-04-12 04:58:48 -05:00
|
|
|
"hash/fnv"
|
2022-02-25 02:14:17 -06:00
|
|
|
"sort"
|
|
|
|
"strings"
|
2022-03-04 02:42:18 -06:00
|
|
|
"time"
|
2022-02-25 02:14:17 -06:00
|
|
|
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
|
|
)
|
|
|
|
|
|
|
|
// we adjust the dataframes to be the way frontend & alerting
|
|
|
|
// wants them.
|
2022-04-12 04:58:48 -05:00
|
|
|
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")
|
|
|
|
}
|
2022-02-25 02:14:17 -06:00
|
|
|
|
2022-04-12 04:58:48 -05:00
|
|
|
timeField := fields[0]
|
|
|
|
valueField := fields[1]
|
2022-02-25 02:14:17 -06:00
|
|
|
|
2022-04-12 04:58:48 -05:00
|
|
|
if (timeField.Type() != data.FieldTypeTime) || (valueField.Type() != data.FieldTypeFloat64) {
|
|
|
|
return fmt.Errorf("invalid fields in metric frame")
|
|
|
|
}
|
|
|
|
|
|
|
|
labels := getFrameLabels(frame)
|
2022-02-25 02:14:17 -06:00
|
|
|
|
2022-04-12 04:58:48 -05:00
|
|
|
isMetricRange := query.QueryType == QueryTypeRange
|
2022-02-25 02:14:17 -06:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-12 04:58:48 -05:00
|
|
|
if isMetricRange {
|
|
|
|
if timeField.Config == nil {
|
|
|
|
timeField.Config = &data.FieldConfig{}
|
2022-02-25 02:14:17 -06:00
|
|
|
}
|
2022-04-12 04:58:48 -05:00
|
|
|
timeField.Config.Interval = float64(query.Step.Milliseconds())
|
2022-02-25 02:14:17 -06:00
|
|
|
}
|
|
|
|
|
2022-04-12 04:58:48 -05:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
|
|
|
labelsField := fields[0]
|
|
|
|
timeField := fields[1]
|
|
|
|
lineField := fields[2]
|
|
|
|
|
2022-05-03 01:03:25 -05:00
|
|
|
if (timeField.Type() != data.FieldTypeTime) || (lineField.Type() != data.FieldTypeString) || (labelsField.Type() != data.FieldTypeJSON) {
|
|
|
|
return fmt.Errorf("invalid fields in logs frame")
|
2022-04-12 04:58:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (timeField.Len() != lineField.Len()) || (timeField.Len() != labelsField.Len()) {
|
2022-05-03 01:03:25 -05:00
|
|
|
return fmt.Errorf("invalid fields in logs frame")
|
2022-04-12 04:58:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if frame.Meta == nil {
|
|
|
|
frame.Meta = &data.FrameMeta{}
|
2022-02-25 02:14:17 -06:00
|
|
|
}
|
|
|
|
|
2022-04-12 04:58:48 -05:00
|
|
|
frame.Meta.ExecutedQueryString = "Expr: " + query.Expr
|
|
|
|
|
|
|
|
// we need to send to the browser the nanosecond-precision timestamp too.
|
2022-03-04 02:42:18 -06:00
|
|
|
// 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.
|
2022-04-12 04:58:48 -05:00
|
|
|
stringTimeField := makeStringTimeField(timeField)
|
2022-03-04 02:42:18 -06:00
|
|
|
|
2022-04-12 04:58:48 -05:00
|
|
|
idField, err := makeIdField(stringTimeField, lineField, labelsField, frame.RefID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
frame.Fields = append(frame.Fields, stringTimeField, idField)
|
|
|
|
return nil
|
2022-02-25 02:14:17 -06:00
|
|
|
}
|
|
|
|
|
2022-04-12 04:58:48 -05:00
|
|
|
func makeStringTimeField(timeField *data.Field) *data.Field {
|
|
|
|
length := timeField.Len()
|
2022-03-04 02:42:18 -06:00
|
|
|
stringTimestamps := make([]string, length)
|
|
|
|
|
|
|
|
for i := 0; i < length; i++ {
|
2022-04-12 04:58:48 -05:00
|
|
|
nsNumber := timeField.At(i).(time.Time).UnixNano()
|
|
|
|
stringTimestamps[i] = fmt.Sprintf("%d", nsNumber)
|
|
|
|
}
|
|
|
|
return data.NewField("tsNs", timeField.Labels.Copy(), stringTimestamps)
|
|
|
|
}
|
|
|
|
|
2022-05-03 01:03:25 -05:00
|
|
|
func calculateCheckSum(time string, line string, labels []byte) (string, error) {
|
|
|
|
input := []byte(line + "_")
|
|
|
|
input = append(input, labels...)
|
2022-04-12 04:58:48 -05:00
|
|
|
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)
|
2022-05-03 01:03:25 -05:00
|
|
|
labels := labelsField.At(i).(json.RawMessage)
|
2022-04-12 04:58:48 -05:00
|
|
|
|
|
|
|
sum, err := calculateCheckSum(time, line, labels)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2022-03-04 02:42:18 -06:00
|
|
|
}
|
2022-04-12 04:58:48 -05:00
|
|
|
|
|
|
|
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
|
2022-03-04 02:42:18 -06:00
|
|
|
}
|
2022-04-12 04:58:48 -05:00
|
|
|
return data.NewField("id", nil, ids), nil
|
2022-03-04 02:42:18 -06:00
|
|
|
}
|
|
|
|
|
2022-02-25 02:14:17 -06:00
|
|
|
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
|
|
|
|
}
|