Merge branch 'multidatasource-pr' of github.com:dneth/grafana-zabbix into dneth-multidatasource-pr

This commit is contained in:
Alexander Zobnin
2019-12-13 13:52:09 +03:00
22 changed files with 6852 additions and 860 deletions

View File

@@ -1,7 +0,0 @@
{
"presets": [
"env",
"react"
],
"retainLines": true
}

7
babel.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
"presets": [
[ "@babel/env", { targets: { node: 'current' } } ],
"@babel/react"
],
"retainLines": true
}

8
go.mod
View File

@@ -5,10 +5,16 @@ go 1.12
require (
github.com/bitly/go-simplejson v0.5.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/grafana/grafana_plugin_model v0.0.0-20180518082423-84176c64269d
github.com/hashicorp/go-hclog v0.9.2
github.com/hashicorp/go-plugin v1.0.1
github.com/kr/pretty v0.1.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible
golang.org/x/net v0.0.0-20190311183353-d8887717615a
github.com/pkg/errors v0.8.1 // indirect
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
gotest.tools v2.2.0+incompatible
)

17
go.sum
View File

@@ -9,6 +9,10 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekf
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/grafana/grafana_plugin_model v0.0.0-20180518082423-84176c64269d h1:Ep6bjWDwurT9NpUATiqa8NYIllmZbLDXLn6Ib4lCtAA=
github.com/grafana/grafana_plugin_model v0.0.0-20180518082423-84176c64269d/go.mod h1:70BFhO60E3e7kq+ssiouwX/HX5DvQ3L6XffdH0S2YfU=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
@@ -29,6 +33,8 @@ github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -38,17 +44,24 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc h1:WiYx1rIFmx8c0mXAFtv5D/mHyKe1+jmuP7PViuwqwuQ=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.14.0 h1:ArxJuB1NWfPY6r9Gp9gqwplT0Ge7nqv9msgu03lHLmo=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

View File

@@ -25,7 +25,14 @@
"url": "https://github.com/alexanderzobnin/grafana-zabbix/issues"
},
"devDependencies": {
"@types/classnames": "^2.2.6",
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"@babel/preset-react": "^7.6.3",
"@grafana/data": "^6.4.3",
"@grafana/runtime": "^6.4.3",
"@grafana/toolkit": "^6.4.3",
"@grafana/ui": "^6.4.3",
"@types/classnames": "^2.2.9",
"@types/grafana": "github:CorpGlory/types-grafana",
"@types/jest": "^23.1.1",
"@types/jquery": "^3.3.0",
@@ -34,13 +41,10 @@
"@types/react": "^16.4.6",
"@types/react-dom": "^16.0.11",
"@types/react-transition-group": "^2.0.15",
"babel-core": "^6.26.3",
"babel-jest": "^23.6.0",
"babel-loader": "^7.1.2",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"benchmark": "^2.1.4",
"classnames": "^2.2.6",
"clean-webpack-plugin": "^0.1.19",
@@ -75,9 +79,9 @@
"style-loader": "^0.23.1",
"tether-drop": "^1.4.2",
"ts-jest": "^23.10.5",
"ts-loader": "^4.4.1",
"ts-loader": "^6.2.0",
"tslint": "^5.11.0",
"typescript": "^2.9.2",
"typescript": "^3.6.4",
"webpack": "^4.22.0",
"webpack-cli": "^3.1.2"
},

View File

