mirror of https://github.com/grafana/grafana
Alerting: Recording rule mapping logic for data frames to Prometheus metrics (#88550)
* Add stub Prometheus writer with mapping logic * Add testspull/88939/head
parent
330da7916d
commit
63e9969c1b
@ -0,0 +1,96 @@ |
|||||||
|
package writer |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/grafana/dataplane/sdata/numeric" |
||||||
|
"github.com/grafana/grafana/pkg/infra/log" |
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||||
|
) |
||||||
|
|
||||||
|
type PrometheusWriter struct { |
||||||
|
logger log.Logger |
||||||
|
} |
||||||
|
|
||||||
|
// Metric represents a Prometheus time series metric.
|
||||||
|
type Metric struct { |
||||||
|
T int64 |
||||||
|
V float64 |
||||||
|
} |
||||||
|
|
||||||
|
// Point is a logical representation of a single point in time for a Prometheus time series.
|
||||||
|
type Point struct { |
||||||
|
Name string |
||||||
|
Labels map[string]string |
||||||
|
Metric Metric |
||||||
|
} |
||||||
|
|
||||||
|
func NewPrometheusWriter(l log.Logger) *PrometheusWriter { |
||||||
|
return &PrometheusWriter{ |
||||||
|
logger: l, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Write writes the given frames to the Prometheus remote write endpoint.
|
||||||
|
// TODO: stub implementation, does not make any remote write calls.
|
||||||
|
func (w PrometheusWriter) Write(ctx context.Context, name string, t time.Time, frames data.Frames, extraLabels map[string]string) error { |
||||||
|
l := w.logger.FromContext(ctx) |
||||||
|
|
||||||
|
points, err := PointsFromFrames(name, t, frames, extraLabels) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: placeholder for actual remote write call
|
||||||
|
l.Debug("writing points", "points", points) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func PointsFromFrames(name string, t time.Time, frames data.Frames, extraLabels map[string]string) ([]Point, error) { |
||||||
|
cr, err := numeric.CollectionReaderFromFrames(frames) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
col, err := cr.GetCollection(false) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
points := make([]Point, 0, len(col.Refs)) |
||||||
|
for _, ref := range col.Refs { |
||||||
|
var f float64 |
||||||
|
if fp, empty, err := ref.NullableFloat64Value(); !empty && fp != nil { |
||||||
|
f = *fp |
||||||
|
} else if err != nil { |
||||||
|
return nil, fmt.Errorf("unable to get float64 value: %w", err) |
||||||
|
} else { |
||||||
|
return nil, fmt.Errorf("unable to get metric value") |
||||||
|
} |
||||||
|
|
||||||
|
metric := Metric{ |
||||||
|
T: t.Unix(), |
||||||
|
V: f, |
||||||
|
} |
||||||
|
|
||||||
|
labels := ref.GetLabels().Copy() |
||||||
|
if labels == nil { |
||||||
|
labels = data.Labels{} |
||||||
|
} |
||||||
|
delete(labels, "__name__") |
||||||
|
for k, v := range extraLabels { |
||||||
|
labels[k] = v |
||||||
|
} |
||||||
|
|
||||||
|
points = append(points, Point{ |
||||||
|
Name: name, |
||||||
|
Labels: labels, |
||||||
|
Metric: metric, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return points, nil |
||||||
|
} |
@ -0,0 +1,230 @@ |
|||||||
|
package writer |
||||||
|
|
||||||
|
import ( |
||||||
|
"math" |
||||||
|
"math/rand/v2" |
||||||
|
"reflect" |
||||||
|
"slices" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
) |
||||||
|
|
||||||
|
func TestPrometheusWriter_Write(t *testing.T) { |
||||||
|
t.Skip("TODO: implement") |
||||||
|
} |
||||||
|
|
||||||
|
func TestPointsFromFrames(t *testing.T) { |
||||||
|
extraLabels := map[string]string{"extra": "label"} |
||||||
|
|
||||||
|
type testCase struct { |
||||||
|
name string |
||||||
|
frameType data.FrameType |
||||||
|
} |
||||||
|
|
||||||
|
testCases := []testCase{ |
||||||
|
{name: "wide", frameType: data.FrameTypeNumericWide}, |
||||||
|
{name: "long", frameType: data.FrameTypeNumericLong}, |
||||||
|
{name: "multi", frameType: data.FrameTypeNumericMulti}, |
||||||
|
} |
||||||
|
|
||||||
|
t.Run("error when frames are empty", func(t *testing.T) { |
||||||
|
for _, tc := range testCases { |
||||||
|
t.Run(tc.name, func(t *testing.T) { |
||||||
|
frames := data.Frames{data.NewFrame("test")} |
||||||
|
now := time.Now() |
||||||
|
|
||||||
|
_, err := PointsFromFrames("test", now, frames, extraLabels) |
||||||
|
require.Error(t, err) |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("maps frames correctly", func(t *testing.T) { |
||||||
|
series := []map[string]string{{"foo": "1"}, {"foo": "2"}, {"foo": "3"}, {"foo": "4"}} |
||||||
|
for _, tc := range testCases { |
||||||
|
t.Run(tc.name, func(t *testing.T) { |
||||||
|
frames := frameGenFromLabels(t, tc.frameType, series) |
||||||
|
now := time.Now() |
||||||
|
|
||||||
|
points, err := PointsFromFrames("test", now, frames, extraLabels) |
||||||
|
|
||||||
|
require.NoError(t, err) |
||||||
|
require.Len(t, points, len(series)) |
||||||
|
for i, point := range points { |
||||||
|
v := extractValue(t, frames, series[i], tc.frameType) |
||||||
|
expectedLabels := map[string]string{"extra": "label"} |
||||||
|
for k, v := range series[i] { |
||||||
|
expectedLabels[k] = v |
||||||
|
} |
||||||
|
require.Equal(t, expectedLabels, point.Labels) |
||||||
|
require.Equal(t, "test", point.Name) |
||||||
|
require.Equal(t, now.Unix(), point.Metric.T) |
||||||
|
require.Equal(t, v, point.Metric.V) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func extractValue(t *testing.T, frames data.Frames, labels map[string]string, frameType data.FrameType) float64 { |
||||||
|
t.Helper() |
||||||
|
|
||||||
|
var f func(*testing.T, data.Frames, map[string]string) float64 |
||||||
|
|
||||||
|
switch frames[0].Meta.Type { |
||||||
|
case data.FrameTypeNumericWide: |
||||||
|
f = extractValueWide |
||||||
|
case data.FrameTypeNumericLong: |
||||||
|
f = extractValueLong |
||||||
|
case data.FrameTypeNumericMulti: |
||||||
|
f = extractValueMulti |
||||||
|
default: |
||||||
|
t.Fatalf("unsupported frame type %q", frameType) |
||||||
|
} |
||||||
|
|
||||||
|
return f(t, frames, labels) |
||||||
|
} |
||||||
|
|
||||||
|
func extractValueWide(t *testing.T, frames data.Frames, labels map[string]string) float64 { |
||||||
|
t.Helper() |
||||||
|
|
||||||
|
frame := frames[0] |
||||||
|
for _, field := range frame.Fields { |
||||||
|
if reflect.DeepEqual(field.Labels, data.Labels(labels)) { |
||||||
|
return field.At(0).(float64) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
t.Fatalf("could not find value for labels %v", labels) |
||||||
|
return math.NaN() |
||||||
|
} |
||||||
|
|
||||||
|
func extractValueLong(t *testing.T, frames data.Frames, labels map[string]string) float64 { |
||||||
|
t.Helper() |
||||||
|
|
||||||
|
frame := frames[0] |
||||||
|
foundLabels := make(map[string]string) |
||||||
|
|
||||||
|
l := frame.Fields[0].Len() |
||||||
|
for i := 0; i < l; i++ { |
||||||
|
for _, field := range frame.Fields[1 : len(frame.Fields)-1] { |
||||||
|
foundLabels[field.Name] = field.At(i).(string) |
||||||
|
} |
||||||
|
if reflect.DeepEqual(foundLabels, labels) { |
||||||
|
return frame.Fields[len(frame.Fields)-1].At(i).(float64) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
t.Fatalf("could not find value for labels %v", labels) |
||||||
|
return math.NaN() |
||||||
|
} |
||||||
|
|
||||||
|
func extractValueMulti(t *testing.T, frames data.Frames, labels map[string]string) float64 { |
||||||
|
t.Helper() |
||||||
|
|
||||||
|
for _, frame := range frames { |
||||||
|
if reflect.DeepEqual(frame.Fields[1].Labels, data.Labels(labels)) { |
||||||
|
return frame.Fields[1].At(0).(float64) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
t.Fatalf("could not find value for labels %v", labels) |
||||||
|
return math.NaN() |
||||||
|
} |
||||||
|
|
||||||
|
func frameGenFromLabels(t *testing.T, frameType data.FrameType, labelSet []map[string]string) data.Frames { |
||||||
|
var f func(*testing.T, []map[string]string) data.Frames |
||||||
|
|
||||||
|
switch frameType { |
||||||
|
case data.FrameTypeNumericWide: |
||||||
|
f = frameGenWide |
||||||
|
case data.FrameTypeNumericLong: |
||||||
|
f = frameGenLong |
||||||
|
case data.FrameTypeNumericMulti: |
||||||
|
f = frameGenMulti |
||||||
|
default: |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return f(t, labelSet) |
||||||
|
} |
||||||
|
|
||||||
|
func frameGenWide(t *testing.T, labelMaps []map[string]string) data.Frames { |
||||||
|
t.Helper() |
||||||
|
|
||||||
|
frame := data.NewFrame("test", fieldGenWide(time.Now(), labelMaps)...) |
||||||
|
frame.SetMeta(&data.FrameMeta{ |
||||||
|
Type: data.FrameTypeNumericWide, |
||||||
|
TypeVersion: data.FrameTypeVersion{0, 1}, |
||||||
|
}) |
||||||
|
return data.Frames{frame} |
||||||
|
} |
||||||
|
|
||||||
|
func fieldGenWide(t time.Time, labelSet []map[string]string) []*data.Field { |
||||||
|
fields := make([]*data.Field, 1, len(labelSet)+1) |
||||||
|
fields[0] = data.NewField("T", nil, []time.Time{t}) |
||||||
|
for _, labels := range labelSet { |
||||||
|
field := data.NewField("value", data.Labels(labels), []float64{rand.Float64() * (100 - 0)}) // arbitrary range
|
||||||
|
fields = append(fields, field) |
||||||
|
} |
||||||
|
return fields |
||||||
|
} |
||||||
|
|
||||||
|
func frameGenLong(t *testing.T, labelSet []map[string]string) data.Frames { |
||||||
|
t.Helper() |
||||||
|
|
||||||
|
frame := data.NewFrame("test", fieldGenLong(time.Now(), labelSet)...) |
||||||
|
frame.SetMeta(&data.FrameMeta{ |
||||||
|
Type: data.FrameTypeNumericLong, |
||||||
|
TypeVersion: data.FrameTypeVersion{0, 1}, |
||||||
|
}) |
||||||
|
|
||||||
|
return data.Frames{frame} |
||||||
|
} |
||||||
|
|
||||||
|
func fieldGenLong(t time.Time, labelSet []map[string]string) []*data.Field { |
||||||
|
fields := make([]*data.Field, 1, len(labelSet)+1) |
||||||
|
times := make([]time.Time, 0, len(labelSet)) |
||||||
|
labelFields := make(map[string][]string) |
||||||
|
values := make([]float64, 0, len(labelSet)) |
||||||
|
|
||||||
|
for _, labels := range labelSet { |
||||||
|
times = append(times, t) |
||||||
|
for k, v := range labels { |
||||||
|
if !slices.Contains(labelFields[k], v) { |
||||||
|
labelFields[k] = append(labelFields[k], v) |
||||||
|
} |
||||||
|
} |
||||||
|
values = append(values, rand.Float64()*(100-0)) // arbitrary range
|
||||||
|
} |
||||||
|
fields[0] = data.NewField("T", nil, times) |
||||||
|
for k, v := range labelFields { |
||||||
|
fields = append(fields, data.NewField(k, nil, v)) |
||||||
|
} |
||||||
|
fields = append(fields, data.NewField("value", nil, values)) |
||||||
|
|
||||||
|
return fields |
||||||
|
} |
||||||
|
|
||||||
|
func frameGenMulti(t *testing.T, labelSet []map[string]string) data.Frames { |
||||||
|
t.Helper() |
||||||
|
|
||||||
|
frames := make(data.Frames, 0, len(labelSet)) |
||||||
|
now := time.Now() |
||||||
|
for _, labels := range labelSet { |
||||||
|
frame := data.NewFrame("test", |
||||||
|
data.NewField("T", nil, []time.Time{now}), |
||||||
|
data.NewField("value", data.Labels(labels), []float64{rand.Float64() * (100 - 0)}), |
||||||
|
) |
||||||
|
frame.SetMeta(&data.FrameMeta{ |
||||||
|
Type: data.FrameTypeNumericMulti, |
||||||
|
TypeVersion: data.FrameTypeVersion{0, 1}, |
||||||
|
}) |
||||||
|
frames = append(frames, frame) |
||||||
|
} |
||||||
|
|
||||||
|
return frames |
||||||
|
} |
Loading…
Reference in new issue