Skip to content

Commit

Permalink
Caching Neo4j driver in the backend;Added Screenshots
Browse files Browse the repository at this point in the history
  • Loading branch information
denniskniep committed Feb 28, 2022
1 parent 7f8cc6f commit 9806962
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 88 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -141,14 +148,22 @@ return datetime() - duration({minutes: 5}) as Time, 32 as Test

## Signing

Sign plugin
Sign plugin as private

```bash
cd neo4j-datasource-plugin
export GRAFANA_API_KEY=<GRAFANA_API_KEY>
yarn sign --rootUrls http://localhost:3000/
```

Sign plugin as community

```bash
cd neo4j-datasource-plugin
export GRAFANA_API_KEY=<GRAFANA_API_KEY>
yarn sign
```


## Learn more
- [Build a data source plugin tutorial](https://grafana.com/tutorials/build-a-data-source-plugin)
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions neo4j-datasource-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions neo4j-datasource-plugin/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions neo4j-datasource-plugin/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
2 changes: 1 addition & 1 deletion neo4j-datasource-plugin/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion neo4j-datasource-plugin/pkg/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
149 changes: 75 additions & 74 deletions neo4j-datasource-plugin/pkg/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {

Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
}

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 9806962

Please sign in to comment.