@@ -3,8 +3,10 @@ package main
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"time"
"github.com/grafana/grafana_plugin_model/go/datasource"
cache "github.com/patrickmn/go-cache"
)
@@ -30,9 +32,18 @@ func (c *Cache) Get(request string) (interface{}, bool) {
return c.cache.Get(request)
}
// Hash converts the given text string to hash string
func Hash(text string) string {
// HashString converts the given text string to hash string
func HashString(text string) string {
hash := sha1.New()
hash.Write([]byte(text))
return hex.EncodeToString(hash.Sum(nil))
}
// HashDatasourceInfo converts the given datasource info to hash string
func HashDatasourceInfo(dsInfo *datasource.DatasourceInfo) string {
digester := sha1.New()
if err := json.NewEncoder(digester).Encode(dsInfo); err != nil {
panic(err) // This shouldn't be possible but just in case DatasourceInfo changes
}
return hex.EncodeToString(digester.Sum(nil))
}

View File

@@ -1,3 +1,68 @@
package main
// Dummy test file for now
import (
"testing"
"github.com/grafana/grafana_plugin_model/go/datasource"
"gotest.tools/assert"
)
func TestHashDatasourceInfo(t *testing.T) {
tests := []struct {
name string
dsInfo *datasource.DatasourceInfo
want string
}{
{
name: "Normal Datasource Info",
dsInfo: &datasource.DatasourceInfo{
Id: 1,
OrgId: 1,
Name: "Zabbix",
Type: "alexanderzobnin-zabbix-datasource",
Url: "https://localhost:3306/zabbix/api_jsonrpc.php",
JsonData: "{}",
DecryptedSecureJsonData: map[string]string{
"username": "grafanaZabbixUser",
"password": "$uper$ecr3t!!!",
},
},
want: "ed161f89179c46d9a578e4d7e92ff95444222e0a",
},
// Can't find a case where the input causes the encoder to fail
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := HashDatasourceInfo(tt.dsInfo)
assert.Equal(t, tt.want, got)
})
}
}
func BenchmarkHashDatasourceInfo(b *testing.B) {
benches := []struct {
name string
dsInfo *datasource.DatasourceInfo
}{
{
"Normal Datasource Info",
&datasource.DatasourceInfo{
Id: 4,
OrgId: 6,
Name: "MyZabbixDatasource",
Type: "alexanderzobnin-zabbix-datasource",
Url: "https://localhost:3306/zabbix/api_jsonrpc.php",
JsonData: `{ "addThresholds": true, "disableReadOnlyUsersAck": true }`,
DecryptedSecureJsonData: map[string]string{
"username": "grafanaZabbixUser",
"password": "$uper$ecr3t!!!",
},
},
},
}
for _, bt := range benches {
b.Run(bt.name, func(b *testing.B) {
HashDatasourceInfo(bt.dsInfo)
})
}
}

View File

