diff --git a/go.mod b/go.mod index 6476efd..1b61780 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/kr/pretty v0.1.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.8.1 // indirect + github.com/stretchr/testify v1.3.0 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect golang.org/x/text v0.3.2 // indirect diff --git a/pkg/datasource.go b/pkg/datasource.go index 1bbec41..f591eda 100644 --- a/pkg/datasource.go +++ b/pkg/datasource.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "runtime/debug" simplejson "github.com/bitly/go-simplejson" "github.com/grafana/grafana_plugin_model/go/datasource" @@ -27,7 +28,16 @@ func (b *ZabbixBackend) newZabbixDatasource() *ZabbixDatasource { // Query receives requests from the Grafana backend. Requests are filtered by query type and sent to the // applicable ZabbixDatasource. -func (b *ZabbixBackend) Query(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) { +func (b *ZabbixBackend) Query(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (resp *datasource.DatasourceResponse, _e error) { + defer func() { + if r := recover(); r != nil { + pErr, _ := r.(error) + b.logger.Error("Fatal error in Zabbix Plugin Backend", "Error", pErr) + b.logger.Error(string(debug.Stack())) + resp = BuildErrorResponse(fmt.Errorf("Unrecoverable error in grafana-zabbix plugin backend")) + } + }() + zabbixDs := b.getCachedDatasource(tsdbReq) queryType, err := GetQueryType(tsdbReq) @@ -37,13 +47,17 @@ func (b *ZabbixBackend) Query(ctx context.Context, tsdbReq *datasource.Datasourc switch queryType { case "zabbixAPI": - return zabbixDs.ZabbixAPIQuery(ctx, tsdbReq) + resp, err = zabbixDs.ZabbixAPIQuery(ctx, tsdbReq) + case "query": + resp, err = zabbixDs.queryNumericItems(ctx, tsdbReq) case "connectionTest": - return zabbixDs.TestConnection(ctx, tsdbReq) + resp, err = zabbixDs.TestConnection(ctx, tsdbReq) default: err = errors.New("Query not implemented") return BuildErrorResponse(err), nil } + + return } func (b *ZabbixBackend) getCachedDatasource(tsdbReq *datasource.DatasourceRequest) *ZabbixDatasource { @@ -59,7 +73,10 @@ func (b *ZabbixBackend) getCachedDatasource(tsdbReq *datasource.DatasourceReques dsInfo := tsdbReq.GetDatasource() b.logger.Debug(fmt.Sprintf("Datasource cache miss (Org %d Id %d '%s' %s)", dsInfo.GetOrgId(), dsInfo.GetId(), dsInfo.GetName(), dsInfoHash)) } - return b.newZabbixDatasource() + + ds := b.newZabbixDatasource() + b.datasourceCache.Set(dsInfoHash, ds) + return ds } // GetQueryType determines the query type from a query or list of queries @@ -104,3 +121,15 @@ func BuildErrorResponse(err error) *datasource.DatasourceResponse { }, } } + +// BuildMetricsResponse builds a response object using a given TimeSeries array +func BuildMetricsResponse(metrics []*datasource.TimeSeries) (*datasource.DatasourceResponse, error) { + return &datasource.DatasourceResponse{ + Results: []*datasource.QueryResult{ + &datasource.QueryResult{ + RefId: "zabbixMetrics", + Series: metrics, + }, + }, + }, nil +} diff --git a/pkg/datasource_test.go b/pkg/datasource_test.go index 26738a5..2d861fe 100644 --- a/pkg/datasource_test.go +++ b/pkg/datasource_test.go @@ -78,6 +78,11 @@ func TestZabbixBackend_getCachedDatasource(t *testing.T) { // Only checking the authToken, being the easiest value to, and guarantee equality for assert.Equal(t, tt.want.authToken, got.authToken) + + // Ensure the datasource is in the cache + cacheds, ok := tt.cache.Get(HashDatasourceInfo(tt.request.GetDatasource())) + assert.Equal(t, true, ok) + assert.Equal(t, got, cacheds) }) } } diff --git a/pkg/models.go b/pkg/models.go index b252f00..be0b258 100644 --- a/pkg/models.go +++ b/pkg/models.go @@ -1,5 +1,10 @@ package main +import ( + "encoding/json" + "fmt" +) + type connectionTestResponse struct { ZabbixVersion string `json:"zabbixVersion"` DbConnectorStatus *dbConnectionStatus `json:"dbConnectorStatus"` @@ -9,3 +14,77 @@ type dbConnectionStatus struct { DsType string `json:"dsType"` DsName string `json:"dsName"` } + +type requestModel struct { + Target queryRequest `json:"target,omitempty"` +} + +type queryRequest struct { + Method string `json:"method,omitempty"` + Params zabbixParams `json:"params,omitempty"` +} + +type zabbixParamOutput struct { + Mode string + Fields []string +} + +func (p *zabbixParamOutput) MarshalJSON() ([]byte, error) { + if p.Mode != "" { + return json.Marshal(p.Mode) + } + + return json.Marshal(p.Fields) +} + +func (p *zabbixParamOutput) UnmarshalJSON(data []byte) error { + if p == nil { + return fmt.Errorf("zabbixParamOutput: UnmarshalJSON on nil pointer") + } + + var strArray []string + err := json.Unmarshal(data, &strArray) + if err == nil { + p.Fields = strArray + return nil + } + + var str string + err = json.Unmarshal(data, &str) + if err == nil { + p.Mode = str + return nil + } + + return fmt.Errorf("Unsupported type: %w", err) + +} + +type zabbixParams struct { + Output *zabbixParamOutput `json:"output,omitempty"` + SortField string `json:"sortfield,omitempty"` + SortOrder string `json:"sortorder,omitempty"` + Filter map[string][]int `json:"filter,omitempty"` + + // Login + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` + + // Item GET + WebItems bool `json:"webitems,omitempty"` + SelectHosts []string `json:"selectHosts,omitempty"` + ItemIDs []string `json:"itemids,omitempty"` + GroupIDs []string `json:"groupids,omitempty"` + HostIDs []string `json:"hostids,omitempty"` + AppIDs []string `json:"applicationids,omitempty"` + + // Host Group GET + RealHosts bool `json:"real_hosts,omitempty"` + + // History GET + History *int `json:"history,omitempty,string"` + + // History/Trends GET + TimeFrom int64 `json:"time_from,omitempty"` + TimeTill int64 `json:"time_till,omitempty"` +} diff --git a/pkg/models_test.go b/pkg/models_test.go new file mode 100644 index 0000000..22c6128 --- /dev/null +++ b/pkg/models_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_zabbixParamOutput(t *testing.T) { + tests := []struct { + name string + input zabbixParams + want string + }{ + { + name: "Mode extend", + input: zabbixParams{ + Output: &zabbixParamOutput{ + Mode: "extend", + }, + GroupIDs: []string{"test1", "test2"}, + }, + want: `{ "output": "extend", "groupids": ["test1", "test2"] }`, + }, + { + name: "Fields", + input: zabbixParams{ + Output: &zabbixParamOutput{ + Fields: []string{"name", "key_", "hostid"}, + }, + GroupIDs: []string{"test1", "test2"}, + }, + want: `{ "output": ["name", "key_", "hostid"], "groupids": ["test1", "test2"] }`, + }, + { + name: "No Output", + input: zabbixParams{ + GroupIDs: []string{"test1", "test2"}, + }, + want: `{ "groupids": ["test1", "test2"] }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jsonOut, err := json.Marshal(tt.input) + fmt.Printf("Output: %s\n", jsonOut) + assert.NoError(t, err) + if !assert.JSONEq(t, tt.want, string(jsonOut)) { + return + } + + objOut := zabbixParams{} + err = json.Unmarshal(jsonOut, &objOut) + assert.NoError(t, err) + assert.Equal(t, tt.input, objOut) + }) + } +} diff --git a/pkg/zabbix/response_models.go b/pkg/zabbix/response_models.go new file mode 100644 index 0000000..866e85c --- /dev/null +++ b/pkg/zabbix/response_models.go @@ -0,0 +1,35 @@ +package zabbix + +type Items []Item +type Item struct { + ID string `json:"itemid,omitempty"` + Key string `json:"key_,omitempty"` + Name string `json:"name,omitempty"` + ValueType int `json:"value_type,omitempty,string"` + HostID string `json:"hostid,omitempty"` + Hosts []ItemHost `json:"hosts,omitempty"` + Status string `json:"status,omitempty"` + State string `json:"state,omitempty"` +} +type ItemHost struct { + ID string `json:"hostid,omitempty"` + Name string `json:"name,omitempty"` +} + +type Trend []TrendPoint +type TrendPoint struct { + ItemID string `json:"itemid,omitempty"` + Clock int64 `json:"clock,omitempty,string"` + Num string `json:"num,omitempty"` + ValueMin string `json:"value_min,omitempty"` + ValueAvg string `json:"value_avg,omitempty"` + ValueMax string `json:"value_max,omitempty"` +} + +type History []HistoryPoint +type HistoryPoint struct { + ItemID string `json:"itemid,omitempty"` + Clock int64 `json:"clock,omitempty,string"` + Value float64 `json:"value,omitempty,string"` + NS int64 `json:"ns,omitempty,string"` +} diff --git a/pkg/zabbix_api.go b/pkg/zabbix_api.go index c3775dc..2facf3e 100644 --- a/pkg/zabbix_api.go +++ b/pkg/zabbix_api.go @@ -8,11 +8,14 @@ import ( "fmt" "io" "io/ioutil" + "math" "net" "net/http" "net/url" + "regexp" "time" + "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" simplejson "github.com/bitly/go-simplejson" "github.com/grafana/grafana_plugin_model/go/datasource" hclog "github.com/hashicorp/go-hclog" @@ -29,6 +32,16 @@ type ZabbixDatasource struct { authToken string } +type categories struct { + Transform []map[string]interface{} + Aggregate []map[string]interface{} + Filter []map[string]interface{} + Trends []map[string]interface{} + Time []map[string]interface{} + Alias []map[string]interface{} + Special []map[string]interface{} +} + // NewZabbixDatasource returns an initialized ZabbixDatasource func NewZabbixDatasource() *ZabbixDatasource { return &ZabbixDatasource{ @@ -60,41 +73,31 @@ func (ds *ZabbixDatasource) ZabbixAPIQuery(ctx context.Context, tsdbReq *datasou if !queryExistInCache { dsInfo := tsdbReq.GetDatasource() - jsonQueries := make([]*simplejson.Json, 0) + queries := []requestModel{} for _, query := range tsdbReq.Queries { - json, err := simplejson.NewJson([]byte(query.ModelJson)) - apiMethod := json.GetPath("target", "method").MustString() - apiParams := json.GetPath("target", "params").MustMap() + req := requestModel{} + err := json.Unmarshal([]byte(query.GetModelJson()), &req) if err != nil { return nil, err } - - ds.logger.Debug("ZabbixAPIQuery", "method", apiMethod, "params", apiParams) - - jsonQueries = append(jsonQueries, json) + queries = append(queries, req) } - if len(jsonQueries) == 0 { + if len(queries) == 0 { return nil, errors.New("At least one query should be provided") } - jsonQuery := jsonQueries[0].Get("target") - apiMethod := jsonQuery.Get("method").MustString() - apiParams := jsonQuery.Get("params").MustMap() - - response, err := ds.ZabbixRequest(ctx, dsInfo, apiMethod, apiParams) - ds.queryCache.Set(HashString(tsdbReq.String()), response) - result = response + query := queries[0].Target + var err error + result, err = ds.ZabbixRequest(ctx, dsInfo, query.Method, query.Params) + ds.queryCache.Set(HashString(tsdbReq.String()), result) if err != nil { ds.logger.Debug("ZabbixAPIQuery", "error", err) return nil, errors.New("ZabbixAPIQuery is not implemented yet") } } - resultByte, _ := result.(*simplejson.Json).MarshalJSON() - ds.logger.Debug("ZabbixAPIQuery", "result", string(resultByte)) - return BuildResponse(result) } @@ -104,14 +107,14 @@ func (ds *ZabbixDatasource) TestConnection(ctx context.Context, tsdbReq *datasou auth, err := ds.loginWithDs(ctx, dsInfo) if err != nil { - return BuildErrorResponse(fmt.Errorf("Authentication failed: %w", err)), nil + return BuildErrorResponse(fmt.Errorf("Authentication failed: %s", err)), nil } ds.authToken = auth - response, err := ds.zabbixAPIRequest(ctx, dsInfo.GetUrl(), "apiinfo.version", map[string]interface{}{}, "") + response, err := ds.zabbixAPIRequest(ctx, dsInfo.GetUrl(), "apiinfo.version", zabbixParams{}, "") if err != nil { ds.logger.Debug("TestConnection", "error", err) - return BuildErrorResponse(fmt.Errorf("Version check failed: %w", err)), nil + return BuildErrorResponse(fmt.Errorf("Version check failed: %s", err)), nil } resultByte, _ := response.MarshalJSON() @@ -125,7 +128,7 @@ func (ds *ZabbixDatasource) TestConnection(ctx context.Context, tsdbReq *datasou } // ZabbixRequest checks authentication and makes a request to the Zabbix API -func (ds *ZabbixDatasource) ZabbixRequest(ctx context.Context, dsInfo *datasource.DatasourceInfo, method string, params map[string]interface{}) (*simplejson.Json, error) { +func (ds *ZabbixDatasource) ZabbixRequest(ctx context.Context, dsInfo *datasource.DatasourceInfo, method string, params zabbixParams) (*simplejson.Json, error) { zabbixURL := dsInfo.GetUrl() var result *simplejson.Json @@ -181,9 +184,9 @@ func (ds *ZabbixDatasource) loginWithDs(ctx context.Context, dsInfo *datasource. } func (ds *ZabbixDatasource) login(ctx context.Context, apiURL string, username string, password string) (string, error) { - params := map[string]interface{}{ - "user": username, - "password": password, + params := zabbixParams{ + User: username, + Password: password, } auth, err := ds.zabbixAPIRequest(ctx, apiURL, "user.login", params, "") if err != nil { @@ -193,7 +196,7 @@ func (ds *ZabbixDatasource) login(ctx context.Context, apiURL string, username s return auth.MustString(), nil } -func (ds *ZabbixDatasource) zabbixAPIRequest(ctx context.Context, apiURL string, method string, params map[string]interface{}, auth string) (*simplejson.Json, error) { +func (ds *ZabbixDatasource) zabbixAPIRequest(ctx context.Context, apiURL string, method string, params zabbixParams, auth string) (*simplejson.Json, error) { zabbixURL, err := url.Parse(apiURL) // TODO: inject auth token (obtain from 'user.login' first) @@ -229,12 +232,14 @@ func (ds *ZabbixDatasource) zabbixAPIRequest(ctx context.Context, apiURL string, Body: rc, } + tStart := time.Now() response, err := makeHTTPRequest(ctx, ds.httpClient, req) if err != nil { return nil, err } - ds.logger.Debug("zabbixAPIRequest", "response", string(response)) + requestTime := time.Now().Sub(tStart) + ds.logger.Debug("Response from Zabbix Request", "method", method, "requestTime", requestTime) return handleAPIResult(response) } @@ -275,3 +280,412 @@ func isNotAuthorized(message string) bool { message == "Not authorised." || message == "Not authorized." } + +func (ds *ZabbixDatasource) queryNumericItems(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) { + tStart := time.Now() + jsonQueries := make([]*simplejson.Json, 0) + for _, query := range tsdbReq.Queries { + json, err := simplejson.NewJson([]byte(query.ModelJson)) + if err != nil { + return nil, err + } + + jsonQueries = append(jsonQueries, json) + } + + if len(jsonQueries) == 0 { + return nil, errors.New("At least one query should be provided") + } + + firstQuery := jsonQueries[0] + + groupFilter := firstQuery.GetPath("group", "filter").MustString() + hostFilter := firstQuery.GetPath("host", "filter").MustString() + appFilter := firstQuery.GetPath("application", "filter").MustString() + itemFilter := firstQuery.GetPath("item", "filter").MustString() + + ds.logger.Debug("queryNumericItems", + "func", "ds.getItems", + "groupFilter", groupFilter, + "hostFilter", hostFilter, + "appFilter", appFilter, + "itemFilter", itemFilter) + items, err := ds.getItems(ctx, tsdbReq.GetDatasource(), groupFilter, hostFilter, appFilter, itemFilter, "num") + if err != nil { + return nil, err + } + ds.logger.Debug("queryNumericItems", "finished", "ds.getItems", "timeElapsed", time.Now().Sub(tStart)) + + metrics, err := ds.queryNumericDataForItems(ctx, tsdbReq, items, jsonQueries, isUseTrend(tsdbReq.GetTimeRange())) + if err != nil { + return nil, err + } + ds.logger.Debug("queryNumericItems", "finished", "queryNumericDataForItems", "timeElapsed", time.Now().Sub(tStart)) + + return BuildMetricsResponse(metrics) +} + +func (ds *ZabbixDatasource) getItems(ctx context.Context, dsInfo *datasource.DatasourceInfo, groupFilter string, hostFilter string, appFilter string, itemFilter string, itemType string) (zabbix.Items, error) { + tStart := time.Now() + + hosts, err := ds.getHosts(ctx, dsInfo, groupFilter, hostFilter) + if err != nil { + return nil, err + } + var hostids []string + for _, k := range hosts { + hostids = append(hostids, k["hostid"].(string)) + } + ds.logger.Debug("getItems", "finished", "getHosts", "timeElapsed", time.Now().Sub(tStart)) + + apps, err := ds.getApps(ctx, dsInfo, groupFilter, hostFilter, appFilter) + if err != nil { + return nil, err + } + var appids []string + for _, l := range apps { + appids = append(appids, l["applicationid"].(string)) + } + ds.logger.Debug("getItems", "finished", "getApps", "timeElapsed", time.Now().Sub(tStart)) + + var allItems *simplejson.Json + if len(hostids) > 0 { + ds.logger.Debug("getAllItems", "with", "hostFilter") + allItems, err = ds.getAllItems(ctx, dsInfo, hostids, nil, itemType) + } else if len(appids) > 0 { + ds.logger.Debug("getAllItems", "with", "appFilter") + allItems, err = ds.getAllItems(ctx, dsInfo, nil, appids, itemType) + } + ds.logger.Debug("getItems", "finished", "getAllItems", "timeElapsed", time.Now().Sub(tStart)) + + var items zabbix.Items + + if allItems == nil { + items = zabbix.Items{} + } else { + itemsJSON, err := allItems.MarshalJSON() + if err != nil { + return nil, err + } + + err = json.Unmarshal(itemsJSON, &items) + if err != nil { + return nil, err + } + } + + re, err := parseFilter(itemFilter) + if err != nil { + return nil, err + } + + filteredItems := zabbix.Items{} + for _, item := range items { + if item.Status == "0" { + if re != nil { + if re.MatchString(item.Name) { + filteredItems = append(filteredItems, item) + } + } else if item.Name == itemFilter { + filteredItems = append(filteredItems, item) + } + } + } + ds.logger.Debug("getItems", "found", len(items), "matches", len(filteredItems)) + ds.logger.Debug("getItems", "totalTimeTaken", time.Now().Sub(tStart)) + return filteredItems, nil +} + +func (ds *ZabbixDatasource) getApps(ctx context.Context, dsInfo *datasource.DatasourceInfo, groupFilter string, hostFilter string, appFilter string) ([]map[string]interface{}, error) { + hosts, err := ds.getHosts(ctx, dsInfo, groupFilter, hostFilter) + if err != nil { + return nil, err + } + var hostids []string + for _, k := range hosts { + hostids = append(hostids, k["hostid"].(string)) + } + allApps, err := ds.getAllApps(ctx, dsInfo, hostids) + if err != nil { + return nil, err + } + + re, err := parseFilter(appFilter) + if err != nil { + return nil, err + } + + var apps []map[string]interface{} + for _, i := range allApps.MustArray() { + name := i.(map[string]interface{})["name"].(string) + if re != nil { + if re.MatchString(name) { + apps = append(apps, i.(map[string]interface{})) + } + } else if name == appFilter { + apps = append(apps, i.(map[string]interface{})) + } + } + ds.logger.Debug("getapps", "found", len(allApps.MustArray()), "matches", len(apps)) + return apps, nil +} + +func (ds *ZabbixDatasource) getHosts(ctx context.Context, dsInfo *datasource.DatasourceInfo, groupFilter string, hostFilter string) ([]map[string]interface{}, error) { + groups, err := ds.getGroups(ctx, dsInfo, groupFilter) + if err != nil { + return nil, err + } + var groupids []string + for _, k := range groups { + groupids = append(groupids, k["groupid"].(string)) + } + allHosts, err := ds.getAllHosts(ctx, dsInfo, groupids) + if err != nil { + return nil, err + } + + re, err := parseFilter(hostFilter) + if err != nil { + return nil, err + } + + var hosts []map[string]interface{} + for _, i := range allHosts.MustArray() { + name := i.(map[string]interface{})["name"].(string) + if re != nil { + if re.MatchString(name) { + hosts = append(hosts, i.(map[string]interface{})) + } + } else if name == hostFilter { + hosts = append(hosts, i.(map[string]interface{})) + } + + } + ds.logger.Debug("getHosts", "found", len(allHosts.MustArray()), "matches", len(hosts)) + return hosts, nil +} + +func (ds *ZabbixDatasource) getGroups(ctx context.Context, dsInfo *datasource.DatasourceInfo, groupFilter string) ([]map[string]interface{}, error) { + allGroups, err := ds.getAllGroups(ctx, dsInfo) + if err != nil { + return nil, err + } + re, err := parseFilter(groupFilter) + if err != nil { + return nil, err + } + + var groups []map[string]interface{} + for _, i := range allGroups.MustArray() { + name := i.(map[string]interface{})["name"].(string) + if re != nil { + if re.MatchString(name) { + groups = append(groups, i.(map[string]interface{})) + } + } else if name == groupFilter { + groups = append(groups, i.(map[string]interface{})) + } + } + return groups, nil +} + +func (ds *ZabbixDatasource) getAllItems(ctx context.Context, dsInfo *datasource.DatasourceInfo, hostids []string, appids []string, itemtype string) (*simplejson.Json, error) { + params := zabbixParams{ + Output: &zabbixParamOutput{Fields: []string{"itemid", "name", "key_", "value_type", "hostid", "status", "state"}}, + SortField: "name", + WebItems: true, + Filter: map[string][]int{}, + SelectHosts: []string{"hostid", "name"}, + HostIDs: hostids, + AppIDs: appids, + } + + if itemtype == "num" { + params.Filter["value_type"] = []int{0, 3} + } else if itemtype == "text" { + params.Filter["value_type"] = []int{1, 2, 4} + } + + return ds.ZabbixRequest(ctx, dsInfo, "item.get", params) +} + +func (ds *ZabbixDatasource) getAllApps(ctx context.Context, dsInfo *datasource.DatasourceInfo, hostids []string) (*simplejson.Json, error) { + params := zabbixParams{Output: &zabbixParamOutput{Mode: "extend"}, HostIDs: hostids} + + return ds.ZabbixRequest(ctx, dsInfo, "application.get", params) +} + +func (ds *ZabbixDatasource) getAllHosts(ctx context.Context, dsInfo *datasource.DatasourceInfo, groupids []string) (*simplejson.Json, error) { + params := zabbixParams{Output: &zabbixParamOutput{Fields: []string{"name", "host"}}, SortField: "name", GroupIDs: groupids} + + return ds.ZabbixRequest(ctx, dsInfo, "host.get", params) +} + +func (ds *ZabbixDatasource) getAllGroups(ctx context.Context, dsInfo *datasource.DatasourceInfo) (*simplejson.Json, error) { + params := zabbixParams{Output: &zabbixParamOutput{Fields: []string{"name"}}, SortField: "name", RealHosts: true} + + return ds.ZabbixRequest(ctx, dsInfo, "hostgroup.get", params) +} + +func (ds *ZabbixDatasource) queryNumericDataForItems(ctx context.Context, tsdbReq *datasource.DatasourceRequest, items zabbix.Items, jsonQueries []*simplejson.Json, useTrend bool) ([]*datasource.TimeSeries, error) { + valueType := ds.getTrendValueType(jsonQueries) + consolidateBy := ds.getConsolidateBy(jsonQueries) + + if consolidateBy == "" { + consolidateBy = valueType + } + + history, err := ds.getHistotyOrTrend(ctx, tsdbReq, items, useTrend) + if err != nil { + return nil, err + } + + return convertHistory(history, items) +} + +func (ds *ZabbixDatasource) getTrendValueType(jsonQueries []*simplejson.Json) string { + var trendFunctions []string + var trendValueFunc string + + // TODO: loop over populated categories + for _, j := range new(categories).Trends { + trendFunctions = append(trendFunctions, j["name"].(string)) + } + for _, k := range jsonQueries[0].Get("functions").MustArray() { + for _, j := range trendFunctions { + if j == k.(map[string]interface{})["def"].(map[string]interface{})["name"] { + trendValueFunc = j + } + } + } + + if trendValueFunc == "" { + trendValueFunc = "avg" + } + + return trendValueFunc +} + +func (ds *ZabbixDatasource) getConsolidateBy(jsonQueries []*simplejson.Json) string { + var consolidateBy string + + for _, k := range jsonQueries[0].Get("functions").MustArray() { + if k.(map[string]interface{})["def"].(map[string]interface{})["name"] == "consolidateBy" { + defParams := k.(map[string]interface{})["def"].(map[string]interface{})["params"].([]interface{}) + if len(defParams) > 0 { + consolidateBy = defParams[0].(string) + } + } + } + return consolidateBy +} + +func (ds *ZabbixDatasource) getHistotyOrTrend(ctx context.Context, tsdbReq *datasource.DatasourceRequest, items zabbix.Items, useTrend bool) (zabbix.History, error) { + allHistory := zabbix.History{} + + timeRange := tsdbReq.GetTimeRange() + groupedItems := map[int]zabbix.Items{} + + for _, j := range items { + groupedItems[j.ValueType] = append(groupedItems[j.ValueType], j) + } + + for k, l := range groupedItems { + var itemids []string + for _, m := range l { + itemids = append(itemids, m.ID) + } + + params := zabbixParams{ + Output: &zabbixParamOutput{Mode: "extend"}, + SortField: "clock", + SortOrder: "ASC", + ItemIDs: itemids, + TimeFrom: timeRange.GetFromEpochMs() / 1000, + TimeTill: timeRange.GetToEpochMs() / 1000, + } + + var response *simplejson.Json + var err error + if useTrend { + response, err = ds.ZabbixRequest(ctx, tsdbReq.GetDatasource(), "trend.get", params) + } else { + params.History = &k + response, err = ds.ZabbixRequest(ctx, tsdbReq.GetDatasource(), "history.get", params) + } + + if err != nil { + return nil, err + } + + pointJSON, err := response.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("Internal error parsing response JSON: %w", err) + } + + history := zabbix.History{} + err = json.Unmarshal(pointJSON, &history) + if err != nil { + ds.logger.Warn(fmt.Sprintf("Could not map Zabbix response to History: %s", err.Error())) + } else { + allHistory = append(allHistory, history...) + } + } + return allHistory, nil +} + +func isUseTrend(timeRange *datasource.TimeRange) bool { + fromSec := timeRange.GetFromEpochMs() / 1000 + toSec := timeRange.GetToEpochMs() / 1000 + if (fromSec < time.Now().Add(time.Hour*-7*24).Unix()) || + (toSec-fromSec > (4 * 24 * time.Hour).Milliseconds()) { + return true + } + return false +} + +func convertHistory(history zabbix.History, items zabbix.Items) ([]*datasource.TimeSeries, error) { + seriesMap := map[string]*datasource.TimeSeries{} + + for _, item := range items { + seriesMap[item.ID] = &datasource.TimeSeries{ + Name: fmt.Sprintf("%s %s", item.Hosts[0].Name, item.Name), + Points: []*datasource.Point{}, + } + } + + for _, point := range history { + seriesMap[point.ItemID].Points = append(seriesMap[point.ItemID].Points, &datasource.Point{ + Timestamp: point.Clock*1000 + int64(math.Round(float64(point.NS)/1000000)), + Value: point.Value, + }) + } + + seriesList := []*datasource.TimeSeries{} + for _, series := range seriesMap { + seriesList = append(seriesList, series) + } + return seriesList, nil +} + +func parseFilter(filter string) (*regexp.Regexp, error) { + regex := regexp.MustCompile(`^/(.+)/(.*)$`) + flagRE := regexp.MustCompile("[imsU]+") + + matches := regex.FindStringSubmatch(filter) + if len(matches) <= 1 { + return nil, nil + } + + pattern := "" + if matches[2] != "" { + if flagRE.MatchString(matches[2]) { + pattern += "(?" + matches[2] + ")" + } else { + return nil, fmt.Errorf("error parsing regexp: unsupported flags `%s` (expected [imsU])", matches[2]) + } + } + pattern += matches[1] + + return regexp.Compile(pattern) +} diff --git a/pkg/zabbix_api_test.go b/pkg/zabbix_api_test.go index 06ab7d0..0eaee01 100644 --- a/pkg/zabbix_api_test.go +++ b/pkg/zabbix_api_test.go @@ -1 +1,451 @@ package main + +import ( + "bytes" + "io/ioutil" + "net/http" + "regexp" + "testing" + "time" + + simplejson "github.com/bitly/go-simplejson" + "github.com/grafana/grafana_plugin_model/go/datasource" + hclog "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" +) + +type RoundTripFunc func(req *http.Request) *http.Response + +func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +//NewTestClient returns *http.Client with Transport replaced to avoid making real calls +func NewTestClient(fn RoundTripFunc) *http.Client { + return &http.Client{ + Transport: RoundTripFunc(fn), + } +} + +var basicDatasourceInfo = &datasource.DatasourceInfo{ + Id: 1, + Name: "TestDatasource", + Url: "sameUrl", + JsonData: `{"username":"username", "password":"password"}}`, +} + +func mockDataSourceRequest(modelJSON string) *datasource.DatasourceRequest { + return &datasource.DatasourceRequest{ + Datasource: basicDatasourceInfo, + Queries: []*datasource.Query{ + &datasource.Query{ + ModelJson: modelJSON, + }, + }, + } +} + +func mockZabbixDataSource(body string, statusCode int) ZabbixDatasource { + return ZabbixDatasource{ + queryCache: NewCache(10*time.Minute, 10*time.Minute), + httpClient: NewTestClient(func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Body: ioutil.NopCloser(bytes.NewBufferString(body)), + Header: make(http.Header), + } + }), + authToken: "sampleAuthToken", + logger: hclog.Default(), + } +} + +func TestZabbixAPIQuery(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + resp, err := zabbixDatasource.ZabbixAPIQuery(context.Background(), mockDataSourceRequest(`{"target":{"method":"Method","params":{"param1" : "Param1"}}}`)) + + assert.Equal(t, "\"sampleResult\"", resp.GetResults()[0].GetMetaJson()) + assert.Equal(t, "zabbixAPI", resp.GetResults()[0].GetRefId()) + assert.Nil(t, err) +} + +func TestZabbixAPIQueryEmptyQuery(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + resp, err := zabbixDatasource.ZabbixAPIQuery(context.Background(), mockDataSourceRequest(``)) + + assert.Nil(t, resp) + assert.NotNil(t, err) +} + +func TestZabbixAPIQueryNoQueries(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + basicDatasourceRequest := &datasource.DatasourceRequest{ + Datasource: &datasource.DatasourceInfo{ + Id: 1, + Name: "TestDatasource", + }, + } + resp, err := zabbixDatasource.ZabbixAPIQuery(context.Background(), basicDatasourceRequest) + + assert.Nil(t, resp) + assert.Equal(t, "At least one query should be provided", err.Error()) +} + +func TestZabbixAPIQueryError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) + resp, err := zabbixDatasource.ZabbixAPIQuery(context.Background(), mockDataSourceRequest(`{"target":{"method":"Method","params":{"param1" : "Param1"}}}`)) + + assert.Nil(t, resp) + assert.Equal(t, "ZabbixAPIQuery is not implemented yet", err.Error()) +} + +func TestLogin(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + resp, err := zabbixDatasource.login(context.Background(), "apiURL", "username", "password") + + assert.Equal(t, "sampleResult", resp) + assert.Nil(t, err) +} + +func TestLoginError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) + resp, err := zabbixDatasource.login(context.Background(), "apiURL", "username", "password") + + assert.Equal(t, "", resp) + assert.NotNil(t, err) +} + +func TestLoginWithDs(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + resp, err := zabbixDatasource.loginWithDs(context.Background(), basicDatasourceInfo) + + assert.Equal(t, "sampleResult", resp) + assert.Nil(t, err) +} + +func TestLoginWithDsError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) + resp, err := zabbixDatasource.loginWithDs(context.Background(), basicDatasourceInfo) + + assert.Equal(t, "", resp) + assert.NotNil(t, err) +} + +func TestZabbixRequest(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + resp, err := zabbixDatasource.ZabbixRequest(context.Background(), basicDatasourceInfo, "method", zabbixParams{}) + assert.Equal(t, "sampleResult", resp.MustString()) + assert.Nil(t, err) +} + +func TestZabbixRequestWithNoAuthToken(t *testing.T) { + var mockDataSource = ZabbixDatasource{ + queryCache: NewCache(10*time.Minute, 10*time.Minute), + httpClient: NewTestClient(func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString(`{"result":"auth"}`)), + Header: make(http.Header), + } + }), + logger: hclog.Default(), + } + + resp, err := mockDataSource.ZabbixRequest(context.Background(), basicDatasourceInfo, "method", zabbixParams{}) + assert.Equal(t, "auth", resp.MustString()) + assert.Nil(t, err) +} + +func TestZabbixRequestError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) + resp, err := zabbixDatasource.ZabbixRequest(context.Background(), basicDatasourceInfo, "method", zabbixParams{}) + assert.Nil(t, resp) + assert.NotNil(t, err) +} + +func TestZabbixAPIRequest(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + resp, err := zabbixDatasource.zabbixAPIRequest(context.Background(), "apiURL", "item.get", zabbixParams{}, "auth") + + assert.Equal(t, "sampleResult", resp.MustString()) + assert.Nil(t, err) +} + +func TestZabbixAPIRequestError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) + resp, err := zabbixDatasource.zabbixAPIRequest(context.Background(), "apiURL", "item.get", zabbixParams{}, "auth") + + assert.Nil(t, resp) + assert.NotNil(t, err) +} + +func TestTestConnection(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + resp, err := zabbixDatasource.TestConnection(context.Background(), mockDataSourceRequest(``)) + + assert.Equal(t, "{\"zabbixVersion\":\"sampleResult\",\"dbConnectorStatus\":null}", resp.Results[0].GetMetaJson()) + assert.Nil(t, err) +} + +func TestTestConnectionError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) + resp, err := zabbixDatasource.TestConnection(context.Background(), mockDataSourceRequest(``)) + + assert.Equal(t, "", resp.Results[0].GetMetaJson()) + assert.NotNil(t, resp.Results[0].GetError()) + assert.Nil(t, err) +} + +func TestIsNotAuthorized(t *testing.T) { + testPositive := isNotAuthorized("Not authorised.") + assert.True(t, testPositive) + + testNegative := isNotAuthorized("testNegative") + assert.False(t, testNegative) +} + +func TestHandleAPIResult(t *testing.T) { + expectedResponse, err := handleAPIResult([]byte(`{"result":"sampleResult"}`)) + + assert.Equal(t, "sampleResult", expectedResponse.MustString()) + assert.Nil(t, err) +} + +func TestHandleAPIResultFormatError(t *testing.T) { + expectedResponse, err := handleAPIResult([]byte(`{"result"::"sampleResult"}`)) + + assert.NotNil(t, err) + assert.Nil(t, expectedResponse) +} + +func TestHandleAPIResultError(t *testing.T) { + expectedResponse, err := handleAPIResult([]byte(`{"result":"sampleResult", "error":{"message":"Message", "data":"Data"}}`)) + + assert.Equal(t, "Message Data", err.Error()) + assert.Nil(t, expectedResponse) +} + +func TestGetAllGroups(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"groupid": "46489126", "name": "name1"},{"groupid": "46489127", "name":"name2"}]}`, 200) + resp, err := zabbixDatasource.getAllGroups(context.Background(), basicDatasourceInfo) + + assert.Equal(t, "46489126", resp.MustArray()[0].(map[string]interface{})["groupid"]) + assert.Equal(t, "46489127", resp.MustArray()[1].(map[string]interface{})["groupid"]) + assert.Nil(t, err) +} + +func TestGetAllHosts(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"hostid": "46489126", "name": "hostname1"},{"hostid": "46489127", "name":"hostname2"}]}`, 200) + resp, err := zabbixDatasource.getAllHosts(context.Background(), basicDatasourceInfo, []string{"46489127", "46489127"}) + + assert.Equal(t, "46489126", resp.MustArray()[0].(map[string]interface{})["hostid"]) + assert.Equal(t, "46489127", resp.MustArray()[1].(map[string]interface{})["hostid"]) + assert.Nil(t, err) +} + +func TestGetAllApps(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"applicationid": "46489126", "name": "hostname1"},{"applicationid": "46489127", "name":"hostname2"}]}`, 200) + resp, err := zabbixDatasource.getAllApps(context.Background(), basicDatasourceInfo, []string{"46489127", "46489127"}) + + assert.Equal(t, "46489126", resp.MustArray()[0].(map[string]interface{})["applicationid"]) + assert.Equal(t, "46489127", resp.MustArray()[1].(map[string]interface{})["applicationid"]) + assert.Nil(t, err) +} + +func TestGetAllItems(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"itemid": "46489126", "name": "hostname1"},{"itemid": "46489127", "name":"hostname2"}]}`, 200) + resp, err := zabbixDatasource.getAllItems(context.Background(), basicDatasourceInfo, []string{"46489127", "46489127"}, []string{"7947934", "9182763"}, "num") + + assert.Equal(t, "46489126", resp.MustArray()[0].(map[string]interface{})["itemid"]) + assert.Equal(t, "46489127", resp.MustArray()[1].(map[string]interface{})["itemid"]) + assert.Nil(t, err) +} + +func TestGetGroups(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"groupid": "46489126", "name": "name1"},{"groupid": "46489127", "name":"name2"}]}`, 200) + resp, err := zabbixDatasource.getGroups(context.Background(), basicDatasourceInfo, "name1") + + assert.Equal(t, "46489126", resp[0]["groupid"]) + assert.Equal(t, "name1", resp[0]["name"]) + assert.Nil(t, err) +} + +func TestGetGroupsError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"groupid": "46489126", "name": "name1"},{"groupid": "46489127", "name":"name2"}]}`, 500) + resp, err := zabbixDatasource.getGroups(context.Background(), basicDatasourceInfo, "name1") + + assert.Nil(t, resp) + assert.NotNil(t, err) +} + +func TestGetHosts(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"groupid": "46489126", "hostid": "7468763", "name": "hostname1"},{"groupid": "46489127","hostid": "846586", "name":"hostname2"}]}`, 200) + resp, err := zabbixDatasource.getHosts(context.Background(), basicDatasourceInfo, "nam", "hostname1") + + assert.Equal(t, "7468763", resp[0]["hostid"]) + assert.Equal(t, "hostname1", resp[0]["name"]) + assert.Nil(t, err) +} + +func TestGetHostsError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"groupid": "46489126", "hostid": "7468763", "name": "hostname1"},{"groupid": "46489127","hostid": "846586", "name":"hostname2"}]}`, 500) + resp, err := zabbixDatasource.getHosts(context.Background(), basicDatasourceInfo, "nam", "hostna") + + assert.Nil(t, resp) + assert.NotNil(t, err) +} + +func TestGetApps(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"groupid": "46489126", "hostid": "7468763", "applicationid": "7947934", "name": "appname1"}, + {"groupid": "46489127","hostid": "846586", "applicationid": "9182763", "name": "appname2"}]}`, 200) + resp, err := zabbixDatasource.getApps(context.Background(), basicDatasourceInfo, "nam", "hostnam", "appname1") + + assert.Equal(t, "7947934", resp[0]["applicationid"]) + assert.Equal(t, "appname1", resp[0]["name"]) + assert.Nil(t, err) +} + +func TestGetAppsError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"groupid": "46489126", "hostid": "7468763", "applicationid": "7947934", "name": "appname1"}, + {"groupid": "46489127","hostid": "846586", "applicationid": "9182763", "name": "appname2"}]}`, 500) + resp, err := zabbixDatasource.getApps(context.Background(), basicDatasourceInfo, "nam", "hostnam", "appname1") + + assert.Nil(t, resp) + assert.NotNil(t, err) +} + +func TestGetItems(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"groupid": "46489126", "hostid": "7468763", "applicationid": "7947934", "itemid": "837465", "name": "itemname1", "status": "0"}, + {"groupid": "46489127","hostid": "846586", "applicationid": "9182763", "itemid" : "0288374", "name": "itemname2", "status": "0"}]}`, 200) + resp, err := zabbixDatasource.getItems(context.Background(), basicDatasourceInfo, "itemname1", "itemname1", "itemname1", "itemname1", "num") + + assert.Equal(t, "837465", resp[0].ID) + assert.Equal(t, "itemname1", resp[0].Name) + assert.Nil(t, err) +} + +func TestGetItemsError(t *testing.T) { + zabbixDatasource := mockZabbixDataSource(`{"result":[{"groupid": "46489126", "hostid": "7468763", "applicationid": "7947934", "itemid": "837465", "name": "itemname1", "status": "0"}, + {"groupid": "46489127","hostid": "846586", "applicationid": "9182763", "itemid" : "0288374", "name": "itemname2", "status": "0"}]}`, 500) + resp, err := zabbixDatasource.getItems(context.Background(), basicDatasourceInfo, "name", "name", "name", "name", "num") + + assert.Nil(t, resp) + assert.NotNil(t, err) +} + +func TestGetTrendValueType(t *testing.T) { + json1, _ := simplejson.NewJson([]byte(`{"functions":[{"def":{"name":"name1"}},{"def":{"name":"name2"}}]}`)) + json2, _ := simplejson.NewJson([]byte(`{"functions":[{"def":{"name":"name1"}},{"def":{"name":"name2"}}]}`)) + jsonQueries := []*simplejson.Json{json1, json2} + + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + resp := zabbixDatasource.getTrendValueType(jsonQueries) + + assert.Equal(t, "avg", resp) +} + +func TestGetConsolidateBy(t *testing.T) { + json1, _ := simplejson.NewJson([]byte(`{"functions":[{"def":{"name":"consolidateBy", "params":["sum"]}},{"def":{"name":"name2"}}]}`)) + json2, _ := simplejson.NewJson([]byte(`{"functions":[{"def":{"name":"name1"}},{"def":{"name":"name2"}}]}`)) + jsonQueries := []*simplejson.Json{json1, json2} + + zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) + resp := zabbixDatasource.getConsolidateBy(jsonQueries) + + assert.Equal(t, "sum", resp) + +} + +func Test_isUseTrend(t *testing.T) { + tests := []struct { + name string + timeRange *datasource.TimeRange + want bool + }{ + { + name: "History time", + timeRange: &datasource.TimeRange{ + FromEpochMs: time.Now().Add(-time.Hour*48).Unix() * 1000, + ToEpochMs: time.Now().Add(-time.Hour*12).Unix() * 1000, + }, + want: false, + }, + { + name: "Trend time (past 7 days)", + timeRange: &datasource.TimeRange{ + FromEpochMs: time.Now().Add(-time.Hour*24*14).Unix() * 1000, + ToEpochMs: time.Now().Add(-time.Hour*24*13).Unix() * 1000, + }, + want: true, + }, + { + name: "Trend time (longer than 4 days)", + timeRange: &datasource.TimeRange{ + FromEpochMs: time.Now().Add(-time.Hour*24*8).Unix() * 1000, + ToEpochMs: time.Now().Add(-time.Hour*24*1).Unix() * 1000, + }, + want: true, + }, + } + for _, tt := range tests { + got := isUseTrend(tt.timeRange) + assert.Equal(t, tt.want, got, tt.name, tt.timeRange) + } + +} + +func Test_parseFilter(t *testing.T) { + tests := []struct { + name string + filter string + want *regexp.Regexp + wantErr string + }{ + { + name: "Non-regex filter", + filter: "foobar", + want: nil, + }, + { + name: "Non-regex filter (would-be invalid regex)", + filter: "fooba(r", + want: nil, + }, + { + name: "Regex filter", + filter: "/^foo.+/", + want: regexp.MustCompile("^foo.+"), + }, + { + name: "Regex filter with flags", + filter: "/^foo.+/s", + want: regexp.MustCompile("(?s)^foo.+"), + }, + { + name: "Invalid regex", + filter: "/fooba(r/", + wantErr: "error parsing regexp: missing closing ): `fooba(r`", + }, + { + name: "Unsupported flag", + filter: "/foo.+/z", + wantErr: "error parsing regexp: unsupported flags `z` (expected [imsU])", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseFilter(tt.filter) + + if tt.wantErr != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErr) + assert.Nil(t, got) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/src/datasource-zabbix/plugin.json b/src/datasource-zabbix/plugin.json index 4f8618b..f0cd0d2 100644 --- a/src/datasource-zabbix/plugin.json +++ b/src/datasource-zabbix/plugin.json @@ -7,7 +7,7 @@ "annotations": true, "backend": true, - "alerting": false, + "alerting": true, "executable": "../zabbix-plugin", "includes": [