mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 02:12:23 +00:00
cmd, dashboard, stateth: integrate stateth into geth dashboard
This commit is contained in:
parent
7820faf26e
commit
b99e1a441c
12 changed files with 645 additions and 217 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ class Header extends Component<Props> {
|
|||
<Typography type='title' color='inherit' noWrap className={classes.title}>
|
||||
Go Ethereum Dashboard
|
||||
</Typography>
|
||||
<Typography type='title' color='inherit' noWrap className={classes.title}>
|
||||
<a href={'http://localhost:3000'} target='_blank' style={{color: 'inherit', textDecoration: 'none'}} >Grafana</a>
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
49
stateth/config.go
Normal file
49
stateth/config.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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"`
|
||||
}
|
||||
231
stateth/grafana_dashboards/geth.json
Normal file
231
stateth/grafana_dashboards/geth.json
Normal file
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
257
stateth/stateth.go
Normal file
257
stateth/stateth.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Reference in a new issue