diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 09d9c493d1..57271e4de2 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -140,6 +140,15 @@ var ( utils.WhisperMaxMessageSizeFlag, utils.WhisperMinPOWFlag, } + + metricsFlags = []cli.Flag{ + utils.MetricsEnableInfluxDBExportFlag, + utils.MetricsInfluxDBEndpointFlag, + utils.MetricsInfluxDBDatabaseFlag, + utils.MetricsInfluxDBUsernameFlag, + utils.MetricsInfluxDBPasswordFlag, + utils.MetricsInfluxDBHostTagFlag, + } ) func init() { @@ -182,12 +191,17 @@ func init() { app.Flags = append(app.Flags, consoleFlags...) app.Flags = append(app.Flags, debug.Flags...) app.Flags = append(app.Flags, whisperFlags...) + app.Flags = append(app.Flags, metricsFlags...) app.Before = func(ctx *cli.Context) error { runtime.GOMAXPROCS(runtime.NumCPU()) if err := debug.Setup(ctx); err != nil { return err } + + // Start metrics export if enabled + utils.SetupMetrics(ctx) + // Start system runtime metrics collection go metrics.CollectProcessMetrics(3 * time.Second) diff --git a/cmd/stateth/grafana_dashboards/geth.json b/cmd/stateth/grafana_dashboards/geth.json new file mode 100644 index 0000000000..6e269e7f4d --- /dev/null +++ b/cmd/stateth/grafana_dashboards/geth.json @@ -0,0 +1,231 @@ +{ + "annotations": { + "list": [ + { + "$$hashKey": "object:452", + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 1, + "id": 1, + "iteration": 1526677592883, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 51, + "panels": [], + "title": "Geth", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": null, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 4, + "w": 5, + "x": 0, + "y": 1 + }, + "id": 53, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "$$hashKey": "object:731", + "name": "value to text", + "value": 1 + }, + { + "$$hashKey": "object:732", + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "$$hashKey": "object:668", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "measurement": "geth.currentBlock.gauge", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "last" + } + ] + ], + "tags": [] + } + ], + "thresholds": "", + "title": "Current block number", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "$$hashKey": "object:734", + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + ], + "refresh": "5s", + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "10s", + "value": "10s" + }, + "hide": 0, + "label": "resolution", + "name": "myinterval", + "options": [ + { + "selected": false, + "text": "5s", + "value": "5s" + }, + { + "selected": true, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "100s", + "value": "100s" + } + ], + "query": "5s,10s,30s,100s", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Geth", + "uid": "dUpKvj7mz", + "version": 7 +} + + diff --git a/cmd/stateth/stateth.go b/cmd/stateth/stateth.go new file mode 100644 index 0000000000..0d3d6e9dc2 --- /dev/null +++ b/cmd/stateth/stateth.go @@ -0,0 +1,287 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +// puppeth is a command to assemble and maintain private networks. +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/ethereum/go-ethereum/log" + gapi "github.com/teemupo/go-grafana-api" + "gopkg.in/urfave/cli.v1" +) + +var ( + dockerPrefix string // unique prefix used for the created docker resources + dashboardsFolder string // folder containing all dashboards to be imported in Grafana + grafanaPort int // expose port for the Grafana HTTP interface + influxdbPort int // expose port for the InfluxDB HTTP interface +) + +const ( + influxdbAdminUser = "test" // admin username for InfluxDB + influxdbAdminPass = "test" // admin password for InfluxDB + grafanaUser = "admin" // default Grafana username - should not be changed here without first updating the docker image + grafanaPass = "admin" // default Grafana password - should not be changed here without first udpating the docker image +) + +func main() { + app := cli.NewApp() + app.Name = "stateth" + app.Usage = "run a local grafana/influxdb setup for local Geth node stats visualization" + app.Version = "0.0.1" + app.Flags = []cli.Flag{ + cli.IntFlag{ + Name: "loglevel", + Value: 3, + Usage: "log level to emit to the screen", + }, + cli.IntFlag{ + Name: "influxdb-http-port", + Value: 8086, + Usage: "default influxdb http port", + }, + cli.IntFlag{ + Name: "grafana-http-port", + Value: 3000, + Usage: "default grafana http port", + }, + cli.StringFlag{ + Name: "grafana-dashboards-folder", + Value: os.Getenv("GOPATH") + "/src/github.com/ethereum/go-ethereum/cmd/stateth/grafana_dashboards", + Usage: "default grafana dashboards folder", + }, + cli.StringFlag{ + Name: "docker-prefix", + Value: "stateth", + Usage: "prefix to be used for docker network and containers. must be unique.", + }, + } + app.Action = func(c *cli.Context) error { + log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(os.Stdout, log.TerminalFormat(true)))) + + dockerPrefix = c.String("docker-prefix") + grafanaPort = c.Int("grafana-http-port") + influxdbPort = c.Int("influxdb-http-port") + dashboardsFolder = c.String("grafana-dashboards-folder") + + if err := runNetwork(c); err != nil { + return err + } + if err := runInfluxDB(c); err != nil { + return err + } + if err := runGrafana(c); err != nil { + return err + } + log.Info("waiting for grafana to boot up...") + time.Sleep(7 * time.Second) // give time to Grafana to boot up + if err := importGrafanaDatasource(c); err != nil { + return err + } + if err := importGrafanaDashboards(c); err != nil { + return err + } + + sigs := make(chan os.Signal, 1) + done := make(chan bool, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + go func() { + sig := <-sigs + fmt.Println() + fmt.Println(sig) + done <- true + }() + + fmt.Println(fmt.Sprintf("grafana listening on http://localhost:%d", grafanaPort)) + fmt.Println(fmt.Sprintf("username: %s", grafanaUser)) + fmt.Println(fmt.Sprintf("password: %s", grafanaPass)) + fmt.Println() + fmt.Println("waiting for SIGINT or SIGTERM (CTRL^C) to stop service and remove containers...") + <-done + + return cleanupContainers(c) + } + + app.Run(os.Args) +} + +func runNetwork(c *cli.Context) error { + log.Info("creating docker network", "network", dockerPrefix) + command := strings.Split(fmt.Sprintf("docker network create %s", dockerPrefix), " ") + r, err := exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Error(string(r)) + return err + } + return nil +} + +func runInfluxDB(c *cli.Context) error { + log.Info("pulling influxdb:1.5.2 docker image") + command := strings.Split("docker pull influxdb:1.5.2", " ") + r, err := exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Error(err.Error()) + return err + } + + log.Info("running influxdb docker container", "container", fmt.Sprintf("%s_influxdb", dockerPrefix)) + command = strings.Split(fmt.Sprintf("docker run --network %s --name %s_influxdb -e INFLUXDB_DB=metrics -e INFLUXDB_ADMIN_USER=%s -e INFLUXDB_ADMIN_PASSWORD=%s -p %d:8086 -d influxdb:1.5.2", dockerPrefix, dockerPrefix, influxdbAdminUser, influxdbAdminPass, influxdbPort), " ") + r, err = exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Error(string(r)) + return err + } + return nil +} + +func runGrafana(c *cli.Context) error { + log.Info("pulling grafana/grafana:5.1.3 docker image") + command := strings.Split("docker pull grafana/grafana:5.1.3", " ") + r, err := exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Error(string(r)) + return err + } + + log.Info("running grafana docker container", "container", fmt.Sprintf("%s_grafana", dockerPrefix)) + command = strings.Split(fmt.Sprintf("docker run --network %s --name=%s_grafana -p %d:3000 -d grafana/grafana:5.1.3", dockerPrefix, dockerPrefix, grafanaPort), " ") + r, err = exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Error(string(r)) + return err + } + return nil +} + +func cleanupContainers(c *cli.Context) error { + log.Info("removing influxdb container") + command := strings.Split(fmt.Sprintf("docker rm -f %s_influxdb", dockerPrefix), " ") + r, err := exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Warn(string(r)) + } + + log.Info("removing grafana container") + command = strings.Split(fmt.Sprintf("docker rm -f %s_grafana", dockerPrefix), " ") + r, err = exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Warn(string(r)) + } + + log.Info("removing network") + command = strings.Split(fmt.Sprintf("docker network rm %s", dockerPrefix), " ") + r, err = exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Warn(string(r)) + } + + return nil +} + +func importGrafanaDatasource(c *cli.Context) error { + log.Info("importing grafana datasource") + gclient, err := gapi.New(fmt.Sprintf("%s:%s", grafanaUser, grafanaPass), fmt.Sprintf("http://localhost:%d", grafanaPort)) + if err != nil { + log.Warn(err.Error()) + return nil + } + + dataSource := &gapi.DataSource{ + Name: "metrics", + Type: "influxdb", + URL: fmt.Sprintf("http://%s_influxdb:%d", dockerPrefix, influxdbPort), + Access: "proxy", + Database: "metrics", + User: influxdbAdminUser, + Password: influxdbAdminPass, + IsDefault: true, + BasicAuth: false, + } + + _, err = gclient.NewDataSource(dataSource) + if err != nil { + log.Warn(err.Error()) + return err + } + + return nil +} + +func importGrafanaDashboards(c *cli.Context) error { + log.Info("importing grafana dashboards") + gclient, err := gapi.New(fmt.Sprintf("%s:%s", grafanaUser, grafanaPass), fmt.Sprintf("http://localhost:%d", grafanaPort)) + if err != nil { + log.Warn(err.Error()) + return nil + } + + files, err := ioutil.ReadDir(dashboardsFolder) + if err != nil { + log.Warn(err.Error()) + return nil + } + + for _, f := range files { + name := f.Name() + if strings.Contains(name, "json") { + log.Info("importing dashboard", "dashboard", name) + + blob, err := ioutil.ReadFile(dashboardsFolder + "/" + name) + if err != nil { + log.Warn(err.Error()) + return nil + } + + model := prepareDashboardModel(string(blob)) + + _, err = gclient.SaveDashboard(model, false) + if err != nil { + log.Warn(err.Error()) + return nil + } + + } + } + + return nil +} + +func prepareDashboardModel(configJSON string) map[string]interface{} { + configMap := map[string]interface{}{} + err := json.Unmarshal([]byte(configJSON), &configMap) + if err != nil { + panic("invalid JSON got into prepare func") + } + + delete(configMap, "id") + // Only exists in 5.0+ + delete(configMap, "uid") + configMap["version"] = 0 + + return configMap +} diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index ef5f6a9f08..f22fb5620b 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -27,6 +27,7 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -48,6 +49,7 @@ import ( "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/metrics/influxdb" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/discover" @@ -532,6 +534,41 @@ var ( Usage: "Minimum POW accepted", Value: whisper.DefaultMinimumPoW, } + + // Metrics flags + MetricsEnableInfluxDBExportFlag = cli.BoolFlag{ + Name: "metrics.influxdb.export", + Usage: "Enable metrics export/push to an external InfluxDB database", + } + MetricsInfluxDBEndpointFlag = cli.StringFlag{ + Name: "metrics.influxdb.endpoint", + Usage: "Metrics InfluxDB endpoint", + Value: "http://localhost:8086", + } + MetricsInfluxDBDatabaseFlag = cli.StringFlag{ + Name: "metrics.influxdb.database", + Usage: "Metrics InfluxDB database", + Value: "metrics", + } + MetricsInfluxDBUsernameFlag = cli.StringFlag{ + Name: "metrics.influxdb.username", + Usage: "Metrics InfluxDB username", + Value: "test", + } + MetricsInfluxDBPasswordFlag = cli.StringFlag{ + Name: "metrics.influxdb.password", + Usage: "Metrics InfluxDB password", + Value: "test", + } + // The `host` tag is part of every measurement sent to InfluxDB. Queries on tags are faster in InfluxDB. + // It is used so that we can group all nodes and average a measurement across all of them, but also so + // that we can select a specific node and inspect its measurements. + // https://docs.influxdata.com/influxdb/v1.4/concepts/key_concepts/#tag-key + MetricsInfluxDBHostTagFlag = cli.StringFlag{ + Name: "metrics.influxdb.host.tag", + Usage: "Metrics InfluxDB `host` tag attached to all measurements", + Value: "localhost", + } ) // MakeDataDir retrieves the currently requested data directory, terminating @@ -1145,7 +1182,14 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) { // RegisterDashboardService adds a dashboard to the stack. func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) { stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { - return dashboard.New(cfg, commit) + // Retrieve both eth and les services + var ethServ *eth.Ethereum + ctx.Service(ðServ) + + var lesServ *les.LightEthereum + ctx.Service(&lesServ) + + return dashboard.New(cfg, commit, ethServ, lesServ) }) } @@ -1181,6 +1225,27 @@ func SetupNetwork(ctx *cli.Context) { params.TargetGasLimit = ctx.GlobalUint64(TargetGasLimitFlag.Name) } +func SetupMetrics(ctx *cli.Context) { + if metrics.Enabled { + log.Info("Enabling metrics collection") + var ( + enableExport = ctx.GlobalBool(MetricsEnableInfluxDBExportFlag.Name) + endpoint = ctx.GlobalString(MetricsInfluxDBEndpointFlag.Name) + database = ctx.GlobalString(MetricsInfluxDBDatabaseFlag.Name) + username = ctx.GlobalString(MetricsInfluxDBUsernameFlag.Name) + password = ctx.GlobalString(MetricsInfluxDBPasswordFlag.Name) + hosttag = ctx.GlobalString(MetricsInfluxDBHostTagFlag.Name) + ) + + if enableExport { + log.Info("Enabling metrics export to InfluxDB") + go influxdb.InfluxDBWithTags(metrics.DefaultRegistry, 10*time.Second, endpoint, database, username, password, "geth.", map[string]string{ + "host": hosttag, + }) + } + } +} + // MakeChainDatabase open an LevelDB using the flags passed to the client and will hard crash if it fails. func MakeChainDatabase(ctx *cli.Context, stack *node.Node) ethdb.Database { var ( diff --git a/dashboard/dashboard.go b/dashboard/dashboard.go index 399fa34c08..249ac03a3d 100644 --- a/dashboard/dashboard.go +++ b/dashboard/dashboard.go @@ -33,6 +33,8 @@ import ( "time" "github.com/elastic/gosigar" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/p2p" @@ -64,6 +66,9 @@ type Dashboard struct { commit string lock sync.RWMutex // Lock protecting the dashboard's internals + ethServ *eth.Ethereum + lesServ *les.LightEthereum + quit chan chan error // Channel used for graceful exit wg sync.WaitGroup } @@ -76,7 +81,7 @@ type client struct { } // New creates a new dashboard instance with the given configuration. -func New(config *Config, commit string) (*Dashboard, error) { +func New(config *Config, commit string, ethServ *eth.Ethereum, lesServ *les.LightEthereum) (*Dashboard, error) { now := time.Now() db := &Dashboard{ conns: make(map[uint32]*client), @@ -92,7 +97,9 @@ func New(config *Config, commit string) (*Dashboard, error) { DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh), DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh), }, - commit: commit, + commit: commit, + ethServ: ethServ, + lesServ: lesServ, } return db, nil } @@ -306,6 +313,15 @@ func (db *Dashboard) collectData() { prevDiskRead = curDiskRead prevDiskWrite = curDiskWrite + // extract measurements from eth.Downloader and push to metrics registry + p := db.ethServ.Downloader().Progress() + + metrics.GetOrRegisterGauge("currentBlock", nil).Update(int64(p.CurrentBlock)) + metrics.GetOrRegisterGauge("startingBlock", nil).Update(int64(p.StartingBlock)) + metrics.GetOrRegisterGauge("highestBlock", nil).Update(int64(p.HighestBlock)) + metrics.GetOrRegisterGauge("pulledStates", nil).Update(int64(p.PulledStates)) + metrics.GetOrRegisterGauge("knownStates", nil).Update(int64(p.KnownStates)) + now := time.Now() runtime.ReadMemStats(&mem) diff --git a/vendor/github.com/teemupo/go-grafana-api/LICENSE b/vendor/github.com/teemupo/go-grafana-api/LICENSE new file mode 100644 index 0000000000..8f71f43fee --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/github.com/teemupo/go-grafana-api/README.md b/vendor/github.com/teemupo/go-grafana-api/README.md new file mode 100644 index 0000000000..581af01ddb --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/README.md @@ -0,0 +1,11 @@ +# grafana-api-golang-client + +Grafana HTTP API Client for Go + +## Tests + +To run the tests: + +``` +go test +``` diff --git a/vendor/github.com/teemupo/go-grafana-api/README_ethereum.md b/vendor/github.com/teemupo/go-grafana-api/README_ethereum.md new file mode 100644 index 0000000000..927a077acb --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/README_ethereum.md @@ -0,0 +1 @@ +Removed Client.CreateUserForm due to "github.com/grafana/grafana/pkg/api/dtos" dependency. diff --git a/vendor/github.com/teemupo/go-grafana-api/admin.go b/vendor/github.com/teemupo/go-grafana-api/admin.go new file mode 100644 index 0000000000..50557bc5ee --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/admin.go @@ -0,0 +1,21 @@ +package gapi + +import ( + "errors" + "fmt" +) + +func (c *Client) DeleteUser(id int64) error { + req, err := c.newRequest("DELETE", fmt.Sprintf("/api/admin/users/%d", id), nil) + if err != nil { + return err + } + resp, err := c.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + return err +} diff --git a/vendor/github.com/teemupo/go-grafana-api/alertnotification.go b/vendor/github.com/teemupo/go-grafana-api/alertnotification.go new file mode 100644 index 0000000000..39a32b9f40 --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/alertnotification.go @@ -0,0 +1,112 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" +) + +type AlertNotification struct { + Id int64 `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + Settings interface{} `json:"settings"` +} + +func (c *Client) AlertNotification(id int64) (*AlertNotification, error) { + path := fmt.Sprintf("/api/alert-notifications/%d", id) + req, err := c.newRequest("GET", path, nil) + if err != nil { + return nil, err + } + + resp, err := c.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + return nil, errors.New(resp.Status) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + result := &AlertNotification{} + err = json.Unmarshal(data, &result) + return result, err +} + +func (c *Client) NewAlertNotification(a *AlertNotification) (int64, error) { + data, err := json.Marshal(a) + if err != nil { + return 0, err + } + req, err := c.newRequest("POST", "/api/alert-notifications", bytes.NewBuffer(data)) + if err != nil { + return 0, err + } + + resp, err := c.Do(req) + if err != nil { + return 0, err + } + if resp.StatusCode != 200 { + return 0, errors.New(resp.Status) + } + + data, err = ioutil.ReadAll(resp.Body) + if err != nil { + return 0, err + } + + result := struct { + Id int64 `json:"id"` + }{} + err = json.Unmarshal(data, &result) + return result.Id, err +} + +func (c *Client) UpdateAlertNotification(a *AlertNotification) error { + path := fmt.Sprintf("/api/alert-notifications/%d", a.Id) + data, err := json.Marshal(a) + if err != nil { + return err + } + req, err := c.newRequest("PUT", path, bytes.NewBuffer(data)) + if err != nil { + return err + } + + resp, err := c.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + + return nil +} + +func (c *Client) DeleteAlertNotification(id int64) error { + path := fmt.Sprintf("/api/alert-notifications/%d", id) + req, err := c.newRequest("DELETE", path, nil) + if err != nil { + return err + } + + resp, err := c.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + + return nil +} diff --git a/vendor/github.com/teemupo/go-grafana-api/client.go b/vendor/github.com/teemupo/go-grafana-api/client.go new file mode 100644 index 0000000000..d92ba883a8 --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/client.go @@ -0,0 +1,63 @@ +package gapi + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "path" + "strings" +) + +type Client struct { + key string + baseURL url.URL + *http.Client +} + +//New creates a new grafana client +//auth can be in user:pass format, or it can be an api key +func New(auth, baseURL string) (*Client, error) { + u, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + key := "" + if strings.Contains(auth, ":") { + split := strings.Split(auth, ":") + u.User = url.UserPassword(split[0], split[1]) + } else { + key = fmt.Sprintf("Bearer %s", auth) + } + return &Client{ + key, + *u, + &http.Client{}, + }, nil +} + +func (c *Client) newRequest(method, requestPath string, body io.Reader) (*http.Request, error) { + url := c.baseURL + url.Path = path.Join(url.Path, requestPath) + req, err := http.NewRequest(method, url.String(), body) + if err != nil { + return req, err + } + if c.key != "" { + req.Header.Add("Authorization", c.key) + } + + if os.Getenv("GF_LOG") != "" { + if body == nil { + log.Println("request to ", url.String(), "with no body data") + } else { + log.Println("request to ", url.String(), "with body data", body.(*bytes.Buffer).String()) + } + } + + req.Header.Add("Content-Type", "application/json") + return req, err +} diff --git a/vendor/github.com/teemupo/go-grafana-api/dashboard.go b/vendor/github.com/teemupo/go-grafana-api/dashboard.go new file mode 100644 index 0000000000..ca45dc9a07 --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/dashboard.go @@ -0,0 +1,100 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" +) + +type DashboardMeta struct { + IsStarred bool `json:"isStarred"` + Slug string `json:"slug"` +} + +type DashboardSaveResponse struct { + Slug string `json:"slug"` + Status string `json:"status"` + Version int64 `json:"version"` +} + +type Dashboard struct { + Meta DashboardMeta `json:"meta"` + Model map[string]interface{} `json:"dashboard"` +} + +func (c *Client) SaveDashboard(model map[string]interface{}, overwrite bool) (*DashboardSaveResponse, error) { + wrapper := map[string]interface{}{ + "dashboard": model, + "overwrite": overwrite, + } + data, err := json.Marshal(wrapper) + if err != nil { + return nil, err + } + req, err := c.newRequest("POST", "/api/dashboards/db", bytes.NewBuffer(data)) + if err != nil { + return nil, err + } + + resp, err := c.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + return nil, errors.New(resp.Status) + } + + data, err = ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + result := &DashboardSaveResponse{} + err = json.Unmarshal(data, &result) + return result, err +} + +func (c *Client) Dashboard(slug string) (*Dashboard, error) { + path := fmt.Sprintf("/api/dashboards/db/%s", slug) + req, err := c.newRequest("GET", path, nil) + if err != nil { + return nil, err + } + + resp, err := c.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + return nil, errors.New(resp.Status) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + result := &Dashboard{} + err = json.Unmarshal(data, &result) + return result, err +} + +func (c *Client) DeleteDashboard(slug string) error { + path := fmt.Sprintf("/api/dashboards/db/%s", slug) + req, err := c.newRequest("DELETE", path, nil) + if err != nil { + return err + } + + resp, err := c.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + + return nil +} diff --git a/vendor/github.com/teemupo/go-grafana-api/datasource.go b/vendor/github.com/teemupo/go-grafana-api/datasource.go new file mode 100644 index 0000000000..ab42b88e60 --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/datasource.go @@ -0,0 +1,140 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" +) + +type DataSource struct { + Id int64 `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + URL string `json:"url"` + Access string `json:"access"` + + Database string `json:"database,omitempty"` + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` + + OrgId int64 `json:"orgId,omitempty"` + IsDefault bool `json:"isDefault"` + + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser,omitempty"` + BasicAuthPassword string `json:"basicAuthPassword,omitempty"` + + JSONData JSONData `json:"jsonData,omitempty"` + SecureJSONData SecureJSONData `json:"secureJsonData,omitempty"` +} + +// JSONData is a representation of the datasource `jsonData` property +type JSONData struct { + AssumeRoleArn string `json:"assumeRoleArn,omitempty"` + AuthType string `json:"authType,omitempty"` + CustomMetricsNamespaces string `json:"customMetricsNamespaces,omitempty"` + DefaultRegion string `json:"defaultRegion,omitempty"` +} + +// SecureJSONData is a representation of the datasource `secureJsonData` property +type SecureJSONData struct { + AccessKey string `json:"accessKey,omitempty"` + SecretKey string `json:"secretKey,omitempty"` +} + +func (c *Client) NewDataSource(s *DataSource) (int64, error) { + data, err := json.Marshal(s) + if err != nil { + return 0, err + } + req, err := c.newRequest("POST", "/api/datasources", bytes.NewBuffer(data)) + if err != nil { + return 0, err + } + + resp, err := c.Do(req) + if err != nil { + return 0, err + } + if resp.StatusCode != 200 { + return 0, errors.New(resp.Status) + } + + data, err = ioutil.ReadAll(resp.Body) + if err != nil { + return 0, err + } + + result := struct { + Id int64 `json:"id"` + }{} + err = json.Unmarshal(data, &result) + return result.Id, err +} + +func (c *Client) UpdateDataSource(s *DataSource) error { + path := fmt.Sprintf("/api/datasources/%d", s.Id) + data, err := json.Marshal(s) + if err != nil { + return err + } + req, err := c.newRequest("PUT", path, bytes.NewBuffer(data)) + if err != nil { + return err + } + + resp, err := c.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + + return nil +} + +func (c *Client) DataSource(id int64) (*DataSource, error) { + path := fmt.Sprintf("/api/datasources/%d", id) + req, err := c.newRequest("GET", path, nil) + if err != nil { + return nil, err + } + + resp, err := c.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + return nil, errors.New(resp.Status) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + result := &DataSource{} + err = json.Unmarshal(data, &result) + return result, err +} + +func (c *Client) DeleteDataSource(id int64) error { + path := fmt.Sprintf("/api/datasources/%d", id) + req, err := c.newRequest("DELETE", path, nil) + if err != nil { + return err + } + + resp, err := c.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + + return nil +} diff --git a/vendor/github.com/teemupo/go-grafana-api/orgs.go b/vendor/github.com/teemupo/go-grafana-api/orgs.go new file mode 100644 index 0000000000..5a38732c70 --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/orgs.go @@ -0,0 +1,70 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" +) + +type Org struct { + Id int64 + Name string +} + +func (c *Client) Orgs() ([]Org, error) { + orgs := make([]Org, 0) + + req, err := c.newRequest("GET", "/api/orgs/", nil) + if err != nil { + return orgs, err + } + resp, err := c.Do(req) + if err != nil { + return orgs, err + } + if resp.StatusCode != 200 { + return orgs, errors.New(resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return orgs, err + } + err = json.Unmarshal(data, &orgs) + return orgs, err +} + +func (c *Client) NewOrg(name string) error { + settings := map[string]string{ + "name": name, + } + data, err := json.Marshal(settings) + req, err := c.newRequest("POST", "/api/orgs", bytes.NewBuffer(data)) + if err != nil { + return err + } + resp, err := c.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + return err +} + +func (c *Client) DeleteOrg(id int64) error { + req, err := c.newRequest("DELETE", fmt.Sprintf("/api/orgs/%d", id), nil) + if err != nil { + return err + } + resp, err := c.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + return err +} diff --git a/vendor/github.com/teemupo/go-grafana-api/user.go b/vendor/github.com/teemupo/go-grafana-api/user.go new file mode 100644 index 0000000000..13a6e6ff4f --- /dev/null +++ b/vendor/github.com/teemupo/go-grafana-api/user.go @@ -0,0 +1,39 @@ +package gapi + +import ( + "encoding/json" + "errors" + "io/ioutil" +) + +type User struct { + Id int64 + Email string + Name string + Login string + IsAdmin bool +} + +func (c *Client) Users() ([]User, error) { + users := make([]User, 0) + req, err := c.newRequest("GET", "/api/users", nil) + if err != nil { + return users, err + } + resp, err := c.Do(req) + if err != nil { + return users, err + } + if resp.StatusCode != 200 { + return users, errors.New(resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return users, err + } + err = json.Unmarshal(data, &users) + if err != nil { + return users, err + } + return users, err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index dc46b182a9..b9ad2f2cd2 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -489,6 +489,12 @@ "revision": "ae970a0732be3a1f5311da86118d37b9f4bd2a5a", "revisionTime": "2018-05-02T07:23:49Z" }, + { + "checksumSHA1": "2OAzyUvaUKVZkRx9f8NlbHT9cc0=", + "path": "github.com/teemupo/go-grafana-api", + "revision": "7380665c19d6fb0b587e654f25d2604787d890c8", + "revisionTime": "2018-02-22T10:42:15Z" + }, { "checksumSHA1": "TT1rac6kpQp2vz24m5yDGUNQ/QQ=", "path": "golang.org/x/crypto/cast5",