From 26b98f46b970f8a13aa6501cd506e6b245f4f4f8 Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Thu, 13 Nov 2025 11:28:20 -0800 Subject: [PATCH 01/10] initial basic implementation of the dbconnector in the backend --- pkg/dbconnector/dbconnector.go | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 pkg/dbconnector/dbconnector.go diff --git a/pkg/dbconnector/dbconnector.go b/pkg/dbconnector/dbconnector.go new file mode 100644 index 000000000..9111d9f45 --- /dev/null +++ b/pkg/dbconnector/dbconnector.go @@ -0,0 +1,45 @@ +package dbconnector + +type DBConnector struct { + datasourceId string + datasourceName string + datasourceUID string + // TODO: maybe we also need the type here +} +const DefaultQueryLimit = 10000 + +var HistoryToTableMap = map[string]string{ + "0": "history", + "1": "history_str", + "2": "history_log", + "3": "history_uint", + "4": "history_text", +} + +var TrendToTableMap = map[string]string{ + "0": "trends", + "3": "trends_uint", +} + +var ConsolidateByFunc = map[string]string{ + "avg": "AVG", + "min": "MIN", + "max": "MAX", + "sum": "SUM", + "count": "COUNT", +} + +var ConsolidateByTrendColumns = map[string]string{ + "avg": "value_avg", + "min": "value_min", + "max": "value_max", + "sum": "num*value_avg", // sum of sums inside the one-hour trend period +} + +func NewDBConnector(datasourceID, datasourceName, datasourceUID string) *DBConnector { + return &DBConnector { + datasourceId: datasourceID, + datasourceName: datasourceName, + datasourceUID: datasourceUID, + } +} From d7399185c7696fb191512ee91f48659ff4d51cc5 Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Thu, 13 Nov 2025 11:29:05 -0800 Subject: [PATCH 02/10] add datassource type and uid as part of the jsonDataOptions --- pkg/settings/models.go | 12 ++++++++---- src/datasource/components/ConfigEditor.tsx | 3 +++ src/datasource/types/config.ts | 2 ++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/settings/models.go b/pkg/settings/models.go index 60679611e..8491e7adb 100644 --- a/pkg/settings/models.go +++ b/pkg/settings/models.go @@ -16,8 +16,11 @@ type ZabbixDatasourceSettingsDTO struct { CacheTTL string `json:"cacheTTL"` Timeout interface{} `json:"timeout"` - DisableDataAlignment bool `json:"disableDataAlignment"` - DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"` + DisableDataAlignment bool `json:"disableDataAlignment"` + DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"` + EnableDirectDBConnection bool `json:"enableDirectDBConnection"` + DBConnectionDatasourceType string `json:"dbConnectionDatasourceType"` + DBConnectionDatasourceUID string `json:"dbConnectionDatasourceUid"` } // ZabbixDatasourceSettings model @@ -29,6 +32,7 @@ type ZabbixDatasourceSettings struct { CacheTTL time.Duration Timeout time.Duration - DisableDataAlignment bool `json:"disableDataAlignment"` - DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"` + DisableDataAlignment bool `json:"disableDataAlignment"` + DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"` + EnableDirectDBConnection bool `json:"enableDirectDBConnection"` } diff --git a/src/datasource/components/ConfigEditor.tsx b/src/datasource/components/ConfigEditor.tsx index 5daee2b47..9222d09b4 100644 --- a/src/datasource/components/ConfigEditor.tsx +++ b/src/datasource/components/ConfigEditor.tsx @@ -493,6 +493,8 @@ const directDBDatasourceChanegeHandler = jsonData: { ...options.jsonData, dbConnectionDatasourceId: value.value, + dbConnectionDatasourceType: selectedDs.type, + dbConnectionDatasourceUid: selectedDs.uid, }, }); }; @@ -505,6 +507,7 @@ const getDirectDBDatasources = () => { const getDirectDBDSOptions = () => { const dsList = getDirectDBDatasources(); + console.log(dsList); const dsOpts: Array> = dsList.map((ds) => ({ label: ds.name, value: ds.id, diff --git a/src/datasource/types/config.ts b/src/datasource/types/config.ts index 06d424079..540eaf703 100644 --- a/src/datasource/types/config.ts +++ b/src/datasource/types/config.ts @@ -17,6 +17,8 @@ export type ZabbixDSOptions = { dbConnectionEnable: boolean; dbConnectionDatasourceId?: number; dbConnectionDatasourceName?: string; + dbConnectionDatasourceType?: string; + dbConnectionDatasourceUid?: string; dbConnectionRetentionPolicy?: string; disableReadOnlyUsersAck: boolean; disableDataAlignment: boolean; From eea635daf9763c07bcbca5247cafea46503c0fae Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Fri, 14 Nov 2025 08:18:53 -0800 Subject: [PATCH 03/10] remove dbconnector implementation from the backedn --- pkg/dbconnector/dbconnector.go | 45 ---------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 pkg/dbconnector/dbconnector.go diff --git a/pkg/dbconnector/dbconnector.go b/pkg/dbconnector/dbconnector.go deleted file mode 100644 index 9111d9f45..000000000 --- a/pkg/dbconnector/dbconnector.go +++ /dev/null @@ -1,45 +0,0 @@ -package dbconnector - -type DBConnector struct { - datasourceId string - datasourceName string - datasourceUID string - // TODO: maybe we also need the type here -} -const DefaultQueryLimit = 10000 - -var HistoryToTableMap = map[string]string{ - "0": "history", - "1": "history_str", - "2": "history_log", - "3": "history_uint", - "4": "history_text", -} - -var TrendToTableMap = map[string]string{ - "0": "trends", - "3": "trends_uint", -} - -var ConsolidateByFunc = map[string]string{ - "avg": "AVG", - "min": "MIN", - "max": "MAX", - "sum": "SUM", - "count": "COUNT", -} - -var ConsolidateByTrendColumns = map[string]string{ - "avg": "value_avg", - "min": "value_min", - "max": "value_max", - "sum": "num*value_avg", // sum of sums inside the one-hour trend period -} - -func NewDBConnector(datasourceID, datasourceName, datasourceUID string) *DBConnector { - return &DBConnector { - datasourceId: datasourceID, - datasourceName: datasourceName, - datasourceUID: datasourceUID, - } -} From 7115f8ebdaa64e3fe3eb09ef0f8243efe51abc1e Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Fri, 14 Nov 2025 09:17:37 -0800 Subject: [PATCH 04/10] revert changes for passing db connection datasource settings to the backend --- pkg/settings/models.go | 4 ---- src/datasource/components/ConfigEditor.tsx | 3 --- src/datasource/types/config.ts | 2 -- 3 files changed, 9 deletions(-) diff --git a/pkg/settings/models.go b/pkg/settings/models.go index 8491e7adb..e230529a5 100644 --- a/pkg/settings/models.go +++ b/pkg/settings/models.go @@ -18,9 +18,6 @@ type ZabbixDatasourceSettingsDTO struct { DisableDataAlignment bool `json:"disableDataAlignment"` DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"` - EnableDirectDBConnection bool `json:"enableDirectDBConnection"` - DBConnectionDatasourceType string `json:"dbConnectionDatasourceType"` - DBConnectionDatasourceUID string `json:"dbConnectionDatasourceUid"` } // ZabbixDatasourceSettings model @@ -34,5 +31,4 @@ type ZabbixDatasourceSettings struct { DisableDataAlignment bool `json:"disableDataAlignment"` DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"` - EnableDirectDBConnection bool `json:"enableDirectDBConnection"` } diff --git a/src/datasource/components/ConfigEditor.tsx b/src/datasource/components/ConfigEditor.tsx index 9222d09b4..5daee2b47 100644 --- a/src/datasource/components/ConfigEditor.tsx +++ b/src/datasource/components/ConfigEditor.tsx @@ -493,8 +493,6 @@ const directDBDatasourceChanegeHandler = jsonData: { ...options.jsonData, dbConnectionDatasourceId: value.value, - dbConnectionDatasourceType: selectedDs.type, - dbConnectionDatasourceUid: selectedDs.uid, }, }); }; @@ -507,7 +505,6 @@ const getDirectDBDatasources = () => { const getDirectDBDSOptions = () => { const dsList = getDirectDBDatasources(); - console.log(dsList); const dsOpts: Array> = dsList.map((ds) => ({ label: ds.name, value: ds.id, diff --git a/src/datasource/types/config.ts b/src/datasource/types/config.ts index 540eaf703..06d424079 100644 --- a/src/datasource/types/config.ts +++ b/src/datasource/types/config.ts @@ -17,8 +17,6 @@ export type ZabbixDSOptions = { dbConnectionEnable: boolean; dbConnectionDatasourceId?: number; dbConnectionDatasourceName?: string; - dbConnectionDatasourceType?: string; - dbConnectionDatasourceUid?: string; dbConnectionRetentionPolicy?: string; disableReadOnlyUsersAck: boolean; disableDataAlignment: boolean; From 467f0bdd40270108a556ee53462d3f36ae039fbc Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Fri, 14 Nov 2025 09:18:39 -0800 Subject: [PATCH 05/10] remove backend logic from frontend testDatasoruce --- src/datasource/datasource.ts | 27 +++++++++++++++++---------- src/datasource/zabbix/zabbix.ts | 25 +++++++------------------ 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/datasource/datasource.ts b/src/datasource/datasource.ts index ec9dba643..2b5668aaf 100644 --- a/src/datasource/datasource.ts +++ b/src/datasource/datasource.ts @@ -21,6 +21,7 @@ import { toDataQueryResponse, getDataSourceSrv, HealthCheckError, + DataSourceWithBackend, } from '@grafana/runtime'; import { DataFrame, @@ -55,6 +56,7 @@ export class ZabbixDatasource extends DataSourceApi; zabbix: Zabbix; replaceTemplateVars: (target: any, scopedVars?: any) => any; @@ -62,6 +64,7 @@ export class ZabbixDatasource extends DataSourceApi) { super(instanceSettings); + this.instanceSettings = instanceSettings; this.enableDebugLog = config.buildInfo.env === 'development'; this.annotations = { @@ -767,17 +770,21 @@ export class ZabbixDatasource extends DataSourceApi { + return this.zabbix.testDataSource().then((dbConnectorStatus) => { + let message = testResult.message; + if (dbConnectorStatus) { + message += `, DB connector type: ${dbConnectorStatus.dsType}`; + } + return { + status: testResult.status, + message: message, + title: testResult.status, + }; + }); + }); } catch (error: any) { if (error instanceof ZabbixAPIError) { return Promise.reject({ diff --git a/src/datasource/zabbix/zabbix.ts b/src/datasource/zabbix/zabbix.ts index 8ee899e40..7561d06b8 100644 --- a/src/datasource/zabbix/zabbix.ts +++ b/src/datasource/zabbix/zabbix.ts @@ -208,32 +208,21 @@ export class Zabbix implements ZabbixConnector { * ``` */ testDataSource() { - let zabbixVersion; let dbConnectorStatus; - return this.getVersion() - .then((version) => { - zabbixVersion = version; - return this.getAllGroups(); - }) - .then(() => { - if (this.enableDirectDBConnection) { - return this.dbConnector.testDataSource(); - } else { - return Promise.resolve(); - } - }) - .catch((error) => { - return Promise.reject(error); - }) - .then((testResult) => { + + if (this.enableDirectDBConnection) { + return this.dbConnector.testDataSource().then((testResult) => { if (testResult) { dbConnectorStatus = { dsType: this.dbConnector.datasourceTypeName || this.dbConnector.datasourceTypeId, dsName: this.dbConnector.datasourceName, }; + return dbConnectorStatus; } - return { zabbixVersion, dbConnectorStatus }; }); + } else { + return Promise.resolve(); + } } async getVersion() { From c7f9c2be53e1ea7608258f8e46e7a5215e25a0e0 Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Fri, 14 Nov 2025 10:03:21 -0800 Subject: [PATCH 06/10] extract the way we get the version, expand on the message for health check --- pkg/datasource/datasource.go | 3 ++- pkg/datasource/zabbix.go | 5 ++--- pkg/zabbix/methods.go | 15 ++++++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/pkg/datasource/datasource.go b/pkg/datasource/datasource.go index 4fe9a73ee..29723f01f 100644 --- a/pkg/datasource/datasource.go +++ b/pkg/datasource/datasource.go @@ -3,6 +3,7 @@ package datasource import ( "context" "errors" + "fmt" "github.com/alexanderzobnin/grafana-zabbix/pkg/httpclient" "github.com/alexanderzobnin/grafana-zabbix/pkg/metrics" @@ -99,7 +100,7 @@ func (ds *ZabbixDatasource) CheckHealth(ctx context.Context, req *backend.CheckH } res.Status = backend.HealthStatusOk - res.Message = message + res.Message = fmt.Sprintf("Zabbix API version %s", message) return res, nil } diff --git a/pkg/datasource/zabbix.go b/pkg/datasource/zabbix.go index 5c4e42fb1..e990d8cc4 100644 --- a/pkg/datasource/zabbix.go +++ b/pkg/datasource/zabbix.go @@ -36,13 +36,12 @@ func (ds *ZabbixDatasourceInstance) TestConnection(ctx context.Context) (string, return "", err } - response, err := ds.zabbix.Request(ctx, &zabbix.ZabbixAPIRequest{Method: "apiinfo.version"}) + zabbixVersion, err := ds.zabbix.GetFullVersion(ctx) if err != nil { return "", err } - resultByte, _ := response.MarshalJSON() - return string(resultByte), nil + return zabbixVersion, nil } func (ds *ZabbixDatasourceInstance) queryNumericItems(ctx context.Context, query *QueryModel) ([]*data.Frame, error) { diff --git a/pkg/zabbix/methods.go b/pkg/zabbix/methods.go index 554e72023..fb53581cc 100644 --- a/pkg/zabbix/methods.go +++ b/pkg/zabbix/methods.go @@ -524,19 +524,28 @@ func (ds *Zabbix) GetValueMappings(ctx context.Context) ([]ValueMap, error) { return valuemaps, err } -func (ds *Zabbix) GetVersion(ctx context.Context) (int, error) { +func (ds *Zabbix) GetFullVersion(ctx context.Context) (string, error) { result, err := ds.request(ctx, "apiinfo.version", ZabbixAPIParams{}) if err != nil { - return 0, err + return "", err } var version string err = convertTo(result, &version) + if err != nil { + return "", err + } + + return version, nil +} + +func (ds *Zabbix) GetVersion(ctx context.Context) (int, error) { + fullStringVersion, err := ds.GetFullVersion(ctx) if err != nil { return 0, err } - version = strings.Replace(version[0:3], ".", "", 1) + version := strings.Replace(fullStringVersion[0:3], ".", "", 1) versionNum, err := strconv.Atoi(version) return versionNum, err } From af5ad44975bf092eae517abba151c600da3d1b5a Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Fri, 14 Nov 2025 10:42:04 -0800 Subject: [PATCH 07/10] use await instead .then --- src/datasource/datasource.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/datasource/datasource.ts b/src/datasource/datasource.ts index 2b5668aaf..8d18d285f 100644 --- a/src/datasource/datasource.ts +++ b/src/datasource/datasource.ts @@ -772,18 +772,17 @@ export class ZabbixDatasource extends DataSourceApi { - return this.zabbix.testDataSource().then((dbConnectorStatus) => { - let message = testResult.message; - if (dbConnectorStatus) { - message += `, DB connector type: ${dbConnectorStatus.dsType}`; - } - return { - status: testResult.status, - message: message, - title: testResult.status, - }; - }); + const testResult = await backendDS.testDatasource(); + return this.zabbix.testDataSource().then((dbConnectorStatus) => { + let message = testResult.message; + if (dbConnectorStatus) { + message += `, DB connector type: ${dbConnectorStatus.dsType}`; + } + return { + status: testResult.status, + message: message, + title: testResult.status, + }; }); } catch (error: any) { if (error instanceof ZabbixAPIError) { From 168139517a1fee67dd02f19800ffb7e0cbd1b1c7 Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Fri, 14 Nov 2025 10:57:29 -0800 Subject: [PATCH 08/10] log statement --- src/datasource/datasource.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/datasource/datasource.ts b/src/datasource/datasource.ts index 8d18d285f..c5196a6ab 100644 --- a/src/datasource/datasource.ts +++ b/src/datasource/datasource.ts @@ -89,6 +89,7 @@ export class ZabbixDatasource extends DataSourceApi { From 9dff1045678aa3aad652cd1668caa9bac8f12184 Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Fri, 14 Nov 2025 12:51:42 -0800 Subject: [PATCH 09/10] remove console logs --- src/datasource/datasource.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/datasource/datasource.ts b/src/datasource/datasource.ts index c5196a6ab..8d18d285f 100644 --- a/src/datasource/datasource.ts +++ b/src/datasource/datasource.ts @@ -89,7 +89,6 @@ export class ZabbixDatasource extends DataSourceApi { From d9abd21c911b66ab6391c82b777ab5e0e5ee9fb0 Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Fri, 14 Nov 2025 13:54:16 -0800 Subject: [PATCH 10/10] tests --- pkg/zabbix/methods_test.go | 400 +++++++++++++++++++++++++++++++++++++ pkg/zabbix/testing.go | 68 ++++++- pkg/zabbix/zabbix_test.go | 21 +- 3 files changed, 473 insertions(+), 16 deletions(-) create mode 100644 pkg/zabbix/methods_test.go diff --git a/pkg/zabbix/methods_test.go b/pkg/zabbix/methods_test.go new file mode 100644 index 000000000..b7236a474 --- /dev/null +++ b/pkg/zabbix/methods_test.go @@ -0,0 +1,400 @@ +package zabbix + +import ( + "context" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetFullVersion(t *testing.T) { + zabbixClient, err := MockZabbixClient(BasicDatasourceInfo, `{"result":"5.0.12"}`, 200) + require.NoError(t, err) + + version, err := zabbixClient.GetFullVersion(context.Background()) + assert.NoError(t, err) + assert.Equal(t, "5.0.12", version) +} + +func TestGetFullVersionInvalidPayload(t *testing.T) { + zabbixClient, err := MockZabbixClient(BasicDatasourceInfo, `{"result":{"version":"5.0.12"}}`, 200) + require.NoError(t, err) + + version, err := zabbixClient.GetFullVersion(context.Background()) + assert.Error(t, err) + assert.Equal(t, "", version) +} + +func TestGetVersion(t *testing.T) { + zabbixClient, err := MockZabbixClient(BasicDatasourceInfo, `{"result":"5.4.3"}`, 200) + require.NoError(t, err) + + version, err := zabbixClient.GetVersion(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 54, version) +} + +func TestGetVersionReturnsError(t *testing.T) { + zabbixClient, err := MockZabbixClient(BasicDatasourceInfo, `{"result":"a.b.c"}`, 200) + require.NoError(t, err) + + version, err := zabbixClient.GetVersion(context.Background()) + assert.Error(t, err) + assert.Equal(t, 0, version) +} + +func TestGetHistory(t *testing.T) { + var historyCalls []int + client := NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string { + if payload.Method == "history.get" { + historyCalls = append(historyCalls, int(payload.Params["history"].(float64))) + return `{"result":[{"itemid":"1","clock":"1","value":"1.2","ns":"0"}]}` + } + return `{"result":null}` + }) + + items := []*Item{ + {ID: "10", ValueType: 0}, + {ID: "20", ValueType: 3}, + } + tr := backend.TimeRange{ + From: time.Unix(0, 0), + To: time.Unix(10, 0), + } + + history, err := client.GetHistory(context.Background(), items, tr) + require.NoError(t, err) + assert.Len(t, history, 2) + assert.ElementsMatch(t, []int{0, 3}, historyCalls) +} + +func TestGetTrend(t *testing.T) { + var capturedIDs []string + client := NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string { + if payload.Method == "trend.get" { + for _, raw := range payload.Params["itemids"].([]interface{}) { + capturedIDs = append(capturedIDs, raw.(string)) + } + return `{"result":[{"itemid":"1","clock":"1","value_min":"0","value_avg":"1","value_max":"2"}]}` + } + return `{"result":null}` + }) + + items := []*Item{{ID: "100"}, {ID: "200"}} + tr := backend.TimeRange{ + From: time.Unix(0, 0), + To: time.Unix(10, 0), + } + + trend, err := client.GetTrend(context.Background(), items, tr) + require.NoError(t, err) + assert.Len(t, trend, 1) + assert.ElementsMatch(t, []string{"100", "200"}, capturedIDs) +} + +func TestGetItems(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`, + "host.get": `{"result":[{"hostid":"10","name":"web01"}]}`, + "item.get": `{ + "result":[ + {"itemid":"100","name":"CPU usage"}, + {"itemid":"200","name":"Memory usage"} + ] + }`, + }) + + items, err := client.GetItems(context.Background(), "Servers", "web01", "", "/CPU/", "num", false) + require.NoError(t, err) + if assert.Len(t, items, 1) { + assert.Equal(t, "100", items[0].ID) + } +} + +func TestGetItemsBefore54(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`, + "host.get": `{"result":[{"hostid":"10","name":"web01"}]}`, + "application.get": `{ + "result":[ + {"applicationid":"50","name":"Databases"}, + {"applicationid":"60","name":"Apps"} + ] + }`, + "item.get": `{ + "result":[ + {"itemid":"500","name":"DB Size"}, + {"itemid":"600","name":"API latency"} + ] + }`, + }) + + items, err := client.GetItemsBefore54(context.Background(), "Servers", "web01", "Databases", "/DB/", "num", false) + require.NoError(t, err) + if assert.Len(t, items, 1) { + assert.Equal(t, "500", items[0].ID) + } +} + +func TestFilterItemsByQuery(t *testing.T) { + items := []*Item{ + {Name: "CPU usage"}, + {Name: "Memory usage"}, + } + + filtered, err := filterItemsByQuery(items, "/CPU/") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "CPU usage", filtered[0].Name) + + filtered, err = filterItemsByQuery(items, "Memory usage") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "Memory usage", filtered[0].Name) +} + +func TestGetApps(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`, + "host.get": `{"result":[{"hostid":"10","name":"web01"}]}`, + "application.get": `{ + "result":[ + {"applicationid":"50","name":"API"}, + {"applicationid":"60","name":"DB"} + ] + }`, + }) + + apps, err := client.GetApps(context.Background(), "Servers", "web01", "/^API/") + require.NoError(t, err) + if assert.Len(t, apps, 1) { + assert.Equal(t, "50", apps[0].ID) + } +} + +func TestFilterAppsByQuery(t *testing.T) { + apps := []Application{ + {Name: "API"}, + {Name: "DB"}, + } + + filtered, err := filterAppsByQuery(apps, "/^A/") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "API", filtered[0].Name) + + filtered, err = filterAppsByQuery(apps, "DB") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "DB", filtered[0].Name) +} + +func TestGetItemTags(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`, + "host.get": `{"result":[{"hostid":"10","name":"web01"}]}`, + "item.get": `{ + "result":[ + {"itemid":"1","name":"CPU","tags":[{"tag":"Env","value":"prod"},{"tag":"Application","value":"api"}]}, + {"itemid":"2","name":"Mem","tags":[{"tag":"Env","value":"stage"},{"tag":"Env","value":"prod"}]} + ] + }`, + }) + + tags, err := client.GetItemTags(context.Background(), "Servers", "web01", "/^Env/") + require.NoError(t, err) + assert.ElementsMatch(t, []ItemTag{ + {Tag: "Env", Value: "prod"}, + {Tag: "Env", Value: "stage"}, + }, tags) +} + +func TestFilterTags(t *testing.T) { + tags := []ItemTag{ + {Tag: "Env", Value: "prod"}, + {Tag: "Application", Value: "api"}, + } + + filtered, err := filterTags(tags, "/^Env/") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "Env", filtered[0].Tag) + + filtered, err = filterTags(tags, "Application: api") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "Application", filtered[0].Tag) +} + +func TestGetHosts(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`, + "host.get": `{ + "result":[ + {"hostid":"10","name":"web01"}, + {"hostid":"20","name":"db01"} + ] + }`, + }) + + hosts, err := client.GetHosts(context.Background(), "Servers", "/web/") + require.NoError(t, err) + if assert.Len(t, hosts, 1) { + assert.Equal(t, "web01", hosts[0].Name) + } +} + +func TestFilterHostsByQuery(t *testing.T) { + hosts := []Host{ + {Name: "web01"}, + {Name: "db01"}, + } + + filtered, err := filterHostsByQuery(hosts, "/^web/") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "web01", filtered[0].Name) + + filtered, err = filterHostsByQuery(hosts, "db01") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "db01", filtered[0].Name) +} + +func TestGetGroups(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"},{"groupid":"2","name":"Apps"}]}`, + }) + + groups, err := client.GetGroups(context.Background(), "/Apps/") + require.NoError(t, err) + if assert.Len(t, groups, 1) { + assert.Equal(t, "Apps", groups[0].Name) + } +} + +func TestFilterGroupsByQuery(t *testing.T) { + groups := []Group{ + {Name: "Servers"}, + {Name: "Apps"}, + } + + filtered, err := filterGroupsByQuery(groups, "/Apps/") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "Apps", filtered[0].Name) + + filtered, err = filterGroupsByQuery(groups, "Servers") + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, "Servers", filtered[0].Name) +} + +func TestGetAllItemsBuildsParams(t *testing.T) { + var lastRequest ApiRequestPayload + client := NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string { + if payload.Method == "item.get" { + lastRequest = payload + return `{"result":[{"itemid":"1","name":"CPU $1","key_":"system.cpu[user]","value_type":"0","hosts":[{"hostid":"10","name":"web"}]}]}` + } + return `{"result":null}` + }) + + items, err := client.GetAllItems( + context.Background(), + []string{"10"}, + nil, + "num", + false, + "Env: prod, Application: api", + ) + require.NoError(t, err) + if assert.Len(t, items, 1) { + assert.Equal(t, "CPU user", items[0].Name) + } + + require.Equal(t, "item.get", lastRequest.Method) + params := lastRequest.Params + assert.Equal(t, []interface{}{"10"}, params["hostids"]) + assert.Equal(t, true, params["monitored"]) + filter := params["filter"].(map[string]interface{}) + assert.ElementsMatch(t, []interface{}{float64(0), float64(3)}, filter["value_type"].([]interface{})) + tags := params["tags"].([]interface{}) + if assert.Len(t, tags, 2) { + first := tags[0].(map[string]interface{}) + second := tags[1].(map[string]interface{}) + assert.Equal(t, "Application", first["tag"]) + assert.Equal(t, "Env", second["tag"]) + } +} + +func TestGetItemsByIDs(t *testing.T) { + var lastRequest ApiRequestPayload + client := NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string { + if payload.Method == "item.get" { + lastRequest = payload + return `{"result":[{"itemid":"1","name":"CPU"}]}` + } + return `{"result":null}` + }) + + items, err := client.GetItemsByIDs(context.Background(), []string{"1"}) + require.NoError(t, err) + assert.Len(t, items, 1) + assert.Equal(t, []interface{}{"1"}, lastRequest.Params["itemids"].([]interface{})) +} + +func TestGetAllApps(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "application.get": `{"result":[{"applicationid":"10","name":"API"}]}`, + }) + + apps, err := client.GetAllApps(context.Background(), []string{"1"}) + require.NoError(t, err) + assert.Len(t, apps, 1) + assert.Equal(t, "API", apps[0].Name) +} + +func TestGetAllHosts(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "host.get": `{"result":[{"hostid":"10","name":"web01"}]}`, + }) + + hosts, err := client.GetAllHosts(context.Background(), []string{"1"}) + require.NoError(t, err) + assert.Len(t, hosts, 1) + assert.Equal(t, "web01", hosts[0].Name) +} + +func TestGetAllGroups(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`, + }) + + groups, err := client.GetAllGroups(context.Background()) + require.NoError(t, err) + assert.Len(t, groups, 1) + assert.Equal(t, "Servers", groups[0].Name) +} + +func TestGetValueMappings(t *testing.T) { + client := NewZabbixClientWithResponses(t, map[string]string{ + "valuemap.get": `{ + "result":[ + { + "valuemapid":"1", + "name":"Status", + "mappings":[{"value":"0","newvalue":"down"}] + } + ] + }`, + }) + + valueMaps, err := client.GetValueMappings(context.Background()) + require.NoError(t, err) + assert.Len(t, valueMaps, 1) + assert.Equal(t, "Status", valueMaps[0].Name) +} diff --git a/pkg/zabbix/testing.go b/pkg/zabbix/testing.go index f5e0d83cb..32100595a 100644 --- a/pkg/zabbix/testing.go +++ b/pkg/zabbix/testing.go @@ -1,12 +1,31 @@ package zabbix import ( + "encoding/json" + "io" + "net/http" + "strings" + "testing" + "time" + "github.com/alexanderzobnin/grafana-zabbix/pkg/settings" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi" "github.com/grafana/grafana-plugin-sdk-go/backend" - "time" + "github.com/stretchr/testify/require" ) +var BasicDatasourceInfo = &backend.DataSourceInstanceSettings{ + ID: 1, + Name: "TestDatasource", + URL: "http://zabbix.org/zabbix", + JSONData: []byte(`{"username":"username", "password":"password", "cacheTTL":"10m"}`), +} + +type ApiRequestPayload struct { + Method string `json:"method"` + Params map[string]interface{} `json:"params"` +} + func MockZabbixClient(dsInfo *backend.DataSourceInstanceSettings, body string, statusCode int) (*Zabbix, error) { zabbixAPI, err := zabbixapi.MockZabbixAPI(body, statusCode) if err != nil { @@ -34,3 +53,50 @@ func MockZabbixClientResponse(client *Zabbix, body string, statusCode int) (*Zab return client, nil } + +func NewZabbixClientWithHandler(t *testing.T, handler func(ApiRequestPayload) string) *Zabbix { + t.Helper() + + httpClient := zabbixapi.NewTestClient(func(req *http.Request) *http.Response { + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + + var payload ApiRequestPayload + require.NoError(t, json.Unmarshal(body, &payload)) + + responseBody := handler(payload) + if responseBody == "" { + responseBody = `{"result":null}` + } + + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(responseBody)), + Header: make(http.Header), + } + }) + + dsSettings := *BasicDatasourceInfo + zabbixAPI, err := zabbixapi.New(dsSettings, httpClient) + require.NoError(t, err) + + zabbixSettings := &settings.ZabbixDatasourceSettings{ + Timeout: 10 * time.Second, + } + client, err := New(BasicDatasourceInfo, zabbixSettings, zabbixAPI) + require.NoError(t, err) + + client.api.SetAuth("test") + client.version = 60 + + return client +} + +func NewZabbixClientWithResponses(t *testing.T, responses map[string]string) *Zabbix { + return NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string { + if resp, ok := responses[payload.Method]; ok { + return resp + } + return `{"result":null}` + }) +} diff --git a/pkg/zabbix/zabbix_test.go b/pkg/zabbix/zabbix_test.go index 25cabeb44..2091d0efa 100644 --- a/pkg/zabbix/zabbix_test.go +++ b/pkg/zabbix/zabbix_test.go @@ -7,21 +7,12 @@ import ( "github.com/alexanderzobnin/grafana-zabbix/pkg/settings" "github.com/stretchr/testify/assert" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) -var basicDatasourceInfo = &backend.DataSourceInstanceSettings{ - ID: 1, - Name: "TestDatasource", - URL: "http://zabbix.org/zabbix", - JSONData: []byte(`{"username":"username", "password":"password", "cacheTTL":"10m"}`), -} - var emptyParams = map[string]interface{}{} func TestLogin(t *testing.T) { - zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"secretauth"}`, 200) + zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":"secretauth"}`, 200) err := zabbixClient.Authenticate(context.Background()) assert.NoError(t, err) @@ -29,7 +20,7 @@ func TestLogin(t *testing.T) { } func TestLoginError(t *testing.T) { - zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":""}`, 500) + zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":""}`, 500) err := zabbixClient.Authenticate(context.Background()) assert.Error(t, err) @@ -37,7 +28,7 @@ func TestLoginError(t *testing.T) { } func TestZabbixAPIQuery(t *testing.T) { - zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"test"}`, 200) + zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":"test"}`, 200) resp, err := zabbixClient.Request(context.Background(), &ZabbixAPIRequest{Method: "test.get", Params: emptyParams}) assert.NoError(t, err) @@ -50,7 +41,7 @@ func TestZabbixAPIQuery(t *testing.T) { func TestCachedQuery(t *testing.T) { // Using methods with caching enabled query := &ZabbixAPIRequest{Method: "host.get", Params: emptyParams} - zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"testOld"}`, 200) + zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":"testOld"}`, 200) // Run query first time resp, err := zabbixClient.Request(context.Background(), query) @@ -72,7 +63,7 @@ func TestCachedQuery(t *testing.T) { func TestNonCachedQuery(t *testing.T) { // Using methods with caching disabled query := &ZabbixAPIRequest{Method: "history.get", Params: emptyParams} - zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"testOld"}`, 200) + zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":"testOld"}`, 200) // Run query first time resp, err := zabbixClient.Request(context.Background(), query) @@ -93,7 +84,7 @@ func TestNonCachedQuery(t *testing.T) { func TestItemTagCache(t *testing.T) { zabbixClient, _ := MockZabbixClient( - basicDatasourceInfo, + BasicDatasourceInfo, `{"result":[{"itemid":"1","name":"test1"}]}`, 200, )