diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c25d786..a91f8d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: workdir: ./neo4j-datasource-plugin - name: Sign plugin - run: yarn sign --rootUrls http://localhost:3000 # Remove --rootUrls if pluginType is Community + run: yarn sign env: GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} # Requires a Grafana API key from Grafana.com. diff --git a/README.md b/README.md index 2a548f1..31b334c 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,13 @@ go get -u github.com/magefile/mage go mod tidy ``` +2. Build with go: + + ```bash + go get ./... + go build ./... + ``` + 2. Build backend plugin binaries for Linux, Windows and Darwin: ```bash @@ -141,7 +148,7 @@ return datetime() - duration({minutes: 5}) as Time, 32 as Test ## Signing -Sign plugin +Sign plugin as private ```bash cd neo4j-datasource-plugin @@ -149,6 +156,14 @@ export GRAFANA_API_KEY= yarn sign --rootUrls http://localhost:3000/ ``` +Sign plugin as community + +```bash +cd neo4j-datasource-plugin +export GRAFANA_API_KEY= +yarn sign +``` + ## Learn more - [Build a data source plugin tutorial](https://grafana.com/tutorials/build-a-data-source-plugin) diff --git a/docker-compose.yaml b/docker-compose.yaml index 40a2b0a..8d23d30 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,7 +6,7 @@ services: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=admin - GF_FEATURE_TOGGLES_ENABLE=ngalert - - GF_INSTALL_PLUGINS=https://github.com/denniskniep/grafana-datasource-plugin-neo4j/releases/download/v1.1.0-beta/kniepdennis-neo4j-datasource-1.1.0-beta.zip;kniepdennis-neo4j-datasource + - GF_INSTALL_PLUGINS=https://github.com/denniskniep/grafana-datasource-plugin-neo4j/releases/download/v1.1.0/kniepdennis-neo4j-datasource-1.1.0.zip;kniepdennis-neo4j-datasource volumes: - ./grafana/provisioning/:/etc/grafana/provisioning/ ports: diff --git a/neo4j-datasource-plugin/CHANGELOG.md b/neo4j-datasource-plugin/CHANGELOG.md index 6fc5676..ce03c97 100644 --- a/neo4j-datasource-plugin/CHANGELOG.md +++ b/neo4j-datasource-plugin/CHANGELOG.md @@ -5,10 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - YYYY-MM-DD + +## [1.1.0] - 2022-02-28 ### Changed - Use official name Neo4j instead of Neo4J - Use neo4j logo with blue background to support both dark and light theme. Logo was barely visible with the light theme. - Catch errors with connection details and prevent errors with internal network informations +- Caching Neo4j driver in the backend ## [1.1.0-beta] ### Added diff --git a/neo4j-datasource-plugin/go.mod b/neo4j-datasource-plugin/go.mod index aaf1307..7f8af79 100644 --- a/neo4j-datasource-plugin/go.mod +++ b/neo4j-datasource-plugin/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/google/go-cmp v0.5.6 + github.com/google/uuid v1.3.0 github.com/grafana/grafana-plugin-sdk-go v0.121.0 github.com/magefile/mage v1.12.1 github.com/neo4j/neo4j-go-driver/v4 v4.4.0 diff --git a/neo4j-datasource-plugin/go.sum b/neo4j-datasource-plugin/go.sum index 84739d3..7309349 100644 --- a/neo4j-datasource-plugin/go.sum +++ b/neo4j-datasource-plugin/go.sum @@ -141,6 +141,8 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/grafana/grafana-plugin-sdk-go v0.121.0 h1:4+dXoezL9L40iu7ym4u7ZJ4OE57NaVc4WSHlbxtCtGM= diff --git a/neo4j-datasource-plugin/package.json b/neo4j-datasource-plugin/package.json index 371cd42..8ce3141 100644 --- a/neo4j-datasource-plugin/package.json +++ b/neo4j-datasource-plugin/package.json @@ -1,6 +1,6 @@ { "name": "kniepdennis-neo4j-datasource", - "version": "1.1.0-beta", + "version": "1.1.0", "description": "Neo4j Datasource", "scripts": { "build": "grafana-toolkit plugin:build", diff --git a/neo4j-datasource-plugin/pkg/main.go b/neo4j-datasource-plugin/pkg/main.go index 1ddfe02..69c8449 100644 --- a/neo4j-datasource-plugin/pkg/main.go +++ b/neo4j-datasource-plugin/pkg/main.go @@ -18,7 +18,7 @@ func main() { // ID). When datasource configuration changed Dispose method will be called and // new datasource instance created using NewSampleDatasource factory. log.DefaultLogger.Info("Starting neo4j-datasource plugin") - if err := datasource.Manage("kniepdennis-neo4j-datasource", plugin.NewSampleDatasource, datasource.ManageOpts{}); err != nil { + if err := datasource.Manage("kniepdennis-neo4j-datasource", plugin.NewNeo4JDatasource, datasource.ManageOpts{}); err != nil { log.DefaultLogger.Error(err.Error()) os.Exit(1) } diff --git a/neo4j-datasource-plugin/pkg/plugin/plugin.go b/neo4j-datasource-plugin/pkg/plugin/plugin.go index 371b2c7..d742d82 100644 --- a/neo4j-datasource-plugin/pkg/plugin/plugin.go +++ b/neo4j-datasource-plugin/pkg/plugin/plugin.go @@ -3,8 +3,10 @@ package plugin import ( "context" "encoding/json" + "errors" "time" + "github.com/google/uuid" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/log" @@ -13,53 +15,78 @@ import ( "github.com/neo4j/neo4j-go-driver/v4/neo4j/dbtype" ) -// Make sure SampleDatasource implements required interfaces. This is important to do +// Datasource must implement required interfaces. This is important to do // since otherwise we will only get a not implemented error response from plugin in -// runtime. In this example datasource instance implements backend.QueryDataHandler, -// backend.CheckHealthHandler. Plugin should not -// implement all these interfaces - only those which are required for a particular task. -// For example if plugin does not need streaming functionality then you are free to remove -// methods that implement backend.StreamHandler. Implementing instancemgmt.InstanceDisposer +// runtime. Datasource instance implements backend.QueryDataHandler, +// backend.CheckHealthHandler.Implementing instancemgmt.InstanceDisposer // is useful to clean up resources used by previous datasource instance when a new datasource // instance created upon datasource settings changed. var ( - _ backend.QueryDataHandler = (*SampleDatasource)(nil) - _ backend.CheckHealthHandler = (*SampleDatasource)(nil) + _ backend.QueryDataHandler = (*Neo4JDatasource)(nil) + _ backend.CheckHealthHandler = (*Neo4JDatasource)(nil) _ backend.DataSourceInstanceSettings - _ instancemgmt.InstanceDisposer = (*SampleDatasource)(nil) + _ instancemgmt.InstanceDisposer = (*Neo4JDatasource)(nil) ) -// NewSampleDatasource creates a new datasource instance. -func NewSampleDatasource(_ backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return &SampleDatasource{}, nil +const ( + DATASOURCE_UID string = "DATASOURCE_UID" + ERROR string = "err" +) + +// datasource which can respond to data queries and reports its health. +type Neo4JDatasource struct { + id string + settings neo4JSettings + driver neo4j.Driver } -// SampleDatasource is an example datasource which can respond to data queries, reports -// its health and has streaming skills. -type SampleDatasource struct{} +// creates a new datasource instance. +func NewNeo4JDatasource(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + id := uuid.New().String() + log.DefaultLogger.Info("Create Datasource", DATASOURCE_UID, id) + neo4JSettings, err := unmarshalDataSourceSettings(settings) + if err != nil { + errorMsg := "can not deserialize DataSource settings" + log.DefaultLogger.Error(errorMsg, ERROR, err.Error()) + return nil, errors.New(errorMsg) + } + + authToken := neo4j.NoAuth() + if neo4JSettings.Username != "" && neo4JSettings.Password != "" { + authToken = neo4j.BasicAuth(neo4JSettings.Username, neo4JSettings.Password, "") + } + + driver, err := neo4j.NewDriver(neo4JSettings.Url, authToken) + if err != nil { + return nil, err + } + + return &Neo4JDatasource{ + id: id, + settings: neo4JSettings, + driver: driver, + }, nil +} // Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance // created. As soon as datasource settings change detected by SDK old datasource instance will -// be disposed and a new one will be created using NewSampleDatasource factory function. -func (d *SampleDatasource) Dispose() { +// be disposed and a new one will be created using factory function. +func (d *Neo4JDatasource) Dispose() { // Clean up datasource instance resources. + log.DefaultLogger.Info("Dispose Datasource", DATASOURCE_UID, d.id) + defer d.driver.Close() } // QueryData handles multiple queries and returns multiple responses. // req contains the queries []DataQuery (where each query contains RefID as a unique identifier). // The QueryDataResponse contains a map of RefID to the response for each query, and each response // contains Frames ([]*Frame). -func (d *SampleDatasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - log.DefaultLogger.Info("QueryData called") +func (d *Neo4JDatasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + log.DefaultLogger.Info("QueryData called", DATASOURCE_UID, d.id) // create response struct response := backend.NewQueryDataResponse() - neo4JSettings, err := unmarshalDataSourceSettings(req.PluginContext.DataSourceInstanceSettings) - if err != nil { - return response, err - } - // loop over queries and execute them individually. for _, q := range req.Queries { @@ -80,13 +107,13 @@ func (d *SampleDatasource) QueryData(ctx context.Context, req *backend.QueryData neo4JQuery.MaxDataPoints = q.MaxDataPoints neo4JQuery.TimeRange = q.TimeRange - res, err = query(neo4JSettings, neo4JQuery) + res, err = d.query(neo4JQuery) if err != nil { res.Error = err } if res.Error != nil { - log.DefaultLogger.Error("Error in query", res.Error) + log.DefaultLogger.Error("Error in query", ERROR, res.Error) } response.Responses[q.RefID] = res @@ -95,28 +122,27 @@ func (d *SampleDatasource) QueryData(ctx context.Context, req *backend.QueryData return response, nil } -func query(settings neo4JSettings, query neo4JQuery) (backend.DataResponse, error) { - log.DefaultLogger.Info("Execute Cypher Query: '" + query.CypherQuery + "'") +func (d *Neo4JDatasource) query(query neo4JQuery) (backend.DataResponse, error) { + log.DefaultLogger.Info("Execute Cypher Query: '"+query.CypherQuery+"'", DATASOURCE_UID, d.id) response := backend.DataResponse{} - authToken := neo4j.NoAuth() - if settings.Username != "" && settings.Password != "" { - authToken = neo4j.BasicAuth(settings.Username, settings.Password, "") - } - - driver, err := neo4j.NewDriver(settings.Url, authToken) - if err != nil { - return response, err - } - defer driver.Close() - - session := driver.NewSession(neo4j.SessionConfig{DatabaseName: settings.Database, AccessMode: neo4j.AccessModeRead}) + session := d.driver.NewSession(neo4j.SessionConfig{DatabaseName: d.settings.Database, AccessMode: neo4j.AccessModeRead}) defer session.Close() result, err := session.Run(query.CypherQuery, map[string]interface{}{}) + if err != nil { - return response, err + errMsg := "InternalError!" + switch err.(type) { + default: + return response, err + case *neo4j.ConnectivityError: + errMsg = "ConnectivityError: Can not connect to specified url." + } + + log.DefaultLogger.Error(errMsg, ERROR, err.Error()) + return response, errors.New(errMsg + " Please review log for more details.") } return toDataResponse(result) @@ -174,48 +200,23 @@ func toDataResponse(result neo4j.Result) (backend.DataResponse, error) { // The main use case for these health checks is the test button on the // datasource configuration page which allows users to verify that // a datasource is working as expected. -func (d *SampleDatasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - return checkHealth(req.PluginContext.DataSourceInstanceSettings) +func (d *Neo4JDatasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + return d.checkHealth() } -func checkHealth(dataSourceInstanceSettings *backend.DataSourceInstanceSettings) (*backend.CheckHealthResult, error) { - log.DefaultLogger.Info("CheckHealth called") - - settings, err := unmarshalDataSourceSettings(dataSourceInstanceSettings) - - if err != nil { - errorMsg := "Can not deserialize DataSource settings" - log.DefaultLogger.Error(errorMsg, err.Error()) - return &backend.CheckHealthResult{ - Status: backend.HealthStatusError, - Message: errorMsg, - }, nil - } +func (d *Neo4JDatasource) checkHealth() (*backend.CheckHealthResult, error) { + log.DefaultLogger.Info("CheckHealth called", DATASOURCE_UID, d.id) neo4JQuery := neo4JQuery{ CypherQuery: "Match(a) return a limit 1", } - _, err = query(settings, neo4JQuery) + _, err := d.query(neo4JQuery) if err != nil { - errMsg := "Error occured while connecting to Neo4j!" - log.DefaultLogger.Error(errMsg, err.Error()) - - switch t := err.(type) { - case *neo4j.ConnectivityError: - errMsg = "ConnectivityError: Can not connect to specified url" - case *neo4j.UsageError: - errMsg = t.Message - case *neo4j.TokenExpiredError: - errMsg = t.Message - case *neo4j.Neo4jError: - errMsg = t.Msg - } - return &backend.CheckHealthResult{ Status: backend.HealthStatusError, - Message: errMsg, + Message: err.Error(), }, nil } @@ -225,7 +226,7 @@ func checkHealth(dataSourceInstanceSettings *backend.DataSourceInstanceSettings) }, nil } -func unmarshalDataSourceSettings(dSIset *backend.DataSourceInstanceSettings) (neo4JSettings, error) { +func unmarshalDataSourceSettings(dSIset backend.DataSourceInstanceSettings) (neo4JSettings, error) { // Unmarshal the JSON into our settings Model. var neo4JSettings neo4JSettings err := json.Unmarshal(dSIset.JSONData, &neo4JSettings) @@ -297,7 +298,7 @@ func toValue(val interface{}) interface{} { default: r, err := json.Marshal(val) if err != nil { - log.DefaultLogger.Info("Marshalling failed ", "err", err) + log.DefaultLogger.Info("Marshalling failed ", ERROR, err) } val := string(r) return &val diff --git a/neo4j-datasource-plugin/pkg/plugin/plugin_test.go b/neo4j-datasource-plugin/pkg/plugin/plugin_test.go index 5944071..4594afa 100644 --- a/neo4j-datasource-plugin/pkg/plugin/plugin_test.go +++ b/neo4j-datasource-plugin/pkg/plugin/plugin_test.go @@ -45,11 +45,15 @@ func TestHealthcheckIsErrorDueToInvalidHost(t *testing.T) { func TestHealthcheckIsErrorDueToDeserialize(t *testing.T) { skipIfIsShort(t) - settings := &backend.DataSourceInstanceSettings{} - settings.JSONData = []byte { 1 } + settings := backend.DataSourceInstanceSettings{} + settings.JSONData = []byte{1} - const ERROR_STATUS backend.HealthStatus = 2 - testCheckHealthAndMessageWithSettings(t, settings, ERROR_STATUS, "Can not deserialize DataSource settings") + _, err := NewNeo4JDatasource(settings) + + expectedMsg := "can not deserialize DataSource settings" + if !strings.Contains(err.Error(), expectedMsg) { + t.Error("Expected Message " + expectedMsg + ", but was " + err.Error()) + } } func TestHealthcheckIsErrorDueToInvalidPort(t *testing.T) { @@ -122,14 +126,21 @@ func testCheckHealth(t *testing.T, neo4JSettings neo4JSettings, expectedStatus b } func testCheckHealthAndMessage(t *testing.T, neo4JSettings neo4JSettings, expectedStatus backend.HealthStatus, expectedMessagePart string) { - settings := &backend.DataSourceInstanceSettings{} + settings := backend.DataSourceInstanceSettings{} settings.JSONData = asJsonBytes(t, neo4JSettings) testCheckHealthAndMessageWithSettings(t, settings, expectedStatus, expectedMessagePart) } -func testCheckHealthAndMessageWithSettings(t *testing.T, settings *backend.DataSourceInstanceSettings, expectedStatus backend.HealthStatus, expectedMessagePart string) { - res, err := checkHealth(settings) +func testCheckHealthAndMessageWithSettings(t *testing.T, settings backend.DataSourceInstanceSettings, expectedStatus backend.HealthStatus, expectedMessagePart string) { + + instance, err := NewNeo4JDatasource(settings) + if err != nil { + t.Fatal(err) + } + + neo4JDatasource := instance.(*Neo4JDatasource) + res, err := neo4JDatasource.checkHealth() if err != nil { t.Fatal(err) @@ -428,7 +439,13 @@ func runNeo4JIntegrationTest(t *testing.T, cypher string, expected *data.Frame) CypherQuery: cypher, } - res, err := query(neo4JSettings, neo4JQuery) + settings := backend.DataSourceInstanceSettings{} + settings.JSONData = asJsonBytes(t, neo4JSettings) + + instance, _ := NewNeo4JDatasource(settings) + neo4JDatasource := instance.(*Neo4JDatasource) + + res, err := neo4JDatasource.query(neo4JQuery) if err != nil { t.Fatal(err) } diff --git a/neo4j-datasource-plugin/src/plugin.json b/neo4j-datasource-plugin/src/plugin.json index dd38125..e74ebf9 100644 --- a/neo4j-datasource-plugin/src/plugin.json +++ b/neo4j-datasource-plugin/src/plugin.json @@ -28,7 +28,16 @@ "url": "https://github.com/denniskniep/grafana-datasource-plugin-neo4j/blob/main/LICENSE" } ], - "screenshots": [], + "screenshots":[ + { + "name": "DataSource Config Editor", + "path": "https://raw.githubusercontent.com/denniskniep/grafana-datasource-plugin-neo4j/main/images/DataSourceConfigEditor.png" + }, + { + "name": "DataSource Query Editor", + "path": "https://raw.githubusercontent.com/denniskniep/grafana-datasource-plugin-neo4j/main/images/DataSourceQueryEditor.png" + } + ], "version": "%VERSION%", "updated": "%TODAY%" },