diff --git a/cmd/geth/config.go b/cmd/geth/config.go index e6bd4d5bef..bff07d58fc 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/stateth" whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" "github.com/naoina/toml" ) @@ -80,6 +81,7 @@ type gethConfig struct { Node node.Config Ethstats ethstatsConfig Dashboard dashboard.Config + Stateth stateth.Config } func loadConfig(file string, cfg *gethConfig) error { @@ -114,6 +116,7 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { Shh: whisper.DefaultConfig, Node: defaultNodeConfig(), Dashboard: dashboard.DefaultConfig, + Stateth: stateth.DefaultConfig, } // Load config file. @@ -135,6 +138,7 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { } utils.SetShhConfig(ctx, stack, &cfg.Shh) + utils.SetStatethConfig(ctx, &cfg.Stateth) utils.SetDashboardConfig(ctx, &cfg.Dashboard) return stack, cfg @@ -156,6 +160,7 @@ func makeFullNode(ctx *cli.Context) *node.Node { utils.RegisterEthService(stack, &cfg.Eth) if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) { + utils.RegisterStatethService(stack, &cfg.Stateth, ctx) utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit) } // Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode diff --git a/cmd/geth/main.go b/cmd/geth/main.go index bdd691d334..e9a14ab9af 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -62,6 +62,11 @@ var ( utils.DashboardAddrFlag, utils.DashboardPortFlag, utils.DashboardRefreshFlag, + utils.StatethDockerPrefixFlag, + utils.StatethDashboardsFolderFlag, + utils.StatethGrafanaPortFlag, + utils.StatethInfluxdbPortFlag, + utils.StatethRmFlag, utils.EthashCacheDirFlag, utils.EthashCachesInMemoryFlag, utils.EthashCachesOnDiskFlag, diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index d934c6b021..47f8c8b0a5 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -110,6 +110,16 @@ var AppHelpFlagGroups = []flagGroup{ // utils.DashboardAssetsFlag, // }, //}, + //{ + // Name: "STATETH", + // Flags: []cli.Flag{ + // utils.StatethDockerPrefixFlag, + // utils.StatethDashboardsFolderFlag, + // utils.StatethGrafanaPortFlag, + // utils.StatethInfluxdbPortFlag, + // utils.StatethRmFlag, + // }, + //}, { Name: "TRANSACTION POOL", Flags: []cli.Flag{ @@ -284,7 +294,7 @@ func init() { uncategorized := []cli.Flag{} for _, flag := range data.(*cli.App).Flags { if _, ok := categorized[flag.String()]; !ok { - if strings.HasPrefix(flag.GetName(), "dashboard") { + if strings.HasPrefix(flag.GetName(), "dashboard") || strings.HasPrefix(flag.GetName(), "stateth") { continue } uncategorized = append(uncategorized, flag) diff --git a/cmd/stateth/stateth.go b/cmd/stateth/stateth.go index 0d3d6e9dc2..74502eadf5 100644 --- a/cmd/stateth/stateth.go +++ b/cmd/stateth/stateth.go @@ -18,33 +18,13 @@ 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" + "github.com/ethereum/go-ethereum/stateth" "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 + "os" + "os/signal" + "syscall" ) func main() { @@ -78,30 +58,25 @@ func main() { Value: "stateth", Usage: "prefix to be used for docker network and containers. must be unique.", }, + cli.BoolFlag{ + Name: "rm", + Usage: "Remove existing stateth network and stateth containers upon startup. make sure that the start up works every time, even if the service wasn't shut down gracefully", + }, } - app.Action = func(c *cli.Context) error { - log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(os.Stdout, log.TerminalFormat(true)))) + app.Action = func(ctx *cli.Context) error { + log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(ctx.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 { + se, err := stateth.New(ctx, &stateth.Config{ + DockerPrefix: ctx.String("docker-prefix"), + GrafanaPort: ctx.Int("grafana-http-port"), + InfluxDBPort: ctx.Int("influxdb-http-port"), + DashboardsFolder: ctx.String("grafana-dashboards-folder"), + Rm: ctx.Bool("rm"), + }) + if 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 { + if err = se.StartExternal(); err != nil { return err } @@ -116,172 +91,11 @@ func main() { 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) + return se.StopExternal() } 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 49b8568d82..8a8a955739 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -57,6 +57,7 @@ import ( "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/stateth" whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" "gopkg.in/urfave/cli.v1" ) @@ -193,7 +194,7 @@ var ( } // Dashboard settings DashboardEnabledFlag = cli.BoolFlag{ - Name: "dashboard", + Name: metrics.DashboardEnabledFlag, Usage: "Enable the dashboard", } DashboardAddrFlag = cli.StringFlag{ @@ -211,6 +212,31 @@ var ( Usage: "Dashboard metrics collection refresh rate", Value: dashboard.DefaultConfig.Refresh, } + // Stateth settings + StatethDockerPrefixFlag = cli.StringFlag{ + Name: "stateth.dockerprefix", + Usage: "Prefix to be used for docker network and containers. Must be unique.", + Value: stateth.DefaultConfig.DockerPrefix, + } + StatethDashboardsFolderFlag = cli.StringFlag{ + Name: "stateth.dashboardsfolder", + Usage: "Grafana dashboards folder", + Value: stateth.DefaultConfig.DashboardsFolder, + } + StatethGrafanaPortFlag = cli.IntFlag{ + Name: "stateth.grafanaport", + Usage: "Grafana http port", + Value: stateth.DefaultConfig.GrafanaPort, + } + StatethInfluxdbPortFlag = cli.IntFlag{ + Name: "stateth.influxdbport", + Usage: "Influxdb http port", + Value: stateth.DefaultConfig.InfluxDBPort, + } + StatethRmFlag = cli.BoolFlag{ + Name: "stateth.rm", + Usage: "Remove existing stateth network and stateth containers upon startup. Make sure that the start up works every time, even if the service wasn't shut down gracefully.", + } // Ethash settings EthashCacheDirFlag = DirectoryFlag{ Name: "ethash.cachedir", @@ -1157,6 +1183,14 @@ func SetDashboardConfig(ctx *cli.Context, cfg *dashboard.Config) { cfg.Refresh = ctx.GlobalDuration(DashboardRefreshFlag.Name) } +func SetStatethConfig(ctx *cli.Context, cfg *stateth.Config) { + cfg.DockerPrefix = ctx.GlobalString(StatethDockerPrefixFlag.Name) + cfg.DashboardsFolder = ctx.GlobalString(StatethDashboardsFolderFlag.Name) + cfg.GrafanaPort = ctx.GlobalInt(StatethGrafanaPortFlag.Name) + cfg.InfluxDBPort = ctx.GlobalInt(StatethInfluxdbPortFlag.Name) + cfg.Rm = ctx.GlobalBool(StatethRmFlag.Name) +} + // RegisterEthService adds an Ethereum client to the stack. func RegisterEthService(stack *node.Node, cfg *eth.Config) { var err error @@ -1193,6 +1227,12 @@ func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit st }) } +func RegisterStatethService(stack *node.Node, cfg *stateth.Config, cliCtx *cli.Context) { + stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { + return stateth.New(cliCtx, cfg) + }) +} + // RegisterShhService configures Whisper and adds it to the given node. func RegisterShhService(stack *node.Node, cfg *whisper.Config) { if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) { diff --git a/dashboard/assets.go b/dashboard/assets.go index a8b040c42c..ccbdaf7fe3 100644 --- a/dashboard/assets.go +++ b/dashboard/assets.go @@ -29943,7 +29943,19 @@ var _bundleJs = []byte((((((((((`!function(modules) { color: "inherit", noWrap: !0, className: classes.title - }, "Go Ethereum Dashboard"))); + }, "Go Ethereum Dashboard"), _react2.default.createElement(_Typography2.default, { + type: "title", + color: "inherit", + noWrap: !0, + className: classes.title + }, _react2.default.createElement("a", { + href: "http://localhost:3000", + target: "_blank", + style: { + color: "inherit", + textDecoration: "none" + } + }, "Grafana")))); } } ]), Header; }(_react.Component); @@ -40572,7 +40584,7 @@ func bundleJs() (*asset, error) { } info := bindataFileInfo{name: "bundle.js", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x88, 0x4d, 0x2e, 0x77, 0x25, 0xbe, 0x43, 0xdf, 0x86, 0xed, 0x5b, 0x2c, 0xa8, 0xdd, 0xa2, 0xd5, 0x38, 0xf3, 0x82, 0xbe, 0x29, 0x9d, 0x52, 0x3, 0xd4, 0x4c, 0xd3, 0x7e, 0xaa, 0x3e, 0xf6, 0x8c}} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x49, 0x2b, 0x2a, 0x45, 0xb3, 0x61, 0xc, 0xc3, 0xa3, 0x8a, 0x9e, 0x66, 0x30, 0x3e, 0x98, 0xe7, 0xde, 0x48, 0x25, 0xf3, 0x77, 0x8f, 0xbe, 0x86, 0x48, 0x3, 0x1, 0x3a, 0x4a, 0xab, 0xac, 0x38}} return a, nil } diff --git a/dashboard/assets/components/Header.jsx b/dashboard/assets/components/Header.jsx index ccdfbc6f02..6f6fc73d0a 100644 --- a/dashboard/assets/components/Header.jsx +++ b/dashboard/assets/components/Header.jsx @@ -64,6 +64,9 @@ class Header extends Component { Go Ethereum Dashboard + + Grafana + ); diff --git a/dashboard/dashboard.go b/dashboard/dashboard.go index c9c2845a47..c2ed40514f 100644 --- a/dashboard/dashboard.go +++ b/dashboard/dashboard.go @@ -111,7 +111,7 @@ func New(config *Config, commit string, ethServ *eth.Ethereum, lesServ *les.Ligh }, ethServ: ethServ, lesServ: lesServ, - logdir: logdir, + logdir: logdir, }, nil } @@ -126,13 +126,14 @@ func emptyChartEntries(t time.Time, limit int, refresh time.Duration) ChartEntri return ce } -// Protocols is a meaningless implementation of node.Service. +// Protocols implements the node.Service interface. func (db *Dashboard) Protocols() []p2p.Protocol { return nil } -// APIs is a meaningless implementation of node.Service. +// APIs implements the node.Service interface. func (db *Dashboard) APIs() []rpc.API { return nil } -// Start implements node.Service, starting the data collection thread and the listening server of the dashboard. +// Start starts the data collection thread and the listening server of the dashboard. +// Implements the node.Service interface. func (db *Dashboard) Start(server *p2p.Server) error { log.Info("Starting dashboard") @@ -154,7 +155,8 @@ func (db *Dashboard) Start(server *p2p.Server) error { return nil } -// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard. +// Stop stops the data collection thread and the connection listener of the dashboard. +// Implements the node.Service interface. func (db *Dashboard) Stop() error { // Close the connection listener. var errs []error diff --git a/dashboard/message.go b/dashboard/message.go index 593ad3c7c7..4a5e4f971b 100644 --- a/dashboard/message.go +++ b/dashboard/message.go @@ -71,8 +71,8 @@ type SystemMessage struct { } type LogsMessage struct { - Old *LogFile `json:"old,omitempty"` // Attributes of the log file. - Chunk json.RawMessage `json:"chunk"` // Contains log records. + Old *LogFile `json:"old,omitempty"` // Attributes of the log file. + Chunk json.RawMessage `json:"chunk"` // Contains log records. } type LogFile struct { diff --git a/stateth/config.go b/stateth/config.go new file mode 100644 index 0000000000..6f1cb7c81c --- /dev/null +++ b/stateth/config.go @@ -0,0 +1,49 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library 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 Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package stateth + +import ( + "os" +) + +// DefaultConfig contains default settings for the stateth. +var DefaultConfig = Config{ + DockerPrefix: "stateth", + DashboardsFolder: os.Getenv("GOPATH") + "/src/github.com/ethereum/go-ethereum/stateth/grafana_dashboards", + GrafanaPort: 3000, + InfluxDBPort: 8086, + Rm: false, +} + +// Config contains the configuration parameters of the dashboard. +type Config struct { + // DockerPrefix is a unique prefix used for the created docker resources. + DockerPrefix string `toml:",omitempty"` + + // DashboardsFolder is a folder containing all dashboards to be imported in Grafana. + DashboardsFolder string `toml:",omitempty"` + + // GrafanaPort is the expose port for the Grafana HTTP interface. + GrafanaPort int `toml:",omitempty"` + + // InfluxDBPort is the expose port for the InfluxDB HTTP interface. + InfluxDBPort int `toml:",omitempty"` + + // Rm removes existing stateth network and stateth containers upon startup if true. + // Makes sure that the start up works every time, even if the service wasn't shut down gracefully. + Rm bool `toml:",omitempty"` +} diff --git a/stateth/grafana_dashboards/geth.json b/stateth/grafana_dashboards/geth.json new file mode 100644 index 0000000000..6e269e7f4d --- /dev/null +++ b/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/stateth/stateth.go b/stateth/stateth.go new file mode 100644 index 0000000000..935f15bc38 --- /dev/null +++ b/stateth/stateth.go @@ -0,0 +1,257 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library 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 Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package stateth + +import ( + "encoding/json" + "fmt" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rpc" + gapi "github.com/teemupo/go-grafana-api" + "gopkg.in/urfave/cli.v1" + "io/ioutil" + "os/exec" + "path/filepath" + "strings" + "time" +) + +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. +) + +// Stateth contains the stateth internals. +type Stateth struct { + ctx *cli.Context + config *Config + gclient *gapi.Client // Grafana client. +} + +// New creates a new stateth instance with the given configuration. +func New(ctx *cli.Context, config *Config) (*Stateth, error) { + return &Stateth{ + ctx: ctx, + config: config, + }, nil +} + +// Protocols implements the node.Service interface. +func (se *Stateth) Protocols() []p2p.Protocol { return nil } + +// APIs implements the node.Service interface. +func (se *Stateth) APIs() []rpc.API { return nil } + +// Start starts InfluxDB and Grafana. +// Implements the node.Service interface. +func (se *Stateth) Start(server *p2p.Server) error { + return se.StartExternal() +} + +// Stop cleans up the containers. +// Implements the node.Service interface. +func (se *Stateth) Stop() error { return se.StopExternal() } + +func (se *Stateth) StartExternal() error { + var err error + if se.config.Rm { + if err = se.cleanupContainers(); err != nil { + return err + } + } + if err = se.runNetwork(); err != nil { + return err + } + if err = se.runInfluxDB(); err != nil { + return err + } + if err = se.runGrafana(); err != nil { + return err + } + log.Info("waiting for grafana to boot up...") + time.Sleep(7 * time.Second) // give time to Grafana to boot up + + se.gclient, err = gapi.New(fmt.Sprintf("%s:%s", grafanaUser, grafanaPass), fmt.Sprintf("http://localhost:%d", se.config.GrafanaPort)) + if err != nil { + log.Warn(err.Error()) + return nil + } + if err = se.importGrafanaDatasource(); err != nil { + return err + } + if err = se.importGrafanaDashboards(); err != nil { + return err + } + fmt.Println(fmt.Sprintf("grafana listening on http://localhost:%d", se.config.GrafanaPort)) + fmt.Println(fmt.Sprintf("username: %s", grafanaUser)) + fmt.Println(fmt.Sprintf("password: %s", grafanaPass)) + fmt.Println() + + return nil +} + +func (se *Stateth) StopExternal() error { return se.cleanupContainers() } + +func (se *Stateth) runNetwork() error { + log.Info("creating docker network", "network", se.config.DockerPrefix) + command := strings.Split(fmt.Sprintf("docker network create %s", se.config.DockerPrefix), " ") + r, err := exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Error(string(r)) + return err + } + return nil +} + +func (se *Stateth) runInfluxDB() 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", se.config.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", se.config.DockerPrefix, se.config.DockerPrefix, influxDBAdminUser, influxDBAdminPass, se.config.InfluxDBPort), " ") + r, err = exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Error(string(r)) + return err + } + return nil +} + +func (se *Stateth) runGrafana() 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", se.config.DockerPrefix)) + //command = strings.Split(fmt.Sprintf("docker run --network %s --name=%s_grafana -p %d:3000 -d grafana/grafana:5.1.3", se.config.DockerPrefix, se.config.DockerPrefix, se.config.GrafanaPort), " ") + command = strings.Split(fmt.Sprintf("docker run --network %s --name=%s_grafana -e GF_AUTH_ANONYMOUS_ENABLED=true -p %d:3000 -d grafana/grafana:5.1.3", se.config.DockerPrefix, se.config.DockerPrefix, se.config.GrafanaPort), " ") + r, err = exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Error(string(r)) + return err + } + return nil +} + +func (se *Stateth) cleanupContainers() error { + log.Info("removing influxdb container") + command := strings.Split(fmt.Sprintf("docker rm -f %s_influxdb", se.config.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", se.config.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", se.config.DockerPrefix), " ") + r, err = exec.Command(command[0], command[1:]...).CombinedOutput() + if err != nil { + log.Warn(string(r)) + } + + return nil +} + +func (se *Stateth) importGrafanaDatasource() error { + log.Info("importing grafana datasource") + + dataSource := &gapi.DataSource{ + Name: "metrics", + Type: "influxdb", + URL: fmt.Sprintf("http://%s_influxdb:%d", se.config.DockerPrefix, se.config.InfluxDBPort), + Access: "proxy", + Database: "metrics", + User: influxDBAdminUser, + Password: influxDBAdminPass, + IsDefault: true, + BasicAuth: false, + } + + _, err := se.gclient.NewDataSource(dataSource) + if err != nil { + log.Warn(err.Error()) + return err + } + + return nil +} + +func (se *Stateth) importGrafanaDashboards() error { + log.Info("importing grafana dashboards") + + files, err := ioutil.ReadDir(se.config.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(filepath.Join(se.config.DashboardsFolder, name)) + if err != nil { + log.Warn(err.Error()) + return nil + } + + model := se.prepareDashboardModel(string(blob)) + + _, err = se.gclient.SaveDashboard(model, false) + if err != nil { + log.Warn(err.Error()) + return nil + } + } + } + + return nil +} + +func (se *Stateth) 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 +}