@@ -16,7 +16,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrFunctionsNotSupported = errors.New("zabbix queries with functions are not supported")
|
|
||||||
ErrNonMetricQueryNotSupported = errors.New("non-metrics queries 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...)
|
res.Frames = append(res.Frames, frames...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.Error = ErrNonMetricQueryNotSupported
|
res.Error = backend.DownstreamError(ErrNonMetricQueryNotSupported)
|
||||||
}
|
}
|
||||||
qdr.Responses[q.RefID] = res
|
qdr.Responses[q.RefID] = res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/gtime"
|
"github.com/alexanderzobnin/grafana-zabbix/pkg/gtime"
|
||||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/timeseries"
|
"github.com/alexanderzobnin/grafana-zabbix/pkg/timeseries"
|
||||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix"
|
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
const RANGE_VARIABLE_VALUE = "range_series"
|
const RANGE_VARIABLE_VALUE = "range_series"
|
||||||
@@ -103,26 +104,26 @@ func applyFunctions(series []*timeseries.TimeSeriesData, functions []QueryFuncti
|
|||||||
for _, s := range series {
|
for _, s := range series {
|
||||||
result, err := applyFunc(s.TS, f.Params...)
|
result, err := applyFunc(s.TS, f.Params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, backend.DownstreamError(err)
|
||||||
}
|
}
|
||||||
s.TS = result
|
s.TS = result
|
||||||
}
|
}
|
||||||
} else if applyAggFunc, ok := aggFuncMap[f.Def.Name]; ok {
|
} else if applyAggFunc, ok := aggFuncMap[f.Def.Name]; ok {
|
||||||
result, err := applyAggFunc(series, f.Params...)
|
result, err := applyAggFunc(series, f.Params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, backend.DownstreamError(err)
|
||||||
}
|
}
|
||||||
series = result
|
series = result
|
||||||
} else if applyFilterFunc, ok := filterFuncMap[f.Def.Name]; ok {
|
} else if applyFilterFunc, ok := filterFuncMap[f.Def.Name]; ok {
|
||||||
result, err := applyFilterFunc(series, f.Params...)
|
result, err := applyFilterFunc(series, f.Params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, backend.DownstreamError(err)
|
||||||
}
|
}
|
||||||
series = result
|
series = result
|
||||||
} else if _, ok := skippedFuncMap[f.Def.Name]; ok {
|
} else if _, ok := skippedFuncMap[f.Def.Name]; ok {
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
err := errFunctionNotSupported(f.Def.Name)
|
err := backend.DownstreamError(errFunctionNotSupported(f.Def.Name))
|
||||||
return series, err
|
return series, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +136,7 @@ func applyFunctionsPre(query *QueryModel, items []*zabbix.Item) error {
|
|||||||
if applyFunc, ok := timeFuncMap[f.Def.Name]; ok {
|
if applyFunc, ok := timeFuncMap[f.Def.Name]; ok {
|
||||||
err := applyFunc(query, items, f.Params...)
|
err := applyFunc(query, items, f.Params...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return backend.DownstreamError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
128
pkg/datasource/functions_test.go
Normal file
128
pkg/datasource/functions_test.go
Normal file
@@ -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")
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/timeseries"
|
"github.com/alexanderzobnin/grafana-zabbix/pkg/timeseries"
|
||||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix"
|
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"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)
|
value, err := strconv.ParseFloat(valueStr, 64)
|
||||||
if err != nil {
|
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
|
return value, nil
|
||||||
} else if valueType == "sum" {
|
} else if valueType == "sum" {
|
||||||
avgStr := point.ValueAvg
|
avgStr := point.ValueAvg
|
||||||
avg, err := strconv.ParseFloat(avgStr, 64)
|
avg, err := strconv.ParseFloat(avgStr, 64)
|
||||||
if err != nil {
|
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
|
countStr := point.Num
|
||||||
count, err := strconv.ParseFloat(countStr, 64)
|
count, err := strconv.ParseFloat(countStr, 64)
|
||||||
if err != nil {
|
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 {
|
if count > 0 {
|
||||||
return avg * count, nil
|
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]?)$`)
|
var fixedUpdateIntervalPattern = regexp.MustCompile(`^(\d+)([smhdw]?)$`)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/bitly/go-simplejson"
|
"github.com/bitly/go-simplejson"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func convertTo(value *simplejson.Json, result interface{}) error {
|
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)
|
err = json.Unmarshal(valueJSON, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
backend.Logger.Debug("Error unmarshalling JSON", "error", err, "result", result)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dlclark/regexp2"
|
"github.com/dlclark/regexp2"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (item *Item) ExpandItemName() string {
|
func (item *Item) ExpandItemName() string {
|
||||||
@@ -79,7 +80,7 @@ func parseFilter(filter string) (*regexp2.Regexp, error) {
|
|||||||
if flagRE.MatchString(matches[2]) {
|
if flagRE.MatchString(matches[2]) {
|
||||||
pattern += "(?" + matches[2] + ")"
|
pattern += "(?" + matches[2] + ")"
|
||||||
} else {
|
} 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]
|
pattern += matches[1]
|
||||||
|
|||||||
@@ -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)
|
result, err := zabbix.api.Request(ctx, method, params, zabbix.version)
|
||||||
notAuthorized := isNotAuthorized(err)
|
notAuthorized := isNotAuthorized(err)
|
||||||
isTokenAuth := zabbix.settings.AuthType == settings.AuthTypeToken
|
isTokenAuth := zabbix.settings.AuthType == settings.AuthTypeToken
|
||||||
if err == zabbixapi.ErrNotAuthenticated || (notAuthorized && !isTokenAuth) {
|
if err == backend.DownstreamError(zabbixapi.ErrNotAuthenticated) || (notAuthorized && !isTokenAuth) {
|
||||||
if notAuthorized {
|
if notAuthorized {
|
||||||
zabbix.logger.Debug("Authentication token expired, performing re-login")
|
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 {
|
if authType == settings.AuthTypeToken {
|
||||||
token, exists := zabbix.dsInfo.DecryptedSecureJSONData["apiToken"]
|
token, exists := zabbix.dsInfo.DecryptedSecureJSONData["apiToken"]
|
||||||
if !exists {
|
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)
|
err = zabbix.api.AuthenticateWithToken(ctx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package zabbix
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/bitly/go-simplejson"
|
"github.com/bitly/go-simplejson"
|
||||||
"golang.org/x/net/context/ctxhttp"
|
"golang.org/x/net/context/ctxhttp"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ func (api *ZabbixAPI) SetAuth(auth string) {
|
|||||||
// Request performs API request
|
// Request performs API request
|
||||||
func (api *ZabbixAPI) Request(ctx context.Context, method string, params ZabbixAPIParams, version int) (*simplejson.Json, error) {
|
func (api *ZabbixAPI) Request(ctx context.Context, method string, params ZabbixAPIParams, version int) (*simplejson.Json, error) {
|
||||||
if api.auth == "" {
|
if api.auth == "" {
|
||||||
return nil, ErrNotAuthenticated
|
return nil, backend.DownstreamError(ErrNotAuthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.request(ctx, method, params, api.auth, version)
|
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.
|
// AuthenticateWithToken performs authentication with API token.
|
||||||
func (api *ZabbixAPI) AuthenticateWithToken(ctx context.Context, token string) error {
|
func (api *ZabbixAPI) AuthenticateWithToken(ctx context.Context, token string) error {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return errors.New("API token is empty")
|
return backend.DownstreamError(errors.New("API token is empty"))
|
||||||
}
|
}
|
||||||
api.SetAuth(token)
|
api.SetAuth(token)
|
||||||
return nil
|
return nil
|
||||||
@@ -198,8 +199,8 @@ func handleAPIResult(response []byte) (*simplejson.Json, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if errJSON, isError := jsonResp.CheckGet("error"); isError {
|
if errJSON, isError := jsonResp.CheckGet("error"); isError {
|
||||||
errMessage := fmt.Sprintf("%s %s", errJSON.Get("message").MustString(), errJSON.Get("data").MustString())
|
errMessage := fmt.Errorf("%s %s", errJSON.Get("message").MustString(), errJSON.Get("data").MustString())
|
||||||
return nil, errors.New(errMessage)
|
return nil, backend.DownstreamError(errMessage)
|
||||||
}
|
}
|
||||||
jsonResult := jsonResp.Get("result")
|
jsonResult := jsonResp.Get("result")
|
||||||
return jsonResult, nil
|
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)
|
res, err := ctxhttp.Do(ctx, httpClient, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if backend.IsDownstreamHTTPError(err) {
|
||||||
|
return nil, backend.DownstreamError(err)
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
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)
|
body, err := io.ReadAll(res.Body)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ func TestZabbixAPI(t *testing.T) {
|
|||||||
mockApiResponse: `{"result":"sampleResult"}`,
|
mockApiResponse: `{"result":"sampleResult"}`,
|
||||||
mockApiResponseCode: 200,
|
mockApiResponseCode: 200,
|
||||||
expectedResult: "",
|
expectedResult: "",
|
||||||
expectedError: ErrNotAuthenticated,
|
expectedError: backend.DownstreamError(ErrNotAuthenticated),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user