grafana/pkg/expr/hysteresis.go
Yuri Tseretyan bf8be46e6f
SSE: Add utility methods for HysteresisCommand (#79157)
* add GetCommandsFromPipeline
* refactor method GetCommandType to func GetExpressionCommandType
* add function to create fingerprint frames
* add function to determine whether raw query represents a hysteresis command and a function to patch it with loaded metrics
2023-12-11 22:40:31 +02:00

137 lines
5.0 KiB
Go

package expr
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/grafana/grafana/pkg/infra/tracing"
)
type Fingerprints map[data.Fingerprint]struct{}
// HysteresisCommand is a special case of ThresholdCommand that encapsulates two thresholds that are applied depending on the results of the previous evaluations:
// - first threshold - "loading", is used when the metric is determined as not loaded, i.e. it does not exist in the data provided by the reader.
// - second threshold - "unloading", is used when the metric is determined as loaded.
// To determine whether a metric is loaded, the command uses LoadedDimensions that is supposed to contain data.Fingerprint of
// the metrics that were loaded during the previous evaluation.
// The result of the execution of the command is the same as ThresholdCommand: 0 or 1 for each metric.
type HysteresisCommand struct {
RefID string
ReferenceVar string
LoadingThresholdFunc ThresholdCommand
UnloadingThresholdFunc ThresholdCommand
LoadedDimensions Fingerprints
}
func (h *HysteresisCommand) NeedsVars() []string {
return []string{h.ReferenceVar}
}
func (h *HysteresisCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) {
results := vars[h.ReferenceVar]
// shortcut for NoData
if results.IsNoData() {
return mathexp.Results{Values: mathexp.Values{mathexp.NewNoData()}}, nil
}
if h.LoadedDimensions == nil || len(h.LoadedDimensions) == 0 {
return h.LoadingThresholdFunc.Execute(ctx, now, vars, tracer)
}
var loadedVals, unloadedVals mathexp.Values
for _, value := range results.Values {
_, ok := h.LoadedDimensions[value.GetLabels().Fingerprint()]
if ok {
loadedVals = append(loadedVals, value)
} else {
unloadedVals = append(unloadedVals, value)
}
}
if len(loadedVals) == 0 { // if all values are unloaded
return h.LoadingThresholdFunc.Execute(ctx, now, vars, tracer)
}
if len(unloadedVals) == 0 { // if all values are loaded
return h.UnloadingThresholdFunc.Execute(ctx, now, vars, tracer)
}
defer func() {
// return back the old values
vars[h.ReferenceVar] = results
}()
vars[h.ReferenceVar] = mathexp.Results{Values: unloadedVals}
loadingResults, err := h.LoadingThresholdFunc.Execute(ctx, now, vars, tracer)
if err != nil {
return mathexp.Results{}, fmt.Errorf("failed to execute loading threshold: %w", err)
}
vars[h.ReferenceVar] = mathexp.Results{Values: loadedVals}
unloadingResults, err := h.UnloadingThresholdFunc.Execute(ctx, now, vars, tracer)
if err != nil {
return mathexp.Results{}, fmt.Errorf("failed to execute unloading threshold: %w", err)
}
return mathexp.Results{Values: append(loadingResults.Values, unloadingResults.Values...)}, nil
}
func NewHysteresisCommand(refID string, referenceVar string, loadCondition ThresholdCommand, unloadCondition ThresholdCommand, l Fingerprints) (*HysteresisCommand, error) {
return &HysteresisCommand{
RefID: refID,
LoadingThresholdFunc: loadCondition,
UnloadingThresholdFunc: unloadCondition,
ReferenceVar: referenceVar,
LoadedDimensions: l,
}, nil
}
// FingerprintsFromFrame converts data.Frame to Fingerprints.
// The input data frame must have a single field of uint64 type.
// Returns error if the input data frame has invalid format
func FingerprintsFromFrame(frame *data.Frame) (Fingerprints, error) {
frameType, frameVersion := frame.TypeInfo("")
if frameType != "fingerprints" {
return nil, fmt.Errorf("invalid format of loaded dimensions frame: expected frame type 'fingerprints'")
}
if frameVersion.Greater(data.FrameTypeVersion{1, 0}) {
return nil, fmt.Errorf("invalid format of loaded dimensions frame: expected frame type 'fingerprints' of version 1.0 or lower")
}
if len(frame.Fields) != 1 {
return nil, fmt.Errorf("invalid format of loaded dimensions frame: expected a single field but got %d", len(frame.Fields))
}
fld := frame.Fields[0]
if fld.Type() != data.FieldTypeUint64 {
return nil, fmt.Errorf("invalid format of loaded dimensions frame: the field type must be uint64 but got %s", fld.Type().String())
}
result := make(Fingerprints, fld.Len())
for i := 0; i < fld.Len(); i++ {
val, ok := fld.ConcreteAt(i)
if !ok {
continue
}
switch v := val.(type) {
case uint64:
result[data.Fingerprint(v)] = struct{}{}
default:
return nil, fmt.Errorf("cannot read the value at index [%d], expected uint64 but got '%T'", i, val)
}
}
return result, nil
}
// FingerprintsToFrame converts Fingerprints to data.Frame.
func FingerprintsToFrame(fingerprints Fingerprints) *data.Frame {
fp := make([]uint64, 0, len(fingerprints))
for fingerprint := range fingerprints {
fp = append(fp, uint64(fingerprint))
}
frame := data.NewFrame("", data.NewField("fingerprints", nil, fp))
frame.SetMeta(&data.FrameMeta{
Type: "fingerprints",
TypeVersion: data.FrameTypeVersion{1, 0},
})
return frame
}