cmd, dashboard, stateth: integrate stateth into geth dashboard

This commit is contained in:
Kurkó Mihály 2018-06-06 20:27:45 +03:00
parent 7820faf26e
commit b99e1a441c
12 changed files with 645 additions and 217 deletions

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -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
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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>
);

View file

@ -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

View file

@ -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
View 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"`
}

View 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
View 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
}