SQL Expressions: Rework backend errors and error instrumentation (#109633)
* Capture error_type label on metrics/traces * Make error messages more helpful to user * Use errutil, categorized errors, and tie them to error_type (category in code) * Misc trace fixes * Add metric to track SQL input conversion
This commit is contained in:
@@ -8,17 +8,19 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||
"github.com/grafana/grafana/pkg/expr/metrics"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func TestNewCommand(t *testing.T) {
|
||||
cmd, err := NewSQLCommand(t.Context(), "a", "", "select a from foo, bar", 0, 0, 0)
|
||||
cmd, err := NewSQLCommand(t.Context(), log.NewNullLogger(), "a", "", "select a from foo, bar", 0, 0, 0)
|
||||
if err != nil && strings.Contains(err.Error(), "feature is not enabled") {
|
||||
return
|
||||
}
|
||||
@@ -91,7 +93,7 @@ func TestSQLCommandCellLimits(t *testing.T) {
|
||||
},
|
||||
vars: []string{"foo"},
|
||||
expectError: true,
|
||||
errorContains: "exceeds limit",
|
||||
errorContains: "exceeded the configured limit",
|
||||
},
|
||||
{
|
||||
name: "single (wide) frame exceeds cell limit",
|
||||
@@ -101,7 +103,7 @@ func TestSQLCommandCellLimits(t *testing.T) {
|
||||
},
|
||||
vars: []string{"foo"},
|
||||
expectError: true,
|
||||
errorContains: "exceeds limit",
|
||||
errorContains: "exceeded the configured limit",
|
||||
},
|
||||
{
|
||||
name: "multiple frames exceed cell limit",
|
||||
@@ -112,7 +114,7 @@ func TestSQLCommandCellLimits(t *testing.T) {
|
||||
},
|
||||
vars: []string{"foo", "bar"},
|
||||
expectError: true,
|
||||
errorContains: "exceeds limit",
|
||||
errorContains: "exceeded the configured limit",
|
||||
},
|
||||
{
|
||||
name: "limit of 0 means no limit: allow large frame",
|
||||
@@ -126,7 +128,7 @@ func TestSQLCommandCellLimits(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd, err := NewSQLCommand(t.Context(), "a", "", "select a from foo, bar", tt.limit, 0, 0)
|
||||
cmd, err := NewSQLCommand(t.Context(), log.New(), "a", "", "select a from foo, bar", tt.limit, 0, 0)
|
||||
require.NoError(t, err, "Failed to create SQL command")
|
||||
|
||||
vars := mathexp.Vars{}
|
||||
@@ -154,15 +156,15 @@ func TestSQLCommandMetrics(t *testing.T) {
|
||||
m := metrics.NewTestMetrics()
|
||||
|
||||
// Create a command
|
||||
cmd, err := NewSQLCommand(t.Context(), "A", "someformat", "select * from foo", 0, 0, 0)
|
||||
cmd, err := NewSQLCommand(t.Context(), log.NewNullLogger(), "A", "someformat", "select * from foo", 0, 0, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Execute successful command
|
||||
_, err = cmd.Execute(context.Background(), time.Now(), mathexp.Vars{}, &testTracer{}, m)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify error count was not incremented
|
||||
require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandCount), "Expected error metric not to be recorded")
|
||||
// Verify count metric was recorded
|
||||
require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandCount), "Expected count metric to be recorded")
|
||||
|
||||
// Verify duration was recorded
|
||||
require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandDuration), "Expected duration metric to be recorded")
|
||||
@@ -171,6 +173,90 @@ func TestSQLCommandMetrics(t *testing.T) {
|
||||
require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandCellCount), "Expected cell count metric to be recorded")
|
||||
}
|
||||
|
||||
func TestHandleSqlInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
frames data.Frames
|
||||
expectErr string
|
||||
expectFrame bool
|
||||
converted bool
|
||||
}{
|
||||
{
|
||||
name: "single frame with no fields and no type is passed through",
|
||||
frames: data.Frames{data.NewFrame("")},
|
||||
expectFrame: true,
|
||||
},
|
||||
{
|
||||
name: "single frame with no fields but type timeseries-multi is passed through",
|
||||
frames: data.Frames{data.NewFrame("").SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti})},
|
||||
expectFrame: true,
|
||||
},
|
||||
{
|
||||
name: "single frame, no labels, no type → passes through",
|
||||
frames: data.Frames{
|
||||
data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
||||
data.NewField("value", nil, []*float64{fp(2)}),
|
||||
),
|
||||
},
|
||||
expectFrame: true,
|
||||
},
|
||||
{
|
||||
name: "single frame with labels, but missing FrameMeta.Type → error",
|
||||
frames: data.Frames{
|
||||
data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
||||
data.NewField("value", data.Labels{"foo": "bar"}, []*float64{fp(2)}),
|
||||
),
|
||||
},
|
||||
expectErr: "labels in the response that can not be mapped to a table",
|
||||
},
|
||||
{
|
||||
name: "multiple frames, no type → error",
|
||||
frames: data.Frames{
|
||||
data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
||||
data.NewField("value", nil, []*float64{fp(2)}),
|
||||
),
|
||||
data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
||||
data.NewField("value", nil, []*float64{fp(2)}),
|
||||
),
|
||||
},
|
||||
expectErr: "more than one dataframe that can not be automatically mapped to a single table",
|
||||
},
|
||||
{
|
||||
name: "supported type (timeseries-multi) triggers ConvertToFullLong",
|
||||
frames: data.Frames{
|
||||
data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
||||
data.NewField("value", data.Labels{"host": "a"}, []*float64{fp(2)}),
|
||||
).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti}),
|
||||
},
|
||||
expectFrame: true,
|
||||
converted: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, c := handleSqlInput(t.Context(), &testTracer{}, "a", map[string]struct{}{"b": {}}, "fakeDS", tc.frames)
|
||||
require.Equal(t, tc.converted, c, "conversion bool mismatch")
|
||||
if tc.expectErr != "" {
|
||||
require.Error(t, res.Error)
|
||||
require.ErrorContains(t, res.Error, tc.expectErr)
|
||||
} else {
|
||||
require.NoError(t, res.Error)
|
||||
if tc.expectFrame {
|
||||
require.Len(t, res.Values, 1)
|
||||
require.IsType(t, mathexp.TableData{}, res.Values[0])
|
||||
require.NotNil(t, res.Values[0].(mathexp.TableData).Frame)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testTracer struct {
|
||||
trace.Tracer
|
||||
}
|
||||
@@ -193,3 +279,7 @@ func (ts *testSpan) RecordError(err error, opt ...trace.EventOption) {
|
||||
}
|
||||
|
||||
func (ts *testSpan) SetStatus(code codes.Code, msg string) {}
|
||||
|
||||
func (ts *testSpan) AddEvent(name string, opts ...trace.EventOption) {}
|
||||
|
||||
func (ts *testSpan) SetAttributes(kv ...attribute.KeyValue) {}
|
||||
|
||||
Reference in New Issue
Block a user