mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-16 17:03:46 +00:00
Merge 6037e64b11 into c4d21bc8e5
This commit is contained in:
commit
f3d6fe9b88
9 changed files with 993 additions and 7 deletions
|
|
@ -30,6 +30,7 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/contracts/release"
|
||||
"github.com/ethereum/go-ethereum/dashboard"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
|
|
@ -76,10 +77,11 @@ type ethstatsConfig struct {
|
|||
}
|
||||
|
||||
type gethConfig struct {
|
||||
Eth eth.Config
|
||||
Shh whisper.Config
|
||||
Node node.Config
|
||||
Ethstats ethstatsConfig
|
||||
Eth eth.Config
|
||||
Shh whisper.Config
|
||||
Node node.Config
|
||||
Ethstats ethstatsConfig
|
||||
Dashboard dashboard.Config
|
||||
}
|
||||
|
||||
func loadConfig(file string, cfg *gethConfig) error {
|
||||
|
|
@ -110,9 +112,10 @@ func defaultNodeConfig() node.Config {
|
|||
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
|
||||
// Load defaults.
|
||||
cfg := gethConfig{
|
||||
Eth: eth.DefaultConfig,
|
||||
Shh: whisper.DefaultConfig,
|
||||
Node: defaultNodeConfig(),
|
||||
Eth: eth.DefaultConfig,
|
||||
Shh: whisper.DefaultConfig,
|
||||
Node: defaultNodeConfig(),
|
||||
Dashboard: dashboard.DefaultConfig,
|
||||
}
|
||||
|
||||
// Load config file.
|
||||
|
|
@ -134,6 +137,7 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
|
|||
}
|
||||
|
||||
utils.SetShhConfig(ctx, stack, &cfg.Shh)
|
||||
utils.SetDashboardConfig(ctx, &cfg.Dashboard)
|
||||
|
||||
return stack, cfg
|
||||
}
|
||||
|
|
@ -153,6 +157,9 @@ func makeFullNode(ctx *cli.Context) *node.Node {
|
|||
|
||||
utils.RegisterEthService(stack, &cfg.Eth)
|
||||
|
||||
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
|
||||
utils.RegisterDashboardService(stack, &cfg.Dashboard)
|
||||
}
|
||||
// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
|
||||
shhEnabled := enableWhisper(ctx)
|
||||
shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@ var (
|
|||
utils.DataDirFlag,
|
||||
utils.KeyStoreDirFlag,
|
||||
utils.NoUSBFlag,
|
||||
utils.DashboardEnabledFlag,
|
||||
utils.DashboardAddrFlag,
|
||||
utils.DashboardPortFlag,
|
||||
utils.DashboardRefreshFlag,
|
||||
utils.DashboardAssetsFlag,
|
||||
utils.EthashCacheDirFlag,
|
||||
utils.EthashCachesInMemoryFlag,
|
||||
utils.EthashCachesOnDiskFlag,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,16 @@ var AppHelpFlagGroups = []flagGroup{
|
|||
utils.EthashDatasetsOnDiskFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "DASHBOARD",
|
||||
Flags: []cli.Flag{
|
||||
utils.DashboardEnabledFlag,
|
||||
utils.DashboardAddrFlag,
|
||||
utils.DashboardPortFlag,
|
||||
utils.DashboardRefreshFlag,
|
||||
utils.DashboardAssetsFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TRANSACTION POOL",
|
||||
Flags: []cli.Flag{
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/dashboard"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||
"github.com/ethereum/go-ethereum/eth/gasprice"
|
||||
|
|
@ -177,6 +178,31 @@ var (
|
|||
Name: "lightkdf",
|
||||
Usage: "Reduce key-derivation RAM & CPU usage at some expense of KDF strength",
|
||||
}
|
||||
// Dashboard settings
|
||||
DashboardEnabledFlag = cli.BoolFlag{
|
||||
Name: "dashboard",
|
||||
Usage: "Enable the dashboard",
|
||||
}
|
||||
DashboardAddrFlag = cli.StringFlag{
|
||||
Name: "dashboard.addr",
|
||||
Usage: "Dashboard listening interface",
|
||||
Value: dashboard.DefaultConfig.Host,
|
||||
}
|
||||
DashboardPortFlag = cli.IntFlag{
|
||||
Name: "dashboard.host",
|
||||
Usage: "Dashboard listening port",
|
||||
Value: dashboard.DefaultConfig.Port,
|
||||
}
|
||||
DashboardRefreshFlag = cli.DurationFlag{
|
||||
Name: "dashboard.refresh",
|
||||
Usage: "Dashboard metrics collection refresh rate",
|
||||
Value: dashboard.DefaultConfig.Refresh,
|
||||
}
|
||||
DashboardAssetsFlag = cli.StringFlag{
|
||||
Name: "dashboard.assets",
|
||||
Usage: "Developer flag to serve the dashboard from the local file system (default: \"\")",
|
||||
Value: dashboard.DefaultConfig.Assets,
|
||||
}
|
||||
// Ethash settings
|
||||
EthashCacheDirFlag = DirectoryFlag{
|
||||
Name: "ethash.cachedir",
|
||||
|
|
@ -997,6 +1023,14 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetDashboardConfig applies dashboard related command line flags to the config.
|
||||
func SetDashboardConfig(ctx *cli.Context, cfg *dashboard.Config) {
|
||||
cfg.Host = ctx.GlobalString(DashboardAddrFlag.Name)
|
||||
cfg.Port = ctx.GlobalInt(DashboardPortFlag.Name)
|
||||
cfg.Refresh = ctx.GlobalDuration(DashboardRefreshFlag.Name)
|
||||
cfg.Assets = ctx.GlobalString(DashboardAssetsFlag.Name)
|
||||
}
|
||||
|
||||
// RegisterEthService adds an Ethereum client to the stack.
|
||||
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
||||
var err error
|
||||
|
|
@ -1019,6 +1053,13 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
|||
}
|
||||
}
|
||||
|
||||
// RegisterDashboardService adds a dashboard to the stack.
|
||||
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config) {
|
||||
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
return dashboard.New(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) {
|
||||
|
|
|
|||
260
dashboard/assets.go
Normal file
260
dashboard/assets.go
Normal file
File diff suppressed because one or more lines are too long
179
dashboard/assets/dashboard.html
Normal file
179
dashboard/assets/dashboard.html
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Go Ethereum Dashboard</title>
|
||||
|
||||
<link rel="shortcut icon" type="image/ico" href="https://ethereum.org/favicon.ico"/>
|
||||
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/css/custom.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="nav-md">
|
||||
<div class="container body">
|
||||
<div class="main_container">
|
||||
<div class="col-md-3 left_col">
|
||||
<div class="left_col scroll-view">
|
||||
<div class="navbar nav_title" style="border: 0;">
|
||||
<a href="dashboard.html" class="site_title"><i class="fa fa-paw"></i> <span>Go Ethereum Dashboard</span></a>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<!-- sidebar menu -->
|
||||
<div id="sidebar-menu" class="main_menu_side hidden-print main_menu">
|
||||
<div class="menu_section">
|
||||
<ul class="nav side-menu">
|
||||
<li><a><i class="fa fa-home"></i> Home <span class="fa fa-chevron-down"></span></a>
|
||||
<ul class="nav child_menu">
|
||||
<li><a href="dashboard.html">Dashboard1</a></li>
|
||||
<li><a href="dashboard.html">Dashboard2</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a><i class="fa fa-edit"></i> Networking <span class="fa fa-chevron-down"></span></a>
|
||||
<ul class="nav child_menu">
|
||||
<li><a href="networking.html"></a>Networking1</li>
|
||||
<li><a href="networking.html"></a>Networking2</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a><i class="fa fa-desktop"></i> Txpool <span class="fa fa-chevron-down"></span></a>
|
||||
<ul class="nav child_menu">
|
||||
<li><a href="txpool.html">Txpool1</a></li>
|
||||
<li><a href="txpool.html">Txpool2</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a><i class="fa fa-table"></i> Logs <span class="fa fa-chevron-down"></span></a>
|
||||
<ul class="nav child_menu">
|
||||
<li><a href="logs.html">Logs1</a></li>
|
||||
<li><a href="logs.html">Logs2</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a><i class="fa fa-bar-chart-o"></i> System <span class="fa fa-chevron-down"></span></a>
|
||||
<ul class="nav child_menu">
|
||||
<li><a href="system.html">System1</a></li>
|
||||
<li><a href="system.html">System2</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a><i class="fa fa-clone"></i>Blockchain <span class="fa fa-chevron-down"></span></a>
|
||||
<ul class="nav child_menu">
|
||||
<li><a href="blockchain.html">Blockchain1</a></li>
|
||||
<li><a href="blockchain.html">Blockchain2</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /sidebar menu -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- top navigation -->
|
||||
<div class="top_nav">
|
||||
<div class="nav_menu">
|
||||
<nav class="" role="navigation">
|
||||
<div class="nav toggle">
|
||||
<a id="menu_toggle"><i class="fa fa-bars"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /top navigation -->
|
||||
|
||||
<!-- page content -->
|
||||
<div class="right_col" role="main">
|
||||
<div class="">
|
||||
<div class="page-title">
|
||||
<div class="title_left">
|
||||
<h3>Go Ethereum Dashboard <small>Statistics</small></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>Memory usage <small>system/memory/inuse</small></h2>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><a class="collapse-link"><i class="fa fa-chevron-up"></i></a>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><i class="fa fa-wrench"></i></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#">Settings 1</a>
|
||||
</li>
|
||||
<li><a href="#">Settings 2</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="close-link"><i class="fa fa-close"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<canvas id="memoryLineChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>Inbound traffic <small>p2p/InboundTraffic</small></h2>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><a class="collapse-link"><i class="fa fa-chevron-up"></i></a>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><i class="fa fa-wrench"></i></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#">Settings 1</a>
|
||||
</li>
|
||||
<li><a href="#">Settings 2</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="close-link"><i class="fa fa-close"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<canvas id="trafficLineChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /page content -->
|
||||
|
||||
<!-- footer content -->
|
||||
<footer>
|
||||
<div class="pull-right">
|
||||
Gentelella - Bootstrap Admin Template by <a href="https://colorlib.com">Colorlib</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</footer>
|
||||
<!-- /footer content -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-noty/2.4.1/packaged/jquery.noty.packaged.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.0.0/Chart.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script>
|
||||
|
||||
<script src="js/handlers.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
112
dashboard/assets/js/handlers.js
Normal file
112
dashboard/assets/js/handlers.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
Chart.defaults.global.legend = {
|
||||
enabled: false
|
||||
}
|
||||
|
||||
// Line chart for memory
|
||||
var ctx = document.getElementById("memoryLineChart");
|
||||
var memoryLineChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: "system/memory/inuse",
|
||||
backgroundColor: "rgba(38, 185, 154, 0.31)",
|
||||
borderColor: "rgba(38, 185, 154, 0.7)",
|
||||
pointBorderColor: "rgba(38, 185, 154, 0.7)",
|
||||
pointBackgroundColor: "rgba(38, 185, 154, 0.7)",
|
||||
pointHoverBackgroundColor: "#fff",
|
||||
pointHoverBorderColor: "rgba(220,220,220,1)",
|
||||
pointBorderWidth: 1,
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
});
|
||||
|
||||
// Line chart for traffic
|
||||
var trafficCtx = document.getElementById("trafficLineChart");
|
||||
var trafficLineChart = new Chart(trafficCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: "p2p/InboundTraffic",
|
||||
backgroundColor: "rgba(3, 88, 106, 0.3)",
|
||||
borderColor: "rgba(3, 88, 106, 0.70)",
|
||||
pointBorderColor: "rgba(3, 88, 106, 0.70)",
|
||||
pointBackgroundColor: "rgba(3, 88, 106, 0.70)",
|
||||
pointHoverBackgroundColor: "#fff",
|
||||
pointHoverBorderColor: "rgba(151,187,205,1)",
|
||||
pointBorderWidth: 1,
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
});
|
||||
|
||||
//TODO (kurkomisi): remove static values after debugging
|
||||
const MEMORY_SAMPLE_LIMIT = 200;//{{.memorySampleLimit}}; // Maximum number of memory data samples
|
||||
const TRAFFIC_SAMPLE_LIMIT = 200;//{{.trafficSampleLimit}}; // Maximum number of traffic data samples
|
||||
const PROCESSOR_SAMPLE_LIMIT = 200;//{{.processorSampleLimit}}; // Maximum number of processor data samples
|
||||
|
||||
function updateCharts(msg) {
|
||||
|
||||
// Fill the dashboard with past data
|
||||
if(msg.metrics !== undefined) {
|
||||
// Clear in case the dashboard was opened before
|
||||
memoryLineChart.data.labels = [];
|
||||
trafficLineChart.data.labels = [];
|
||||
memoryLineChart.data.datasets[0].data = [];
|
||||
trafficLineChart.data.datasets[0].data = [];
|
||||
|
||||
var memory = msg.metrics.memory;
|
||||
var processor = msg.metrics.processor;
|
||||
// It is possible to get another message while filling the arrays by push(), so instead
|
||||
// put the history to the beginning of the arrays
|
||||
for (var i = memory.length - 1; i >= 0 && MEMORY_SAMPLE_LIMIT > memoryLineChart.data.labels.length; --i) {
|
||||
memoryLineChart.data.labels.unshift(memory[i].time.substring(memory[i].time.length - 5));
|
||||
trafficLineChart.data.labels.unshift(memory[i].time.substring(memory[i].time.length - 5));
|
||||
memoryLineChart.data.datasets[0].data.unshift(memory[i].value);
|
||||
trafficLineChart.data.datasets[0].data.unshift(processor[i].value);
|
||||
}
|
||||
memoryLineChart.update();
|
||||
trafficLineChart.update();
|
||||
return;
|
||||
}
|
||||
|
||||
// update
|
||||
if(msg.memory !== undefined) {
|
||||
if(memoryLineChart.data.labels.length === MEMORY_SAMPLE_LIMIT) {
|
||||
memoryLineChart.data.labels.shift();
|
||||
trafficLineChart.data.labels.shift();
|
||||
memoryLineChart.data.datasets[0].data.shift();
|
||||
trafficLineChart.data.datasets[0].data.shift();
|
||||
}
|
||||
memoryLineChart.data.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
|
||||
trafficLineChart.data.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
|
||||
memoryLineChart.data.datasets[0].data.push(msg.memory.value);
|
||||
trafficLineChart.data.datasets[0].data.push(msg.processor.value);
|
||||
memoryLineChart.update();
|
||||
trafficLineChart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// Global variables to hold the current status of the dashboard
|
||||
var server;
|
||||
|
||||
// Define a method to reconnect upon server loss
|
||||
var reconnect = function() {
|
||||
server = new WebSocket("ws://" + location.host + "/api");
|
||||
|
||||
server.onmessage = function(event) {
|
||||
var msg = JSON.parse(event.data);
|
||||
if (msg === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateCharts(msg)
|
||||
}
|
||||
|
||||
server.onclose = function() { setTimeout(reconnect, 3000); };
|
||||
}
|
||||
|
||||
// Establish a websocket connection to the API server
|
||||
reconnect();
|
||||
45
dashboard/config.go
Normal file
45
dashboard/config.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2017 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 dashboard
|
||||
|
||||
import "time"
|
||||
|
||||
// DefaultConfig contains default settings for the dashboard.
|
||||
var DefaultConfig = Config{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Refresh: time.Second,
|
||||
}
|
||||
|
||||
// Config contains the configuration parameters of the dashboard.
|
||||
type Config struct {
|
||||
// Host is the host interface on which to start the dashboard server. If this
|
||||
// field is empty, no dashboard will be started.
|
||||
Host string `toml:",omitempty"`
|
||||
|
||||
// Port is the TCP port number on which to start the dashboard server. The
|
||||
// default zero value is/ valid and will pick a port number randomly (useful
|
||||
// for ephemeral nodes).
|
||||
Port int `toml:",omitempty"`
|
||||
|
||||
// Refresh is the refresh rate of the data updates, the data will be collected this often.
|
||||
Refresh time.Duration `toml:",omitempty"`
|
||||
|
||||
// Assets offers a possibility to manually set the dashboard website's location on the server side.
|
||||
// It is useful for debugging, avoids the repeated generation of the binary.
|
||||
Assets string `toml:",omitempty"`
|
||||
}
|
||||
327
dashboard/dashboard.go
Normal file
327
dashboard/dashboard.go
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
// Copyright 2017 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 dashboard
|
||||
|
||||
//go:generate go-bindata -nometadata -o assets.go -prefix assets -pkg dashboard assets/...
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/rcrowley/go-metrics"
|
||||
"golang.org/x/net/websocket"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
processorSampleLimit = 200
|
||||
memorySampleLimit = 200
|
||||
trafficSampleLimit = 200
|
||||
)
|
||||
|
||||
var (
|
||||
nextId uint32 // Next connection id
|
||||
)
|
||||
|
||||
type dashboard struct {
|
||||
config *Config
|
||||
|
||||
listener net.Listener
|
||||
conns []*client // Currently live websocket connections
|
||||
Metrics *metricSamples `json:"metrics,omitempty"`
|
||||
Stats *status `json:"stats,omitempty"`
|
||||
lock sync.RWMutex // Lock protecting the dashboard's internals
|
||||
|
||||
closing map[*chan chan error]bool // Channels used for graceful exit
|
||||
mapLock sync.RWMutex // Lock protecting the closing map's internals
|
||||
}
|
||||
|
||||
type client struct {
|
||||
conn *websocket.Conn // Particular live websocket connection
|
||||
msg chan *map[string]interface{} // Message queue for the update messages
|
||||
logger log.Logger // Logger for the particular live websocket connection
|
||||
}
|
||||
|
||||
type metricSamples struct {
|
||||
Processor []*data `json:"processor,omitempty"`
|
||||
Memory []*data `json:"memory,omitempty"`
|
||||
}
|
||||
|
||||
type data struct {
|
||||
Time time.Time `json:"time,omitempty"`
|
||||
Value float64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type status struct {
|
||||
Peers int `json:"peers,omitempty"`
|
||||
Block int `json:"block,omitempty"`
|
||||
}
|
||||
|
||||
// New creates a new dashboard instance with the given configuration.
|
||||
func New(config *Config) (*dashboard, error) {
|
||||
return &dashboard{
|
||||
config: config,
|
||||
Metrics: &metricSamples{},
|
||||
closing: make(map[*chan chan error]bool),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Protocols is a meaningless implementation of node.Service.
|
||||
func (db *dashboard) Protocols() []p2p.Protocol { return nil }
|
||||
|
||||
// APIs is a meaningless implementation of node.Service.
|
||||
func (db *dashboard) APIs() []rpc.API { return nil }
|
||||
|
||||
// Start implements node.Service, starting the data collection thread and the listening server of the dashboard.
|
||||
func (db *dashboard) Start(server *p2p.Server) error {
|
||||
go db.collectData()
|
||||
|
||||
http.HandleFunc("/", db.webHandler)
|
||||
http.Handle("/api", websocket.Handler(db.apiHandler))
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", db.config.Host, db.config.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.listener = listener
|
||||
|
||||
go func() {
|
||||
if err := http.Serve(listener, nil); err != nil {
|
||||
log.Warn("Server failed", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
|
||||
func (db *dashboard) Stop() error {
|
||||
db.lock.Lock()
|
||||
defer db.lock.Unlock()
|
||||
|
||||
var err error
|
||||
// Close the connection listener
|
||||
if err = db.listener.Close(); err != nil {
|
||||
log.Warn("Failed to close listener", "err", err)
|
||||
}
|
||||
|
||||
errc := make(chan error)
|
||||
for closing := range db.closing {
|
||||
*closing <- errc
|
||||
<-errc
|
||||
close(*closing)
|
||||
}
|
||||
|
||||
for _, c := range db.conns {
|
||||
if err := c.conn.Close(); err != nil {
|
||||
c.logger.Warn("Failed to close connection", "err", err)
|
||||
}
|
||||
}
|
||||
db.conns = db.conns[:0]
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// webHandler handles all non-api requests, simply flattening and returning the dashboard website.
|
||||
func (db *dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info("Request", "URL", r.URL)
|
||||
|
||||
path := r.URL.String()
|
||||
if path == "/" {
|
||||
path = "/dashboard.html"
|
||||
}
|
||||
|
||||
// If the path of the assets is manually set
|
||||
if db.config.Assets != "" {
|
||||
// Create the filename for ReadFile
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(db.config.Assets)
|
||||
buffer.WriteString(path)
|
||||
|
||||
file, err := ioutil.ReadFile(buffer.String())
|
||||
if err != nil {
|
||||
log.Warn("Failed to read file", "err", err)
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Write(file)
|
||||
return
|
||||
}
|
||||
webapp, err := Asset(path[1:])
|
||||
if err != nil {
|
||||
log.Warn("Failed to load the asset", "path", path, "err", err)
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Write(webapp)
|
||||
}
|
||||
|
||||
// apiHandler handles requests for the dashboard.
|
||||
func (db *dashboard) apiHandler(conn *websocket.Conn) {
|
||||
client := &client{
|
||||
conn: conn,
|
||||
msg: make(chan *map[string]interface{}, 128),
|
||||
logger: log.New("id", atomic.AddUint32(&nextId, 1)),
|
||||
}
|
||||
|
||||
loss := make(chan int)
|
||||
|
||||
// Start listening for messages to send.
|
||||
go func() {
|
||||
closing := db.addClosing()
|
||||
defer db.removeClosing(closing)
|
||||
|
||||
for {
|
||||
select {
|
||||
case errc := <-*closing:
|
||||
errc <- nil
|
||||
return
|
||||
case val := <-loss:
|
||||
loss <- val - 1
|
||||
return
|
||||
case msg := <-client.msg:
|
||||
if err := websocket.JSON.Send(client.conn, msg); err != nil {
|
||||
client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
|
||||
// TODO (kurkomisi): Handle message loss
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Send the past data.
|
||||
client.msg <- &map[string]interface{}{
|
||||
"metrics": db.Metrics,
|
||||
}
|
||||
|
||||
// Start tracking the connection and drop at connection loss.
|
||||
db.lock.Lock()
|
||||
db.conns = append(db.conns, client)
|
||||
db.lock.Unlock()
|
||||
|
||||
go func() {
|
||||
closing := db.addClosing()
|
||||
defer db.removeClosing(closing)
|
||||
|
||||
select {
|
||||
case errc := <-*closing:
|
||||
errc <- nil
|
||||
case val := <-loss:
|
||||
loss <- val - 1
|
||||
db.lock.Lock()
|
||||
for i, c := range db.conns {
|
||||
if c.conn == client.conn {
|
||||
db.conns = append(db.conns[:i], db.conns[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
db.lock.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
fail := []byte{}
|
||||
if _, err := conn.Read(fail); err != nil {
|
||||
loss <- 2
|
||||
if val := <-loss; val > 0 {
|
||||
loss <- val
|
||||
}
|
||||
return
|
||||
}
|
||||
// Ignore all messages
|
||||
}
|
||||
}
|
||||
|
||||
// collectData collects the required data to plot on the dashboard.
|
||||
func (db *dashboard) collectData() {
|
||||
closing := db.addClosing()
|
||||
defer db.removeClosing(closing)
|
||||
|
||||
for {
|
||||
select {
|
||||
case errc := <-*closing:
|
||||
errc <- nil
|
||||
return
|
||||
case <-time.After(db.config.Refresh):
|
||||
now := time.Now()
|
||||
traffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1()
|
||||
memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1()
|
||||
traff := &data{
|
||||
Time: now,
|
||||
Value: traffic,
|
||||
}
|
||||
memory := &data{
|
||||
Time: now,
|
||||
Value: memoryInUse,
|
||||
}
|
||||
|
||||
// TODO (kurkomisi): do not mix traffic with processor!
|
||||
db.update(traff, memory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update updates the dashboards through the live websocket connections.
|
||||
func (db *dashboard) update(processor *data, memory *data) {
|
||||
db.lock.Lock()
|
||||
defer db.lock.Unlock()
|
||||
|
||||
// if the samples' # exceeds the limit, just remove the first element
|
||||
first := 0
|
||||
if len(db.Metrics.Processor) == processorSampleLimit {
|
||||
first = 1
|
||||
}
|
||||
db.Metrics.Processor = append(db.Metrics.Processor[first:], processor)
|
||||
first = 0
|
||||
if len(db.Metrics.Memory) == memorySampleLimit {
|
||||
first = 1
|
||||
}
|
||||
db.Metrics.Memory = append(db.Metrics.Memory[first:], memory)
|
||||
|
||||
msg := &map[string]interface{}{
|
||||
"processor": processor,
|
||||
"memory": memory,
|
||||
}
|
||||
|
||||
for _, c := range db.conns {
|
||||
select {
|
||||
case c.msg <- msg:
|
||||
default:
|
||||
c.logger.Warn("Client message queue is full")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *dashboard) addClosing() *chan chan error {
|
||||
closing := make(chan chan error)
|
||||
db.mapLock.Lock()
|
||||
db.closing[&closing] = true
|
||||
db.mapLock.Unlock()
|
||||
return &closing
|
||||
}
|
||||
|
||||
func (db *dashboard) removeClosing(closing *chan chan error) {
|
||||
db.mapLock.Lock()
|
||||
delete(db.closing, closing)
|
||||
db.mapLock.Unlock()
|
||||
}
|
||||
Loading…
Reference in a new issue