From c2ffd31b1a3d8db0dc83de1ad34aab1ad8fddca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?= Date: Tue, 4 Feb 2025 12:40:33 +0100 Subject: [PATCH] Add error source (#1954) Fixes #1879 --- pkg/datasource/datasource.go | 3 +- pkg/datasource/functions.go | 11 +-- pkg/datasource/functions_test.go | 128 +++++++++++++++++++++++++++++ pkg/datasource/response_handler.go | 9 +- pkg/zabbix/type_converters.go | 2 + pkg/zabbix/utils.go | 3 +- pkg/zabbix/zabbix.go | 4 +- pkg/zabbix/zabbix_test.go | 3 +- pkg/zabbixapi/zabbix_api.go | 19 +++-- pkg/zabbixapi/zabbix_api_test.go | 3 +- 10 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 pkg/datasource/functions_test.go diff --git a/pkg/datasource/datasource.go b/pkg/datasource/datasource.go index 392b572..2ed8042 100644 --- a/pkg/datasource/datasource.go +++ b/pkg/datasource/datasource.go @@ -16,7 +16,6 @@ import ( ) var ( - ErrFunctionsNotSupported = errors.New("zabbix queries with functions are not supported") ErrNonMetricQueryNotSupported = errors.New("non-metrics queries are not supported") ) @@ -134,7 +133,7 @@ func (ds *ZabbixDatasource) QueryData(ctx context.Context, req *backend.QueryDat res.Frames = append(res.Frames, frames...) } } else { - res.Error = ErrNonMetricQueryNotSupported + res.Error = backend.DownstreamError(ErrNonMetricQueryNotSupported) } qdr.Responses[q.RefID] = res } diff --git a/pkg/datasource/functions.go b/pkg/datasource/functions.go index 075c712..95ae7a3 100644 --- a/pkg/datasource/functions.go +++ b/pkg/datasource/functions.go @@ -8,6 +8,7 @@ import ( "github.com/alexanderzobnin/grafana-zabbix/pkg/gtime" "github.com/alexanderzobnin/grafana-zabbix/pkg/timeseries" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" + "github.com/grafana/grafana-plugin-sdk-go/backend" ) const RANGE_VARIABLE_VALUE = "range_series" @@ -103,26 +104,26 @@ func applyFunctions(series []*timeseries.TimeSeriesData, functions []QueryFuncti for _, s := range series { result, err := applyFunc(s.TS, f.Params...) if err != nil { - return nil, err + return nil, backend.DownstreamError(err) } s.TS = result } } else if applyAggFunc, ok := aggFuncMap[f.Def.Name]; ok { result, err := applyAggFunc(series, f.Params...) if err != nil { - return nil, err + return nil, backend.DownstreamError(err) } series = result } else if applyFilterFunc, ok := filterFuncMap[f.Def.Name]; ok { result, err := applyFilterFunc(series, f.Params...) if err != nil { - return nil, err + return nil, backend.DownstreamError(err) } series = result } else if _, ok := skippedFuncMap[f.Def.Name]; ok { continue } else { - err := errFunctionNotSupported(f.Def.Name) + err := backend.DownstreamError(errFunctionNotSupported(f.Def.Name)) return series, err } } @@ -135,7 +136,7 @@ func applyFunctionsPre(query *QueryModel, items []*zabbix.Item) error { if applyFunc, ok := timeFuncMap[f.Def.Name]; ok { err := applyFunc(query, items, f.Params...) if err != nil { - return err + return backend.DownstreamError(err) } } } diff --git a/pkg/datasource/functions_test.go b/pkg/datasource/functions_test.go new file mode 100644 index 0000000..10f3e8f --- /dev/null +++ b/pkg/datasource/functions_test.go @@ -0,0 +1,128 @@ +package datasource + +import ( + "testing" + "time" + + "github.com/alexanderzobnin/grafana-zabbix/pkg/timeseries" + "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/assert" +) + +func TestApplyFunctionsFunction(t *testing.T) { + f := new(float64) + *f = 1.0 + series := []*timeseries.TimeSeriesData{ + { + TS: timeseries.TimeSeries{ + {Time: time.Time{}, Value: f}, + {Time: time.Time{}, Value: f}, + }, + }, + } + + tests := []struct { + name string + functions []QueryFunction + wantErr bool + }{ + { + name: "unsupported function", + functions: []QueryFunction{ + { + Def: QueryFunctionDef{ + Name: "unsupportedFunction", + }, + Params: []interface{}{}, + }, + }, + wantErr: true, + }, + { + name: "data processing function with params error", + functions: []QueryFunction{ + { + Def: QueryFunctionDef{ + Name: "groupBy", + }, + Params: []interface { + }{1}, + }, + }, + wantErr: true, + }, + { + name: "aggregate function with params error", + functions: []QueryFunction{ + { + Def: QueryFunctionDef{ + Name: "aggregateBy", + }, + Params: []interface { + }{1}, + }, + }, + wantErr: true, + }, + { + name: "filter function with params error", + functions: []QueryFunction{ + { + Def: QueryFunctionDef{ + Name: "top", + }, + Params: []interface { + }{"string"}, + }, + }, + wantErr: true, + }, + { + name: "skipped function should return no error", + functions: []QueryFunction{ + { + Def: QueryFunctionDef{ + Name: "setAlias", + }, + Params: []interface { + }{}, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := applyFunctions(series, tt.functions) + if tt.wantErr { + assert.Error(t, err, "expected error for function") + // Check if the error is a downstream error + assert.Truef(t, backend.IsDownstreamError(err), "error is not a downstream error") + } else { + assert.NoError(t, err) + } + }) + } +} +// TestApplyFunctionsPreFunction tests the applyFunctionsPre function for error handling +func TestApplyFunctionsPreFunction(t *testing.T) { + query := QueryModel{ + Functions: []QueryFunction{ + { + Def: QueryFunctionDef{ + Name: "timeShift", + }, + Params: []interface{}{1}, + }, + }} + + items := []*zabbix.Item{} + err := applyFunctionsPre(&query, items) + + assert.Error(t, err, "expected error for function") + // Check if the error is a downstream error + assert.Truef(t, backend.IsDownstreamError(err), "error is not a downstream error") + +} diff --git a/pkg/datasource/response_handler.go b/pkg/datasource/response_handler.go index c1834fc..aa91440 100644 --- a/pkg/datasource/response_handler.go +++ b/pkg/datasource/response_handler.go @@ -10,6 +10,7 @@ import ( "github.com/alexanderzobnin/grafana-zabbix/pkg/timeseries" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" ) @@ -154,19 +155,19 @@ func getTrendPointValue(point zabbix.TrendPoint, valueType string) (float64, err value, err := strconv.ParseFloat(valueStr, 64) if err != nil { - return 0, fmt.Errorf("error parsing trend value: %s", err) + return 0, backend.DownstreamError(fmt.Errorf("error parsing trend value: %s", err)) } return value, nil } else if valueType == "sum" { avgStr := point.ValueAvg avg, err := strconv.ParseFloat(avgStr, 64) if err != nil { - return 0, fmt.Errorf("error parsing trend value: %s", err) + return 0, backend.DownstreamError(fmt.Errorf("error parsing trend value: %s", err)) } countStr := point.Num count, err := strconv.ParseFloat(countStr, 64) if err != nil { - return 0, fmt.Errorf("error parsing trend value: %s", err) + return 0, backend.DownstreamError(fmt.Errorf("error parsing trend value: %s", err)) } if count > 0 { return avg * count, nil @@ -175,7 +176,7 @@ func getTrendPointValue(point zabbix.TrendPoint, valueType string) (float64, err } } - return 0, fmt.Errorf("failed to get trend value, unknown value type: %s", valueType) + return 0, backend.DownstreamError(fmt.Errorf("failed to get trend value, unknown value type: %s", valueType)) } var fixedUpdateIntervalPattern = regexp.MustCompile(`^(\d+)([smhdw]?)$`) diff --git a/pkg/zabbix/type_converters.go b/pkg/zabbix/type_converters.go index 9640d2a..b2c9091 100644 --- a/pkg/zabbix/type_converters.go +++ b/pkg/zabbix/type_converters.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/bitly/go-simplejson" + "github.com/grafana/grafana-plugin-sdk-go/backend" ) func convertTo(value *simplejson.Json, result interface{}) error { @@ -14,6 +15,7 @@ func convertTo(value *simplejson.Json, result interface{}) error { err = json.Unmarshal(valueJSON, result) if err != nil { + backend.Logger.Debug("Error unmarshalling JSON", "error", err, "result", result) return err } diff --git a/pkg/zabbix/utils.go b/pkg/zabbix/utils.go index 72ea8d8..8bea620 100644 --- a/pkg/zabbix/utils.go +++ b/pkg/zabbix/utils.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/dlclark/regexp2" + "github.com/grafana/grafana-plugin-sdk-go/backend" ) func (item *Item) ExpandItemName() string { @@ -79,7 +80,7 @@ func parseFilter(filter string) (*regexp2.Regexp, error) { if flagRE.MatchString(matches[2]) { pattern += "(?" + matches[2] + ")" } else { - return nil, fmt.Errorf("error parsing regexp: unsupported flags `%s` (expected [%s])", matches[2], vaildREModifiers) + return nil, backend.DownstreamError(fmt.Errorf("error parsing regexp: unsupported flags `%s` (expected [%s])", matches[2], vaildREModifiers)) } } pattern += matches[1] diff --git a/pkg/zabbix/zabbix.go b/pkg/zabbix/zabbix.go index cae30e5..5eed99f 100644 --- a/pkg/zabbix/zabbix.go +++ b/pkg/zabbix/zabbix.go @@ -95,7 +95,7 @@ func (zabbix *Zabbix) request(ctx context.Context, method string, params ZabbixA result, err := zabbix.api.Request(ctx, method, params, zabbix.version) notAuthorized := isNotAuthorized(err) isTokenAuth := zabbix.settings.AuthType == settings.AuthTypeToken - if err == zabbixapi.ErrNotAuthenticated || (notAuthorized && !isTokenAuth) { + if err == backend.DownstreamError(zabbixapi.ErrNotAuthenticated) || (notAuthorized && !isTokenAuth) { if notAuthorized { zabbix.logger.Debug("Authentication token expired, performing re-login") } @@ -121,7 +121,7 @@ func (zabbix *Zabbix) Authenticate(ctx context.Context) error { if authType == settings.AuthTypeToken { token, exists := zabbix.dsInfo.DecryptedSecureJSONData["apiToken"] if !exists { - return errors.New("cannot find Zabbix API token") + return backend.DownstreamError(errors.New("cannot find Zabbix API token")) } err = zabbix.api.AuthenticateWithToken(ctx, token) if err != nil { diff --git a/pkg/zabbix/zabbix_test.go b/pkg/zabbix/zabbix_test.go index 3e51864..25cabeb 100644 --- a/pkg/zabbix/zabbix_test.go +++ b/pkg/zabbix/zabbix_test.go @@ -2,9 +2,10 @@ package zabbix import ( "context" - "github.com/alexanderzobnin/grafana-zabbix/pkg/settings" "testing" + "github.com/alexanderzobnin/grafana-zabbix/pkg/settings" + "github.com/stretchr/testify/assert" "github.com/grafana/grafana-plugin-sdk-go/backend" diff --git a/pkg/zabbixapi/zabbix_api.go b/pkg/zabbixapi/zabbix_api.go index 72bb8fa..9f4bab0 100644 --- a/pkg/zabbixapi/zabbix_api.go +++ b/pkg/zabbixapi/zabbix_api.go @@ -15,6 +15,7 @@ import ( "github.com/bitly/go-simplejson" "golang.org/x/net/context/ctxhttp" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" ) @@ -76,7 +77,7 @@ func (api *ZabbixAPI) SetAuth(auth string) { // Request performs API request func (api *ZabbixAPI) Request(ctx context.Context, method string, params ZabbixAPIParams, version int) (*simplejson.Json, error) { if api.auth == "" { - return nil, ErrNotAuthenticated + return nil, backend.DownstreamError(ErrNotAuthenticated) } return api.request(ctx, method, params, api.auth, version) @@ -177,7 +178,7 @@ func (api *ZabbixAPI) Authenticate(ctx context.Context, username string, passwor // AuthenticateWithToken performs authentication with API token. func (api *ZabbixAPI) AuthenticateWithToken(ctx context.Context, token string) error { if token == "" { - return errors.New("API token is empty") + return backend.DownstreamError(errors.New("API token is empty")) } api.SetAuth(token) return nil @@ -198,8 +199,8 @@ func handleAPIResult(response []byte) (*simplejson.Json, error) { return nil, err } if errJSON, isError := jsonResp.CheckGet("error"); isError { - errMessage := fmt.Sprintf("%s %s", errJSON.Get("message").MustString(), errJSON.Get("data").MustString()) - return nil, errors.New(errMessage) + errMessage := fmt.Errorf("%s %s", errJSON.Get("message").MustString(), errJSON.Get("data").MustString()) + return nil, backend.DownstreamError(errMessage) } jsonResult := jsonResp.Get("result") return jsonResult, nil @@ -211,12 +212,20 @@ func makeHTTPRequest(ctx context.Context, httpClient *http.Client, req *http.Req res, err := ctxhttp.Do(ctx, httpClient, req) if err != nil { + if backend.IsDownstreamHTTPError(err) { + return nil, backend.DownstreamError(err) + } return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("request failed, status: %v", res.Status) + err = fmt.Errorf("request failed, status: %v", res.Status) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return nil, backend.DownstreamError(err) + } + + return nil, err } body, err := io.ReadAll(res.Body) diff --git a/pkg/zabbixapi/zabbix_api_test.go b/pkg/zabbixapi/zabbix_api_test.go index 6d3e585..77cf1a2 100644 --- a/pkg/zabbixapi/zabbix_api_test.go +++ b/pkg/zabbixapi/zabbix_api_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/stretchr/testify/assert" ) @@ -50,7 +51,7 @@ func TestZabbixAPI(t *testing.T) { mockApiResponse: `{"result":"sampleResult"}`, mockApiResponseCode: 200, expectedResult: "", - expectedError: ErrNotAuthenticated, + expectedError: backend.DownstreamError(ErrNotAuthenticated), }, }