@@ -1,7 +1,9 @@
package main
import (
"encoding/json"
"errors"
"fmt"
simplejson "github.com/bitly/go-simplejson"
"github.com/grafana/grafana_plugin_model/go/datasource"
@@ -10,39 +12,95 @@ import (
"golang.org/x/net/context"
)
type ZabbixDatasource struct {
// ZabbixBackend implements the Grafana backend interface and forwards queries to the ZabbixDatasource
type ZabbixBackend struct {
plugin.NetRPCUnsupportedPlugin
logger hclog.Logger
datasourceCache *Cache
}
func (ds *ZabbixDatasource) Query(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) {
func (b *ZabbixBackend) newZabbixDatasource() *ZabbixDatasource {
ds := NewZabbixDatasource()
ds.logger = b.logger
return ds
}
// 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) {
zabbixDs := b.getCachedDatasource(tsdbReq)
queryType, err := GetQueryType(tsdbReq)
if err != nil {
return nil, err
}
dsInfo := tsdbReq.GetDatasource()
ds.logger.Debug("createRequest", "dsInfo", dsInfo)
ds.logger.Debug("createRequest", "queryType", queryType)
switch queryType {
case "zabbixAPI":
return ds.ZabbixAPIQuery(ctx, tsdbReq)
return zabbixDs.ZabbixAPIQuery(ctx, tsdbReq)
case "connectionTest":
return zabbixDs.TestConnection(ctx, tsdbReq)
default:
return nil, errors.New("Query is not implemented yet")
err = errors.New("Query not implemented")
return BuildErrorResponse(err), nil
}
}
func (b *ZabbixBackend) getCachedDatasource(tsdbReq *datasource.DatasourceRequest) *ZabbixDatasource {
dsInfoHash := HashDatasourceInfo(tsdbReq.GetDatasource())
if cachedData, ok := b.datasourceCache.Get(dsInfoHash); ok {
if cachedDS, ok := cachedData.(*ZabbixDatasource); ok {
return cachedDS
}
}
if b.logger.IsDebug() {
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()
}
// GetQueryType determines the query type from a query or list of queries
func GetQueryType(tsdbReq *datasource.DatasourceRequest) (string, error) {
queryType := "query"
if len(tsdbReq.Queries) > 0 {
firstQuery := tsdbReq.Queries[0]
queryJson, err := simplejson.NewJson([]byte(firstQuery.ModelJson))
queryJSON, err := simplejson.NewJson([]byte(firstQuery.ModelJson))
if err != nil {
return "", err
}
queryType = queryJson.Get("queryType").MustString("query")
queryType = queryJSON.Get("queryType").MustString("query")
}
return queryType, nil
}
// BuildResponse transforms a Zabbix API response to a DatasourceResponse
func BuildResponse(responseData interface{}) (*datasource.DatasourceResponse, error) {
jsonBytes, err := json.Marshal(responseData)
if err != nil {
return nil, err
}
return &datasource.DatasourceResponse{
Results: []*datasource.QueryResult{
&datasource.QueryResult{
RefId: "zabbixAPI",
MetaJson: string(jsonBytes),
},
},
}, nil
}
// BuildErrorResponse creates a QueryResult that forwards an error to the front-end
func BuildErrorResponse(err error) *datasource.DatasourceResponse {
return &datasource.DatasourceResponse{
Results: []*datasource.QueryResult{
&datasource.QueryResult{
RefId: "zabbixAPI",
Error: err.Error(),
},
},
}
}

143
pkg/datasource_test.go Normal file
View File

@@ -0,0 +1,143 @@
package main
import (
"testing"
simplejson "github.com/bitly/go-simplejson"
"github.com/grafana/grafana_plugin_model/go/datasource"
hclog "github.com/hashicorp/go-hclog"
cache "github.com/patrickmn/go-cache"
"gotest.tools/assert"
"gotest.tools/assert/cmp"
)
func TestZabbixBackend_getCachedDatasource(t *testing.T) {
basicDatasourceInfo := &datasource.DatasourceInfo{
Id: 1,
Name: "TestDatasource",
}
basicDatasourceInfoHash := HashDatasourceInfo(basicDatasourceInfo)
modifiedDatasource := NewZabbixDatasource()
modifiedDatasource.authToken = "AB404F1234"
altDatasourceInfo := &datasource.DatasourceInfo{
Id: 2,
Name: "AnotherDatasource",
}
altDatasourceInfoHash := HashDatasourceInfo(altDatasourceInfo)
tests := []struct {
name string
cache *cache.Cache
request *datasource.DatasourceRequest
want *ZabbixDatasource
}{
{
name: "Uncached Datasource (nothing in cache)",
request: &datasource.DatasourceRequest{
Datasource: basicDatasourceInfo,
},
want: NewZabbixDatasource(),
},
{
name: "Uncached Datasource (cache miss)",
cache: cache.NewFrom(cache.NoExpiration, cache.NoExpiration, map[string]cache.Item{
basicDatasourceInfoHash: cache.Item{Object: modifiedDatasource},
}),
request: &datasource.DatasourceRequest{
Datasource: altDatasourceInfo,
},
want: NewZabbixDatasource(),
},
{
name: "Cached Datasource",
cache: cache.NewFrom(cache.NoExpiration, cache.NoExpiration, map[string]cache.Item{
altDatasourceInfoHash: cache.Item{Object: NewZabbixDatasource()},
basicDatasourceInfoHash: cache.Item{Object: modifiedDatasource},
}),
request: &datasource.DatasourceRequest{
Datasource: basicDatasourceInfo,
},
want: modifiedDatasource,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.cache == nil {
tt.cache = cache.New(cache.NoExpiration, cache.NoExpiration)
}
b := &ZabbixBackend{
logger: hclog.New(&hclog.LoggerOptions{
Name: "TestZabbixBackend_getCachedDatasource",
Level: hclog.LevelFromString("DEBUG"),
}),
datasourceCache: &Cache{cache: tt.cache},
}
got := b.getCachedDatasource(tt.request)
// Only checking the authToken, being the easiest value to, and guarantee equality for
assert.Equal(t, tt.want.authToken, got.authToken)
})
}
}
func TestBuildResponse(t *testing.T) {
jsonData := simplejson.New()
jsonData.Set("testing", []int{5, 12, 75})
tests := []struct {
name string
responseData interface{}
want *datasource.DatasourceResponse
wantErr string
}{
{
name: "simplejson Response",
responseData: jsonData,
want: &datasource.DatasourceResponse{
Results: []*datasource.QueryResult{
&datasource.QueryResult{
RefId: "zabbixAPI",
MetaJson: `{"testing":[5,12,75]}`,
},
},
},
},
{
name: "Connetion Status Response",
responseData: connectionTestResponse{
ZabbixVersion: "2.4",
DbConnectorStatus: &dbConnectionStatus{
DsType: "mysql",
DsName: "MyDatabase",
},
},
want: &datasource.DatasourceResponse{
Results: []*datasource.QueryResult{
&datasource.QueryResult{
RefId: "zabbixAPI",
MetaJson: `{"zabbixVersion":"2.4","dbConnectorStatus":{"dsType":"mysql","dsName":"MyDatabase"}}`,
},
},
},
},
{
name: "Unmarshalable",
responseData: 2i,
wantErr: "json: unsupported type: complex128",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := BuildResponse(tt.responseData)
if tt.wantErr != "" {
assert.Error(t, err, tt.wantErr)
assert.Assert(t, cmp.Nil(got))
return
}
assert.NilError(t, err)
assert.DeepEqual(t, got, tt.want)
})
}
}

View File

@@ -1 +1,11 @@
package main
type connectionTestResponse struct {
ZabbixVersion string `json:"zabbixVersion"`
DbConnectorStatus *dbConnectionStatus `json:"dbConnectorStatus"`
}
type dbConnectionStatus struct {
DsType string `json:"dsType"`
DsName string `json:"dsName"`
}

View File

@@ -1,6 +1,8 @@
package main
import (
"time"
"github.com/grafana/grafana_plugin_model/go/datasource"
hclog "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
@@ -22,7 +24,8 @@ func main() {
MagicCookieValue: "datasource",
},
Plugins: map[string]plugin.Plugin{
"zabbix-backend-datasource": &datasource.DatasourcePluginImpl{Plugin: &ZabbixDatasource{
"zabbix-backend-datasource": &datasource.DatasourcePluginImpl{Plugin: &ZabbixBackend{
datasourceCache: NewCache(10*time.Minute, 10*time.Minute),
logger: pluginLogger,
}},
},

View File

@@ -15,11 +15,25 @@ import (
simplejson "github.com/bitly/go-simplejson"
"github.com/grafana/grafana_plugin_model/go/datasource"
hclog "github.com/hashicorp/go-hclog"
"golang.org/x/net/context"
"golang.org/x/net/context/ctxhttp"
)
var httpClient = &http.Client{
// ZabbixDatasource stores state about a specific datasource and provides methods to make
// requests to the Zabbix API
type ZabbixDatasource struct {
queryCache *Cache
logger hclog.Logger
httpClient *http.Client
authToken string
}
// NewZabbixDatasource returns an initialized ZabbixDatasource
func NewZabbixDatasource() *ZabbixDatasource {
return &ZabbixDatasource{
queryCache: NewCache(10*time.Minute, 10*time.Minute),
httpClient: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
@@ -28,7 +42,6 @@ var httpClient = &http.Client{
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
@@ -36,14 +49,13 @@ var httpClient = &http.Client{
IdleConnTimeout: 90 * time.Second,
},
Timeout: time.Duration(time.Second * 30),
},
}
}
var queryCache = NewCache(10*time.Minute, 10*time.Minute)
var zabbixAuth string = ""
// ZabbixAPIQuery handles query requests to Zabbix
func (ds *ZabbixDatasource) ZabbixAPIQuery(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) {
result, queryExistInCache := queryCache.Get(Hash(tsdbReq.String()))
result, queryExistInCache := ds.queryCache.Get(HashString(tsdbReq.String()))
if !queryExistInCache {
dsInfo := tsdbReq.GetDatasource()
@@ -52,7 +64,7 @@ func (ds *ZabbixDatasource) ZabbixAPIQuery(ctx context.Context, tsdbReq *datasou
for _, query := range tsdbReq.Queries {
json, err := simplejson.NewJson([]byte(query.ModelJson))
apiMethod := json.GetPath("target", "method").MustString()
apiParams := json.GetPath("target", "params")
apiParams := json.GetPath("target", "params").MustMap()
if err != nil {
return nil, err
@@ -69,11 +81,11 @@ func (ds *ZabbixDatasource) ZabbixAPIQuery(ctx context.Context, tsdbReq *datasou
jsonQuery := jsonQueries[0].Get("target")
apiMethod := jsonQuery.Get("method").MustString()
apiParams := jsonQuery.Get("params")
apiParams := jsonQuery.Get("params").MustMap()
var err error
result, err = ds.ZabbixRequest(ctx, dsInfo, apiMethod, apiParams)
queryCache.Set(Hash(tsdbReq.String()), result)
response, err := ds.ZabbixRequest(ctx, dsInfo, apiMethod, apiParams)
ds.queryCache.Set(HashString(tsdbReq.String()), response)
result = response
if err != nil {
ds.logger.Debug("ZabbixAPIQuery", "error", err)
return nil, errors.New("ZabbixAPIQuery is not implemented yet")
@@ -83,44 +95,55 @@ func (ds *ZabbixDatasource) ZabbixAPIQuery(ctx context.Context, tsdbReq *datasou
resultByte, _ := result.(*simplejson.Json).MarshalJSON()
ds.logger.Debug("ZabbixAPIQuery", "result", string(resultByte))
return ds.BuildResponse(result.(*simplejson.Json))
return BuildResponse(result)
}
func (ds *ZabbixDatasource) BuildResponse(result *simplejson.Json) (*datasource.DatasourceResponse, error) {
resultByte, err := result.MarshalJSON()
// TestConnection checks authentication and version of the Zabbix API and returns that info
func (ds *ZabbixDatasource) TestConnection(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) {
dsInfo := tsdbReq.GetDatasource()
auth, err := ds.loginWithDs(ctx, dsInfo)
if err != nil {
return nil, err
return BuildErrorResponse(fmt.Errorf("Authentication failed: %w", err)), nil
}
ds.authToken = auth
response, err := ds.zabbixAPIRequest(ctx, dsInfo.GetUrl(), "apiinfo.version", map[string]interface{}{}, "")
if err != nil {
ds.logger.Debug("TestConnection", "error", err)
return BuildErrorResponse(fmt.Errorf("Version check failed: %w", err)), nil
}
return &datasource.DatasourceResponse{
Results: []*datasource.QueryResult{
&datasource.QueryResult{
RefId: "zabbixAPI",
MetaJson: string(resultByte),
},
},
}, nil
resultByte, _ := response.MarshalJSON()
ds.logger.Debug("TestConnection", "result", string(resultByte))
testResponse := connectionTestResponse{
ZabbixVersion: response.MustString(),
}
func (ds *ZabbixDatasource) ZabbixRequest(ctx context.Context, dsInfo *datasource.DatasourceInfo, method string, params *simplejson.Json) (*simplejson.Json, error) {
return BuildResponse(testResponse)
}
// 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) {
zabbixURL := dsInfo.GetUrl()
var result *simplejson.Json
var err error
for attempt := 0; attempt <= 3; attempt++ {
if zabbixAuth == "" {
if ds.authToken == "" {
// Authenticate
zabbixAuth, err = ds.loginWithDs(ctx, dsInfo)
ds.authToken, err = ds.loginWithDs(ctx, dsInfo)
if err != nil {
return nil, err
}
}
result, err = ds.zabbixAPIRequest(ctx, zabbixURL, method, params, zabbixAuth)
result, err = ds.zabbixAPIRequest(ctx, zabbixURL, method, params, ds.authToken)
if err == nil || (err != nil && !isNotAuthorized(err.Error())) {
break
} else {
zabbixAuth = ""
ds.authToken = ""
}
}
return result, err
@@ -162,12 +185,7 @@ func (ds *ZabbixDatasource) login(ctx context.Context, apiURL string, username s
"user": username,
"password": password,
}
paramsJSON, err := json.Marshal(params)
if err != nil {
return "", err
}
data, _ := simplejson.NewJson(paramsJSON)
auth, err := ds.zabbixAPIRequest(ctx, apiURL, "user.login", data, "")
auth, err := ds.zabbixAPIRequest(ctx, apiURL, "user.login", params, "")
if err != nil {
return "", err
}
@@ -175,7 +193,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 *simplejson.Json, auth string) (*simplejson.Json, error) {
func (ds *ZabbixDatasource) zabbixAPIRequest(ctx context.Context, apiURL string, method string, params map[string]interface{}, auth string) (*simplejson.Json, error) {
zabbixURL, err := url.Parse(apiURL)
// TODO: inject auth token (obtain from 'user.login' first)
@@ -211,7 +229,7 @@ func (ds *ZabbixDatasource) zabbixAPIRequest(ctx context.Context, apiURL string,
Body: rc,
}
response, err := makeHTTPRequest(ctx, req)
response, err := makeHTTPRequest(ctx, ds.httpClient, req)
if err != nil {
return nil, err
}
@@ -234,7 +252,7 @@ func handleAPIResult(response []byte) (*simplejson.Json, error) {
return jsonResult, nil
}
func makeHTTPRequest(ctx context.Context, req *http.Request) ([]byte, error) {
func makeHTTPRequest(ctx context.Context, httpClient *http.Client, req *http.Request) ([]byte, error) {
res, err := ctxhttp.Do(ctx, httpClient, req)
if err != nil {
return nil, err

1
pkg/zabbix_api_test.go Normal file
View File

@@ -0,0 +1 @@
package main

View File

@@ -9,13 +9,29 @@ import dataProcessor from './dataProcessor';
import responseHandler from './responseHandler';
import { Zabbix } from './zabbix/zabbix';
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore';
import {
DataSourceApi,
DataSourceInstanceSettings,
} from '@grafana/ui';
import { BackendSrv, DataSourceSrv } from '@grafana/runtime';
import { ZabbixAlertingService } from './zabbixAlerting.service';
import { ZabbixConnectionTestQuery, ZabbixConnectionInfo, TemplateSrv, TSDBResponse } from './types';
const DEFAULT_ZABBIX_VERSION = 3;
export class ZabbixDatasource {
export class ZabbixDatasource extends DataSourceApi {
/** @ngInject */
/**
* @ngInject
* @param {DataSourceInstanceSettings} instanceSettings
* @param {TemplateSrv} templateSrv
* @param {BackendSrv} backendSrv
* @param {DataSourceSrv} datasourceSrv
* @param {ZabbixAlertingService} zabbixAlertingSrv
*/
constructor(instanceSettings, templateSrv, backendSrv, datasourceSrv, zabbixAlertingSrv) {
super(instanceSettings);
this.type = 'zabbix';
this.templateSrv = templateSrv;
this.backendSrv = backendSrv;
this.zabbixAlertingSrv = zabbixAlertingSrv;
@@ -185,11 +201,26 @@ export class ZabbixDatasource {
tsdbRequestData.to = options.range.to.valueOf().toString();
}
return this.backendSrv.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: tsdbRequestData
});
return this.backendSrv.post('/api/tsdb/query', tsdbRequestData);
}
/**
* @returns {Promise<TSDBResponse>}
*/
doTSDBConnectionTest() {
/**
* @type {{ queries: ZabbixConnectionTestQuery[] }}
*/
const tsdbRequestData = {
queries: [
{
datasourceId: this.datasourceId,
queryType: 'connectionTest'
}
]
};
return this.backendSrv.post('/api/tsdb/query', tsdbRequestData);
}
/**
@@ -385,10 +416,13 @@ export class ZabbixDatasource {
/**
* Test connection to Zabbix API and external history DB.
*/
testDatasource() {
return this.zabbix.testDataSource()
.then(result => {
const { zabbixVersion, dbConnectorStatus } = result;
async testDatasource() {
try {
const result = await this.doTSDBConnectionTest();
/**
* @type {ZabbixConnectionInfo}
*/
const { zabbixVersion, dbConnectorStatus } = result.results["zabbixAPI"].meta;
let message = `Zabbix API version: ${zabbixVersion}`;
if (dbConnectorStatus) {
message += `, DB connector type: ${dbConnectorStatus.dsType}`;
@@ -398,27 +432,30 @@ export class ZabbixDatasource {
title: "Success",
message: message
};
})
.catch(error => {
}
catch (error) {
if (error instanceof ZabbixAPIError) {
return {
status: "error",
title: error.message,
message: error.message
};
} else if (error.data && error.data.message) {
}
else if (error.data && error.data.message) {
return {
status: "error",
title: "Connection failed",
message: "Connection failed: " + error.data.message
title: "Zabbix Client Error",
message: error.data.message
};
} else if (typeof(error) === 'string') {
}
else if (typeof (error) === 'string') {
return {
status: "error",
title: "Connection failed",
message: "Connection failed: " + error
title: "Unknown Error",
message: error
};
} else {
}
else {
console.log(error);
return {
status: "error",
@@ -426,7 +463,7 @@ export class ZabbixDatasource {
message: "Could not connect to given url"
};
}
});
}
}
/**

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import { ZabbixMetricsQuery } from './types';
/**
* Query format migration.
@@ -19,7 +20,7 @@ export function isGrafana2target(target) {
}
}
export function migrateFrom2To3version(target) {
export function migrateFrom2To3version(target: ZabbixMetricsQuery) {
target.group.filter = target.group.name === "*" ? "/.*/" : target.group.name;
target.host.filter = target.host.name === "*" ? convertToRegex(target.hostFilter) : target.host.name;
target.application.filter = target.application.name === "*" ? "" : target.application.name;

View File

@@ -1,4 +1,4 @@
import { loadPluginCss } from 'grafana/app/plugins/sdk';
import { loadPluginCss } from '@grafana/runtime';
import { ZabbixDatasource } from './datasource';
import { ZabbixQueryController } from './query.controller';
import { ZabbixDSConfigController } from './config.controller';

View File

@@ -0,0 +1,85 @@
import { DataQuery } from '@grafana/ui';
export interface ZabbixConnectionInfo {
zabbixVersion: string;
dbConnectorStatus: {
dsType: string;
dsName: string;
};
}
export interface ZabbixConnectionTestQuery {
datasourceId: number;
queryType: string;
}
export interface ZabbixMetricsQuery extends DataQuery {
triggers: { minSeverity: string; acknowledged: boolean; count: number; };
queryType: string;
datasourceId: number;
functions: { name: string; params: any; def: { name: string; params: any; } }[];
options: any;
textFilter: string;
mode: number;
itemids: number[];
useCaptureGroups: boolean;
group: { filter: string; name: string; };
host: { filter: string; name: string; };
hostFilter: string;
application: { filter: string; name: string; };
item: { filter: string; name: string; };
itemFilter: string;
}
// export { TemplateSrv } from 'grafana/app/features/templating/template_srv';
// export { DashboardSrv } from 'grafana/app/features/dashboard/dashboard_srv';
// The paths of these files have moved around in Grafana and they don't resolve properly
// either. Safer not to bother trying to import them just for type hinting.
export interface TemplateSrv {
variables: {
name: string;
};
highlightVariablesAsHtml(str: any): any;
replace(target: any, scopedVars?: any, format?: any): any;
}
export interface DashboardSrv {
dash: any
}
// Grafana types from backend code
type RowValues = object[];
type TimePoint = [number?, number?];
type TimeSeriesPoints = TimePoint[];
type TimeSeriesSlice = TimeSeries[];
interface TimeSeries {
name: string;
points: TimeSeriesPoints;
tags: { [key: string]: string };
}
interface TableColumn {
text: string;
}
interface Table {
columns: TableColumn[];
rows: RowValues[];
}
interface QueryResult {
error: string;
refId: string;
meta: any;
series: TimeSeriesSlice[];
tables: Table[];
}
export interface TSDBResponse {
results: { [key: string]: QueryResult };
message: string;
}

View File

@@ -110,37 +110,37 @@ export class Zabbix {
}
```
*/
testDataSource() {
let zabbixVersion;
let dbConnectorStatus;
return this.getVersion()
.then(version => {
zabbixVersion = version;
return this.login();
})
.then(() => {
if (this.enableDirectDBConnection) {
return this.dbConnector.testDataSource();
} else {
return Promise.resolve();
}
})
.catch(error => {
if (error instanceof ZabbixNotImplemented) {
return Promise.resolve();
}
return Promise.reject(error);
})
.then(testResult => {
if (testResult) {
dbConnectorStatus = {
dsType: this.dbConnector.datasourceTypeName,
dsName: this.dbConnector.datasourceName
};
}
return { zabbixVersion, dbConnectorStatus };
});
}
// testDataSource() {
// let zabbixVersion;
// let dbConnectorStatus;
// return this.getVersion()
// .then(version => {
// zabbixVersion = version;
// return this.login();
// })
// .then(() => {
// if (this.enableDirectDBConnection) {
// return this.dbConnector.testDataSource();
// } else {
// return Promise.resolve();
// }
// })
// .catch(error => {
// if (error instanceof ZabbixNotImplemented) {
// return Promise.resolve();
// }
// return Promise.reject(error);
// })
// .then(testResult => {
// if (testResult) {
// dbConnectorStatus = {
// dsType: this.dbConnector.datasourceTypeName,
// dsName: this.dbConnector.datasourceName
// };
// }
// return { zabbixVersion, dbConnectorStatus };
// });
// }
getItemsFromTarget(target, options) {
let parts = ['group', 'host', 'application', 'item'];

View File

@@ -2,9 +2,11 @@ import _ from 'lodash';
import $ from 'jquery';
import angular from 'angular';
class ZabbixAlertingService {
export class ZabbixAlertingService {
/** @ngInject */
/**
* @ngInject
*/
constructor(dashboardSrv) {
this.dashboardSrv = dashboardSrv;
}

View File

@@ -18,6 +18,14 @@ jest.mock('angular', () => {
};
}, {virtual: true});
jest.mock('grafana/app/features/templating/template_srv', () => {
return {};
}, {virtual: true});
jest.mock('grafana/app/features/dashboard/dashboard_srv', () => {
return {};
}, {virtual: true});
jest.mock('grafana/app/core/core_module', () => {
return {
directive: function() {},
@@ -28,7 +36,6 @@ let mockPanelCtrl = PanelCtrl;
jest.mock('grafana/app/plugins/sdk', () => {
return {
QueryCtrl: null,
loadPluginCss: () => {},
PanelCtrl: mockPanelCtrl
};
}, {virtual: true});
@@ -74,9 +81,9 @@ jest.mock('grafana/app/core/config', () => {
jest.mock('jquery', () => 'module not found', {virtual: true});
jest.mock('@grafana/ui', () => {
return {};
}, {virtual: true});
jest.mock('@grafana/ui');
jest.mock('@grafana/runtime');
// Required for loading angularjs
let dom = new JSDOM('<html><head><script></script></head><body></body></html>');

View File

@@ -28,7 +28,7 @@ module.exports = {
externals: [
// remove the line below if you don't want to use builtin versions
'jquery', 'lodash', 'moment', 'angular',
'react', 'react-dom', '@grafana/ui',
'react', 'react-dom', '@grafana/ui', '@grafana/runtime',
function (context, request, callback) {
var prefix = 'grafana/';
if (request.indexOf(prefix) === 0) {
@@ -63,7 +63,7 @@ module.exports = {
use: {
loader: 'babel-loader',
query: {
presets: ['babel-preset-env']
presets: ['@babel/preset-env']
}
}
},

6950
yarn.lock

File diff suppressed because it is too large Load Diff