SQL Expressions: Make SQL Expressions work with Alerting (#101820)

Initial support for alerting with SQL expressions

- When `format` is set to `alerting`, SQL expressions output in a format suitable for alerting evaluation.
- Outstanding TODOs:
  - Deduplicate output rows
  - Add more tests
  - Fix broken alerting UI rendering (likely due to shape change to undocumented full-long format)
- Basic usage:
  - SQL must return one numeric column and one or more string columns.
  - Each row may become an alert.
  - The alert fires if the numeric value is non-zero.
  - String columns are treated as labels.

---------

Co-authored-by: Konrad Lalik <konradlalik@gmail.com>
Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
Co-authored-by: Sam Jewell <sam.jewell@grafana.com>
This commit is contained in:
Kyle Brandt
2025-04-02 09:39:36 -04:00
committed by GitHub
parent 192d3783d5
commit c6e52c4766
19 changed files with 409 additions and 357 deletions

View File

@ -30,10 +30,11 @@ type SQLCommand struct {
varsToQuery []string
refID string
limit int64
format string
}
// NewSQLCommand creates a new SQLCommand.
func NewSQLCommand(refID, rawSQL string, limit int64) (*SQLCommand, error) {
func NewSQLCommand(refID, format, rawSQL string, limit int64) (*SQLCommand, error) {
if rawSQL == "" {
return nil, ErrMissingSQLQuery
}
@ -62,6 +63,7 @@ func NewSQLCommand(refID, rawSQL string, limit int64) (*SQLCommand, error) {
varsToQuery: tables,
refID: refID,
limit: limit,
format: format,
}, nil
}
@ -83,7 +85,10 @@ func UnmarshalSQLCommand(rn *rawNode, limit int64) (*SQLCommand, error) {
return nil, fmt.Errorf("expected sql expression to be type string, but got type %T", expressionRaw)
}
return NewSQLCommand(rn.RefID, expression, limit)
formatRaw := rn.Query["format"]
format, _ := formatRaw.(string)
return NewSQLCommand(rn.RefID, format, expression, limit)
}
// NeedsVars returns the variable names (refIds) that are dependencies
@ -140,10 +145,24 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V
return rsp, nil
}
rsp.Values = mathexp.Values{
mathexp.TableData{Frame: frame},
}
switch gr.format {
case "alerting":
numberSet, err := extractNumberSetFromSQLForAlerting(frame)
if err != nil {
rsp.Error = err
return rsp, nil
}
vals := make([]mathexp.Value, 0, len(numberSet))
for i := range numberSet {
vals = append(vals, numberSet[i])
}
rsp.Values = vals
default:
rsp.Values = mathexp.Values{
mathexp.TableData{Frame: frame},
}
}
return rsp, nil
}
@ -162,3 +181,57 @@ func totalCells(frames []*data.Frame) (total int64) {
}
return
}
func extractNumberSetFromSQLForAlerting(frame *data.Frame) ([]mathexp.Number, error) {
var (
numericField *data.Field
numericFieldIx int
)
// Find the only numeric field
for i, f := range frame.Fields {
if f.Type().Numeric() {
if numericField != nil {
return nil, fmt.Errorf("expected exactly one numeric field, but found multiple")
}
numericField = f
numericFieldIx = i
}
}
if numericField == nil {
return nil, fmt.Errorf("no numeric field found in frame")
}
numbers := make([]mathexp.Number, frame.Rows())
for i := 0; i < frame.Rows(); i++ {
val, err := numericField.FloatAt(i)
if err != nil {
return nil, fmt.Errorf("failed to read numeric value at row %d: %w", i, err)
}
labels := data.Labels{}
for j, f := range frame.Fields {
if j == numericFieldIx || (f.Type() != data.FieldTypeString && f.Type() != data.FieldTypeNullableString) {
continue
}
val := f.At(i)
switch v := val.(type) {
case *string:
if v != nil {
labels[f.Name] = *v
}
case string:
labels[f.Name] = v
}
}
n := mathexp.NewNumber(numericField.Name, labels)
n.Frame.Fields[0].Config = numericField.Config
n.SetValue(&val)
numbers[i] = n
}
return numbers, nil
}