mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-16 17:03:46 +00:00
Merge 32b6ae33ac into 3c8656347f
This commit is contained in:
commit
1712590363
21 changed files with 2258 additions and 7 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -30,3 +30,6 @@ build/_vendor/pkg
|
||||||
# travis
|
# travis
|
||||||
profile.tmp
|
profile.tmp
|
||||||
profile.cov
|
profile.cov
|
||||||
|
|
||||||
|
# dashboard
|
||||||
|
/dashboard/bundler/node_modules/
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
"github.com/ethereum/go-ethereum/contracts/release"
|
"github.com/ethereum/go-ethereum/contracts/release"
|
||||||
|
"github.com/ethereum/go-ethereum/dashboard"
|
||||||
"github.com/ethereum/go-ethereum/eth"
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
"github.com/ethereum/go-ethereum/node"
|
"github.com/ethereum/go-ethereum/node"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
|
|
@ -76,10 +77,11 @@ type ethstatsConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type gethConfig struct {
|
type gethConfig struct {
|
||||||
Eth eth.Config
|
Eth eth.Config
|
||||||
Shh whisper.Config
|
Shh whisper.Config
|
||||||
Node node.Config
|
Node node.Config
|
||||||
Ethstats ethstatsConfig
|
Ethstats ethstatsConfig
|
||||||
|
Dashboard dashboard.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(file string, cfg *gethConfig) error {
|
func loadConfig(file string, cfg *gethConfig) error {
|
||||||
|
|
@ -110,9 +112,10 @@ func defaultNodeConfig() node.Config {
|
||||||
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
|
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
|
||||||
// Load defaults.
|
// Load defaults.
|
||||||
cfg := gethConfig{
|
cfg := gethConfig{
|
||||||
Eth: eth.DefaultConfig,
|
Eth: eth.DefaultConfig,
|
||||||
Shh: whisper.DefaultConfig,
|
Shh: whisper.DefaultConfig,
|
||||||
Node: defaultNodeConfig(),
|
Node: defaultNodeConfig(),
|
||||||
|
Dashboard: dashboard.DefaultConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config file.
|
// Load config file.
|
||||||
|
|
@ -134,6 +137,7 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SetShhConfig(ctx, stack, &cfg.Shh)
|
utils.SetShhConfig(ctx, stack, &cfg.Shh)
|
||||||
|
utils.SetDashboardConfig(ctx, &cfg.Dashboard)
|
||||||
|
|
||||||
return stack, cfg
|
return stack, cfg
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +157,9 @@ func makeFullNode(ctx *cli.Context) *node.Node {
|
||||||
|
|
||||||
utils.RegisterEthService(stack, &cfg.Eth)
|
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
|
// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
|
||||||
shhEnabled := enableWhisper(ctx)
|
shhEnabled := enableWhisper(ctx)
|
||||||
shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name)
|
shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DevModeFlag.Name)
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,11 @@ var (
|
||||||
utils.DataDirFlag,
|
utils.DataDirFlag,
|
||||||
utils.KeyStoreDirFlag,
|
utils.KeyStoreDirFlag,
|
||||||
utils.NoUSBFlag,
|
utils.NoUSBFlag,
|
||||||
|
utils.DashboardEnabledFlag,
|
||||||
|
utils.DashboardAddrFlag,
|
||||||
|
utils.DashboardPortFlag,
|
||||||
|
utils.DashboardRefreshFlag,
|
||||||
|
utils.DashboardAssetsFlag,
|
||||||
utils.EthashCacheDirFlag,
|
utils.EthashCacheDirFlag,
|
||||||
utils.EthashCachesInMemoryFlag,
|
utils.EthashCachesInMemoryFlag,
|
||||||
utils.EthashCachesOnDiskFlag,
|
utils.EthashCachesOnDiskFlag,
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,16 @@ var AppHelpFlagGroups = []flagGroup{
|
||||||
utils.EthashDatasetsOnDiskFlag,
|
utils.EthashDatasetsOnDiskFlag,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "DASHBOARD",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
utils.DashboardEnabledFlag,
|
||||||
|
utils.DashboardAddrFlag,
|
||||||
|
utils.DashboardPortFlag,
|
||||||
|
utils.DashboardRefreshFlag,
|
||||||
|
utils.DashboardAssetsFlag,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "TRANSACTION POOL",
|
Name: "TRANSACTION POOL",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/core/state"
|
"github.com/ethereum/go-ethereum/core/state"
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/dashboard"
|
||||||
"github.com/ethereum/go-ethereum/eth"
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||||
"github.com/ethereum/go-ethereum/eth/gasprice"
|
"github.com/ethereum/go-ethereum/eth/gasprice"
|
||||||
|
|
@ -177,6 +178,31 @@ var (
|
||||||
Name: "lightkdf",
|
Name: "lightkdf",
|
||||||
Usage: "Reduce key-derivation RAM & CPU usage at some expense of KDF strength",
|
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
|
// Ethash settings
|
||||||
EthashCacheDirFlag = DirectoryFlag{
|
EthashCacheDirFlag = DirectoryFlag{
|
||||||
Name: "ethash.cachedir",
|
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.
|
// RegisterEthService adds an Ethereum client to the stack.
|
||||||
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
||||||
var err error
|
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.
|
// RegisterShhService configures Whisper and adds it to the given node.
|
||||||
func RegisterShhService(stack *node.Node, cfg *whisper.Config) {
|
func RegisterShhService(stack *node.Node, cfg *whisper.Config) {
|
||||||
if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) {
|
if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) {
|
||||||
|
|
|
||||||
58
dashboard/README.md
Normal file
58
dashboard/README.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
## Go Ethereum Dashboard
|
||||||
|
### Description
|
||||||
|
|
||||||
|
The dashboard is a data visualizer integrated into geth, intended to collect and visualize useful information of an Ethereum node.
|
||||||
|
The dashboard consists of two parts:
|
||||||
|
* The server listens to connections, collects data with a given refresh rate, and updates the dashboards through the opened connections.
|
||||||
|
* The client waits for update messages, updates the content and tries to reconnect on connection loss.
|
||||||
|
|
||||||
|
### Users
|
||||||
|
#### Installation steps
|
||||||
|
|
||||||
|
1. `cd .../go-ethereum/`
|
||||||
|
1. `go install -v ./cmd/geth`
|
||||||
|
1. Run the server with `geth --rinkeby --dashboard --vmodule=dashboard=5 --metrics`.
|
||||||
|
1. Enter `localhost:8080` (or change the configuration).
|
||||||
|
|
||||||
|
### Developers
|
||||||
|
|
||||||
|
The client's UI is maintained by [Inferno][Inferno], a sympathetic React-like JavaScript library.
|
||||||
|
In order to create the Inferno's virtual DOM using JSX syntax, babel plugin is required.
|
||||||
|
|
||||||
|
[Webpack module bundler][Webpack] is used for bundling the resources in order to gain cost efficiency and maintainability.
|
||||||
|
The resources will be bundled into a single JS file (`bundle.js`), which can be then referenced from the main html file.
|
||||||
|
Finally this JS file will also take part in the `assets.go`.
|
||||||
|
|
||||||
|
[Node.js][Node.js] is used for installing the necessary dependencies for the module bundler.
|
||||||
|
|
||||||
|
#### Installation steps
|
||||||
|
|
||||||
|
_Module bundler_
|
||||||
|
|
||||||
|
1. `cd .../go-ethereum/dashboard/bundler/`
|
||||||
|
1. `npm install`
|
||||||
|
1. `./node_modules/.bin/webpack` // check out `webpack.config.js`
|
||||||
|
|
||||||
|
_Server_
|
||||||
|
|
||||||
|
1. Bundle the resources.
|
||||||
|
1. `cd .../go-ethereum/`.
|
||||||
|
1. `go generate ./dashboard && go install -v ./cmd/geth`.
|
||||||
|
1. Run the server with `geth --rinkeby --dashboard --vmodule=dashboard=5 --metrics console`.
|
||||||
|
* Optionally use `--dashboard.assets=<path>` to set the assets' path (e.g. `--dashboard.assets=".../go-ethereum/dashboard/assets"`).
|
||||||
|
Using this flag it is enough to only bundle the resources with webpack and refresh the page.
|
||||||
|
There is no need for stopping the server and regenerating the `assets.go` on every change of the UI.
|
||||||
|
1. Enter `localhost:8080` (or change the configuration).
|
||||||
|
|
||||||
|
#### Tools
|
||||||
|
[Webpack][Webpack] offers great tools for visualizing the bundle's dependency tree and space usage.
|
||||||
|
|
||||||
|
* Generate the bundle's profile by running `webpack --profile --json > stats.json`
|
||||||
|
* For the _dependency tree_ go to [Webpack Analyze][WA], and import `stats.json`
|
||||||
|
* For the _space usage_ go to [Webpack Visualizer][WV], and import `stats.json`
|
||||||
|
|
||||||
|
[Inferno]: https://infernojs.org/
|
||||||
|
[Webpack]: https://webpack.github.io/
|
||||||
|
[WA]: http://webpack.github.io/analyse/
|
||||||
|
[WV]: http://chrisbateman.github.io/webpack-visualizer/
|
||||||
|
[Node.js]: https://nodejs.org/en/
|
||||||
260
dashboard/assets.go
Normal file
260
dashboard/assets.go
Normal file
File diff suppressed because one or more lines are too long
33
dashboard/assets/dashboard.html
Normal file
33
dashboard/assets/dashboard.html
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!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">
|
||||||
|
|
||||||
|
<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.min.js"></script>
|
||||||
|
<script>
|
||||||
|
Chart.defaults.global.legend = { enabled: false };
|
||||||
|
</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.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="https://unpkg.com/inferno@3.9.0/dist/inferno.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/inferno-component@3.9.0/dist/inferno-component.min.js"></script>
|
||||||
|
|
||||||
|
<div id="dashboard" class="nav-md"></div>
|
||||||
|
<script src="js/bundle.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
dashboard/assets/js/bundle.js
Normal file
1
dashboard/assets/js/bundle.js
Normal file
File diff suppressed because one or more lines are too long
38
dashboard/bundler/package.json
Normal file
38
dashboard/bundler/package.json
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"jshintConfig": {
|
||||||
|
"esversion": 6
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
["es2015", {"loose": true, "modules": false}],
|
||||||
|
"stage-0"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"babel-plugin-syntax-jsx",
|
||||||
|
["babel-plugin-inferno", {"imports": true}]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-eslint": "^7.2.3",
|
||||||
|
"babel-loader": "^7.1.2",
|
||||||
|
"babel-plugin-inferno": "^3.2.0",
|
||||||
|
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||||
|
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
|
"babel-preset-es2015": "^6.24.1",
|
||||||
|
"babel-preset-stage-0": "^6.24.1",
|
||||||
|
"eslint": "^4.5.0",
|
||||||
|
"eslint-config-inferno-app": "^4.2.0",
|
||||||
|
"eslint-loader": "^1.9.0",
|
||||||
|
"glob": "^7.1.2",
|
||||||
|
"inferno-devtools": "^3.8.2",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"url": "^0.11.0",
|
||||||
|
"webpack": "^3.5.5",
|
||||||
|
"webpack-dev-server": "^2.7.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
dashboard/bundler/src/components/Common.js
Normal file
10
dashboard/bundler/src/components/Common.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// isNullOrUndefined returns true if a variable is null or undefined.
|
||||||
|
export const isNullOrUndefined = variable => variable === null || typeof variable === 'undefined';
|
||||||
|
|
||||||
|
export const mapChildren = (children, mapFunc) => !Array.isArray(children) || children.length < 1 ||
|
||||||
|
children.length === 1 ? mapFunc(children) : children.map(mapFunc);
|
||||||
|
|
||||||
|
export const Clearfix = () => <div className="clearfix"/>;
|
||||||
|
|
||||||
|
export const MEMORY_SAMPLE_LIMIT = 200; // Maximum number of memory data samples.
|
||||||
|
export const TRAFFIC_SAMPLE_LIMIT = 200; // Maximum number of traffic data samples.
|
||||||
124
dashboard/bundler/src/components/Dashboard.js
Normal file
124
dashboard/bundler/src/components/Dashboard.js
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import Component from 'component';
|
||||||
|
import {isNullOrUndefined, MEMORY_SAMPLE_LIMIT} from "./Common";
|
||||||
|
import {SideBar} from "./SideBar";
|
||||||
|
import {TopNavigation} from "./TopNavigation";
|
||||||
|
import PageContent from './PageContent';
|
||||||
|
import {Footer} from './Footer';
|
||||||
|
|
||||||
|
// Dashboard is the main component, which renders the whole page,
|
||||||
|
// makes connection with the server and listens for messages.
|
||||||
|
// When there is an incoming message, updates the page's content correspondingly.
|
||||||
|
export default class Dashboard extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
charts: { // Stores the state of the charts.
|
||||||
|
memory: {
|
||||||
|
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: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
traffic: {
|
||||||
|
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: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateCharts Analyzes the incoming message, and updates the charts' content correspondingly.
|
||||||
|
updateCharts = msg => {
|
||||||
|
const memory = this.state.charts.memory;
|
||||||
|
const traffic = this.state.charts.traffic;
|
||||||
|
|
||||||
|
// Fill the dashboard with the past data. metrics is set only in the first msg,
|
||||||
|
// after the connection is established.
|
||||||
|
if (msg.metrics !== undefined) {
|
||||||
|
// Clear the arrays to prevent data confusion with the previous connection.
|
||||||
|
memory.labels = [];
|
||||||
|
traffic.labels = [];
|
||||||
|
memory.datasets[0].data = [];
|
||||||
|
traffic.datasets[0].data = [];
|
||||||
|
|
||||||
|
const mem = msg.metrics.memory;
|
||||||
|
const traff = msg.metrics.processor; // TODO (kurkomisi): !!!
|
||||||
|
|
||||||
|
// Put the past data to the beginning of the arrays. This prevents confusion with the next msg data,
|
||||||
|
// which goes to the end.
|
||||||
|
for (let i = mem.length - 1; i >= 0 && MEMORY_SAMPLE_LIMIT > memory.labels.length; --i) {
|
||||||
|
memory.labels.unshift(mem[i].time.substring(mem[i].time.length - 5));
|
||||||
|
traffic.labels.unshift(mem[i].time.substring(mem[i].time.length - 5));
|
||||||
|
memory.datasets[0].data.unshift(mem[i].value);
|
||||||
|
traffic.datasets[0].data.unshift(traff[i].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({charts: {memory, traffic,}}); // Update the components.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the new data to the end of the arrays.
|
||||||
|
if (msg.memory !== undefined) {
|
||||||
|
// Remove the first elements in case the samples' amount exceeds the limit.
|
||||||
|
if (memory.labels.length === MEMORY_SAMPLE_LIMIT) {
|
||||||
|
memory.labels.shift();
|
||||||
|
traffic.labels.shift();
|
||||||
|
memory.datasets[0].data.shift();
|
||||||
|
traffic.datasets[0].data.shift();
|
||||||
|
}
|
||||||
|
memory.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
|
||||||
|
traffic.labels.push(msg.memory.time.substring(msg.memory.time.length - 5));
|
||||||
|
memory.datasets[0].data.push(msg.memory.value);
|
||||||
|
traffic.datasets[0].data.push(msg.processor.value);
|
||||||
|
|
||||||
|
this.setState({charts: {memory, traffic,}}); // Update the components.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// reconnect establishes a websocket connection with the server, listens for incoming messages
|
||||||
|
// and tries to reconnect on connection loss.
|
||||||
|
reconnect = () => {
|
||||||
|
const server = new WebSocket("ws://" + location.host + "/api");
|
||||||
|
|
||||||
|
server.onmessage = event => {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
if (isNullOrUndefined(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateCharts(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
server.onclose = () => setTimeout(this.reconnect, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
|
||||||
|
componentDidMount = () => this.reconnect();
|
||||||
|
|
||||||
|
// render renders the components of the dashboard.
|
||||||
|
render = () => <div className="container body">
|
||||||
|
<div className="main_container">
|
||||||
|
<SideBar/>
|
||||||
|
<TopNavigation/>
|
||||||
|
<PageContent charts={this.state.charts}/>
|
||||||
|
<Footer/>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
9
dashboard/bundler/src/components/Footer.js
Normal file
9
dashboard/bundler/src/components/Footer.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {Clearfix} from "./Common";
|
||||||
|
|
||||||
|
// Footer renders a footer component.
|
||||||
|
export const Footer = () => <footer>
|
||||||
|
<div className="pull-right">
|
||||||
|
Copyright 2017 The go-ethereum Authors
|
||||||
|
</div>
|
||||||
|
<Clearfix/>
|
||||||
|
</footer>;
|
||||||
81
dashboard/bundler/src/components/PageContent.js
Normal file
81
dashboard/bundler/src/components/PageContent.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import Component from 'component';
|
||||||
|
import {isNullOrUndefined, mapChildren, Clearfix} from "./Common";
|
||||||
|
|
||||||
|
// Chart name is already in use.
|
||||||
|
// ChartComponent renders a chart component and updates it, when the related data changes.
|
||||||
|
class ChartComponent extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => this.state.chart = new Chart(this.data, {
|
||||||
|
type: this.props.type,
|
||||||
|
data: this.props.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
render = () => {
|
||||||
|
if (!isNullOrUndefined(this.state.chart)) {
|
||||||
|
this.state.chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={this.props.className}>
|
||||||
|
<div className="x_panel">
|
||||||
|
<div className="x_title">
|
||||||
|
<h2>{this.props.text}</h2>
|
||||||
|
<ul className="nav navbar-right panel_toolbox">
|
||||||
|
<li><a className="collapse-link"><i className="fa fa-chevron-up"/></a></li>
|
||||||
|
{
|
||||||
|
// Render dropdown menu only if there are children.
|
||||||
|
isNullOrUndefined(this.props.children) || <li className="dropdown">
|
||||||
|
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button"
|
||||||
|
aria-expanded="false"><i className="fa fa-wrench"/></a>
|
||||||
|
<ul className="dropdown-menu" role="menu">
|
||||||
|
{mapChildren(this.props.children, child => <li>{child}</li>)}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
<li><a className="close-link"><i className="fa fa-close"/></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Clearfix/>
|
||||||
|
</div>
|
||||||
|
<div className="x_content">
|
||||||
|
{/* The chart will be generated here after the component is mounted (this.componentDidMount). */}
|
||||||
|
<canvas ref={data => this.data = data}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row renders a row component of charts only if there is any chart.
|
||||||
|
class Row extends Component {
|
||||||
|
render = () => isNullOrUndefined(this.props.children) || <div className="row"> {this.props.children} </div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageContent renders a component for the page content.
|
||||||
|
export default class PageContent extends Component {
|
||||||
|
render = () => <div className="right_col" role="main">
|
||||||
|
<div className="">
|
||||||
|
<div className="page-title">
|
||||||
|
<div className="title_left">
|
||||||
|
<h3>Go Ethereum Dashboard
|
||||||
|
<small>Statistics</small>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Clearfix/>
|
||||||
|
<Row>
|
||||||
|
<ChartComponent className="col-md-6 col-sm-6 col-xs-12" text="Memory usage system/memory/inuse"
|
||||||
|
type="line" data={this.props.charts.memory}>
|
||||||
|
<a href="#">Settings 1</a>
|
||||||
|
<a href="#">Settings 2</a>
|
||||||
|
</ChartComponent>
|
||||||
|
<ChartComponent className="col-md-6 col-sm-6 col-xs-12" text="Inbound traffic p2p/InboundTraffic"
|
||||||
|
type="line" data={this.props.charts.traffic}/>
|
||||||
|
</Row>
|
||||||
|
<Clearfix/>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
60
dashboard/bundler/src/components/SideBar.js
Normal file
60
dashboard/bundler/src/components/SideBar.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import Component from 'component';
|
||||||
|
import {isNullOrUndefined, mapChildren, Clearfix} from "./Common";
|
||||||
|
|
||||||
|
// MenuItem renders an item for a Menu component and the belonging submenu items, if there is any.
|
||||||
|
class MenuItem extends Component {
|
||||||
|
render = () => <li>
|
||||||
|
<a>
|
||||||
|
<i className={`fa ${this.props.className}`}/>
|
||||||
|
{this.props.text}
|
||||||
|
<div className="fa fa-chevron-down"/>
|
||||||
|
</a>
|
||||||
|
{
|
||||||
|
// Render dropdown menu only if there are children.
|
||||||
|
isNullOrUndefined(this.props.children) || <ul className="nav child_menu">
|
||||||
|
{mapChildren(this.props.children, child => <li>{child}</li>)}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</li>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu renders a menu component.
|
||||||
|
const Menu = () => <ul className="nav side-menu">
|
||||||
|
<MenuItem className="fa-home" text="Home">
|
||||||
|
<a href="dashboard1.html">Dashboard1</a>
|
||||||
|
<a href="dashboard2.html">Dashboard2</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-edit" text="Networking">
|
||||||
|
<a href="networking.html">Networking</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-desktop" text="Txpool">
|
||||||
|
<a href="txpool.html">Txpool</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-table" text="Logs">
|
||||||
|
<a href="logs.html">Logs</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-clone" text="Blockchain">
|
||||||
|
<a href="blockchain1.html">Blockchain1</a>
|
||||||
|
<a href="blockchain2.html">Blockchain2</a>
|
||||||
|
<a href="blockchain3.html">Blockchain3</a>
|
||||||
|
<a href="blockchain4.html">Blockchain4</a>
|
||||||
|
<a href="blockchain5.html">Blockchain5</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="fa-bar-chart-o" text="System"/>
|
||||||
|
</ul>;
|
||||||
|
|
||||||
|
// SideBar renders a sidebar component.
|
||||||
|
export const SideBar = () => <div className="col-md-3 left_col">
|
||||||
|
<div className="left_col scroll-view">
|
||||||
|
<div className="navbar nav_title" style={{border: 0}}>
|
||||||
|
<a href="dashboard.html" className="site_title"><i className="fa fa-paw"/>
|
||||||
|
<span>Go Ethereum Dashboard</span></a>
|
||||||
|
</div>
|
||||||
|
<Clearfix/>
|
||||||
|
<div id="sidebar-menu" className="main_menu_side hidden-print main_menu">
|
||||||
|
<div className="menu_section">
|
||||||
|
<Menu/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
12
dashboard/bundler/src/components/TopNavigation.js
Normal file
12
dashboard/bundler/src/components/TopNavigation.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// TopNavigation renders a top navigation component.
|
||||||
|
export const TopNavigation = () => <div className="top_nav">
|
||||||
|
<div className="nav_menu">
|
||||||
|
<nav className="" role="navigation">
|
||||||
|
<div className="nav toggle">
|
||||||
|
<a id="_______menu_toggle"> {/* TODO (kurkomisi): Resize the main container on toggle */}
|
||||||
|
<i className="fa fa-bars"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
5
dashboard/bundler/src/index.js
Normal file
5
dashboard/bundler/src/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Inferno from 'inferno';
|
||||||
|
import Dashboard from './components/Dashboard';
|
||||||
|
|
||||||
|
// Renders the whole dashboard.
|
||||||
|
Inferno.render(<Dashboard/>, document.getElementById('dashboard'));
|
||||||
1103
dashboard/bundler/stats.json
Normal file
1103
dashboard/bundler/stats.json
Normal file
File diff suppressed because one or more lines are too long
60
dashboard/bundler/webpack.config.js
Normal file
60
dashboard/bundler/webpack.config.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
|
name: 'main', // Move dependencies to our main file.
|
||||||
|
children: true, // Look for common dependencies in all children,
|
||||||
|
minChunks: 2, // How many times a dependency must come up before being extracted
|
||||||
|
}),
|
||||||
|
|
||||||
|
// This plugins optimizes chunks and modules by
|
||||||
|
// how much they are used in your app.
|
||||||
|
new webpack.optimize.OccurrenceOrderPlugin(),
|
||||||
|
|
||||||
|
// This plugin prevents Webpack from creating chunks
|
||||||
|
// that would be too small to be worth loading separately.
|
||||||
|
new webpack.optimize.MinChunkSizePlugin({
|
||||||
|
minChunkSize: 51200, // ~50kb
|
||||||
|
}),
|
||||||
|
|
||||||
|
// This plugin minifies all the Javascript code of the final bundle.
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
mangle: true,
|
||||||
|
compress: {
|
||||||
|
warnings: false, // Suppress uglification warnings
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// This plugin defines various variables that we can set to false
|
||||||
|
// to avoid code related to them from being compiled in the final bundle.
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
__SERVER__: false,
|
||||||
|
__DEVELOPMENT__: false,
|
||||||
|
__DEVTOOLS__: false,
|
||||||
|
'process.env': {
|
||||||
|
BABEL_ENV: JSON.stringify(process.env.NODE_ENV),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/index.js',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, '../assets/js'),
|
||||||
|
filename: 'bundle.js',
|
||||||
|
},
|
||||||
|
plugins: plugins,
|
||||||
|
externals: { // External libraries, which will not be included in the bundled file(s).
|
||||||
|
inferno: 'Inferno',
|
||||||
|
component: 'Inferno.Component',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.js$/, // regexp for JS files
|
||||||
|
loader: 'babel-loader', // The babel configuration is in the package.json.
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
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"`
|
||||||
|
}
|
||||||
286
dashboard/dashboard.go
Normal file
286
dashboard/dashboard.go
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
// 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 map[uint32]*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 chan chan error // Channel used for graceful exit
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
|
conns: make(map[uint32]*client),
|
||||||
|
config: config,
|
||||||
|
Metrics: &metricSamples{},
|
||||||
|
closing: make(chan chan error),
|
||||||
|
}, 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 {
|
||||||
|
db.wg.Add(1)
|
||||||
|
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 http.Serve(listener, nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
|
||||||
|
func (db *dashboard) Stop() error {
|
||||||
|
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)
|
||||||
|
db.closing <- errc
|
||||||
|
<-errc
|
||||||
|
|
||||||
|
db.lock.Lock()
|
||||||
|
for _, c := range db.conns {
|
||||||
|
if err := c.conn.Close(); err != nil {
|
||||||
|
c.logger.Warn("Failed to close connection", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.lock.Unlock()
|
||||||
|
|
||||||
|
db.wg.Wait()
|
||||||
|
log.Info("Dashboard stopped")
|
||||||
|
|
||||||
|
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) {
|
||||||
|
id := atomic.AddUint32(&nextId, 1)
|
||||||
|
client := &client{
|
||||||
|
conn: conn,
|
||||||
|
msg: make(chan *map[string]interface{}, 128),
|
||||||
|
logger: log.New("id", id),
|
||||||
|
}
|
||||||
|
|
||||||
|
loss := make(chan bool, 1) // Buffered channel as sender may exit early
|
||||||
|
|
||||||
|
// Start listening for messages to send.
|
||||||
|
db.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer db.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-loss:
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 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[id] = client
|
||||||
|
db.lock.Unlock()
|
||||||
|
defer func() {
|
||||||
|
db.lock.Lock()
|
||||||
|
delete(db.conns, id)
|
||||||
|
db.lock.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
fail := []byte{}
|
||||||
|
if _, err := conn.Read(fail); err != nil {
|
||||||
|
loss <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Ignore all messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectData collects the required data to plot on the dashboard.
|
||||||
|
func (db *dashboard) collectData() {
|
||||||
|
defer db.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case errc := <-db.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) {
|
||||||
|
// Remove the first elements in case the samples' amount exceeds the limit.
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
db.lock.Lock()
|
||||||
|
for _, c := range db.conns {
|
||||||
|
select {
|
||||||
|
case c.msg <- msg:
|
||||||
|
default:
|
||||||
|
c.logger.Warn("Client message queue is full")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.lock.Unlock()
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue