mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-17 09:23:48 +00:00
cmd, dashboard, metrics: initial proof of concept dashboard
This commit is contained in:
parent
523ff69b23
commit
c48c55fedb
19 changed files with 1794 additions and 1488 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -36,3 +36,5 @@ profile.cov
|
||||||
|
|
||||||
# dashboard
|
# dashboard
|
||||||
/dashboard/assets/node_modules
|
/dashboard/assets/node_modules
|
||||||
|
/dashboard/assets/stats.json
|
||||||
|
/dashboard/assets/public/bundle.js
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,7 @@ var (
|
||||||
}
|
}
|
||||||
DashboardAssetsFlag = cli.StringFlag{
|
DashboardAssetsFlag = cli.StringFlag{
|
||||||
Name: "dashboard.assets",
|
Name: "dashboard.assets",
|
||||||
Usage: "Developer flag to serve the dashboard from the local file system (default: \"\")",
|
Usage: "Developer flag to serve the dashboard from the local file system",
|
||||||
Value: dashboard.DefaultConfig.Assets,
|
Value: dashboard.DefaultConfig.Assets,
|
||||||
}
|
}
|
||||||
// Ethash settings
|
// Ethash settings
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,41 @@
|
||||||
## Go Ethereum Dashboard
|
## 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 is a data visualizer integrated into geth, intended to collect and visualize useful information of an Ethereum node.
|
||||||
The dashboard consists of two parts:
|
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 visualizes the collected data.
|
||||||
* The client waits for update messages, updates the content and tries to reconnect on connection loss.
|
* The server collects the data, and updates the clients.
|
||||||
|
|
||||||
### Users
|
The client's UI uses [React][React] with JSX syntax, which is validated by the [ESLint][ESLint] linter
|
||||||
#### Installation steps
|
mostly according to the [Airbnb React/JSX Style Guide][Airbnb]. The style is defined in the `.eslintrc` configuration file.
|
||||||
|
The resources are bundled into a single `bundle.js` file using [Webpack][Webpack], which relies on the `webpack.config.js`.
|
||||||
|
The bundled file is referenced from `dashboard.html` and takes part in the `assets.go` too.
|
||||||
|
The necessary dependencies for the module bundler are gathered by [Node.js][Node.js].
|
||||||
|
|
||||||
1. `cd .../go-ethereum/`
|
### Install and run the server
|
||||||
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 [React][React], the facebook's JavaScript library.
|
|
||||||
|
|
||||||
The [ESLint pluggable linting utility][ESLint] validates the JSX syntax mostly according to the [Airbnb React/JSX Style Guide][Airbnb], the style is defined in the `.eslintrc` configuration file.
|
|
||||||
|
|
||||||
[Webpack module bundler][Webpack] is used for bundling the resources in order to gain cost efficiency and maintainability.
|
|
||||||
The resources are bundled into a single JS file (`bundle.js`), which is referenced from the main html file.
|
|
||||||
This JS file also takes 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/assets/`
|
|
||||||
1. `npm install`
|
|
||||||
1. `./node_modules/.bin/webpack` // check out `webpack.config.js`
|
|
||||||
* Optionally use `--watch` to automatically bundle the resources on change.
|
|
||||||
|
|
||||||
_Server_
|
|
||||||
|
|
||||||
1. Bundle the resources.
|
|
||||||
1. `cd .../go-ethereum/`.
|
|
||||||
1. `go generate ./dashboard && go install -v ./cmd/geth`.
|
1. `go generate ./dashboard && go install -v ./cmd/geth`.
|
||||||
1. Run the server with `geth --rinkeby --dashboard --vmodule=dashboard=5 --metrics console`.
|
1. `geth --dashboard --vmodule=dashboard=5`.
|
||||||
* Optionally use `--dashboard.assets=<path>` to set the assets' path (e.g. `--dashboard.assets=".../go-ethereum/dashboard/assets/public"`).
|
|
||||||
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
|
During the development use the `--dashboard.assets=<absolute path>` flag to set the assets' path
|
||||||
[Webpack][Webpack] offers great tools for visualizing the bundle's dependency tree and space usage.
|
(e.g. `geth --rinkeby --dashboard --dashboard.assets="<path>/dashboard/assets/public" --vmodule=dashboard=5 console`).
|
||||||
|
This way there is no need to stop and regenerate the server to modify the client.
|
||||||
|
|
||||||
* Generate the bundle's profile by running `webpack --profile --json > stats.json`
|
### Install the module bundler
|
||||||
|
|
||||||
|
1. `cd dashboard/assets`
|
||||||
|
1. `npm install`
|
||||||
|
|
||||||
|
### Bundle the resources
|
||||||
|
|
||||||
|
1. `cd dashboard/assets`
|
||||||
|
1. `./node_modules/.bin/webpack`
|
||||||
|
1. Enter `localhost:8080` to check the result
|
||||||
|
|
||||||
|
### Have fun
|
||||||
|
|
||||||
|
[Webpack][Webpack] offers handy tools for visualizing the bundle's dependency tree and space usage.
|
||||||
|
|
||||||
|
* Generate the bundle's profile running `webpack --profile --json > stats.json`
|
||||||
* For the _dependency tree_ go to [Webpack Analyze][WA], and import `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`
|
* For the _space usage_ go to [Webpack Visualizer][WV], and import `stats.json`
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
// React syntax style mostly according to https://github.com/airbnb/javascript/tree/master/react
|
// React syntax style mostly according to https://github.com/airbnb/javascript/tree/master/react
|
||||||
{
|
{
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|
@ -26,6 +42,11 @@
|
||||||
"react/self-closing-comp": 2,
|
"react/self-closing-comp": 2,
|
||||||
"react/jsx-no-bind": 2,
|
"react/jsx-no-bind": 2,
|
||||||
"react/require-render-return": 2,
|
"react/require-render-return": 2,
|
||||||
"react/no-is-mounted": 2
|
"react/no-is-mounted": 2,
|
||||||
|
"key-spacing": ["error", {"align": {
|
||||||
|
"beforeColon": false,
|
||||||
|
"afterColon": true,
|
||||||
|
"on": "value"
|
||||||
|
}}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,21 +1,36 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
// isNullOrUndefined returns true if the given variable is null or undefined.
|
// isNullOrUndefined returns true if the given variable is null or undefined.
|
||||||
export const isNullOrUndefined = variable => variable === null || typeof variable === 'undefined';
|
export const isNullOrUndefined = variable => variable === null || typeof variable === 'undefined';
|
||||||
|
|
||||||
// defaultZero returns 0 if the given element is null or undefined, otherwise returns the given element.
|
export const LIMIT = {
|
||||||
export const defaultZero = elem => isNullOrUndefined(elem) ? 0 : elem;
|
memory: 200, // Maximum number of memory data samples.
|
||||||
|
traffic: 200, // Maximum number of traffic data samples.
|
||||||
export const MEMORY_SAMPLE_LIMIT = 200; // Maximum number of memory data samples.
|
log: 200, // Maximum number of logs.
|
||||||
export const TRAFFIC_SAMPLE_LIMIT = 200; // Maximum number of traffic data samples.
|
};
|
||||||
|
|
||||||
// The sidebar menu and the main content are rendered based on these elements.
|
// The sidebar menu and the main content are rendered based on these elements.
|
||||||
export const TAGS = (() => {
|
export const TAGS = (() => {
|
||||||
const T = {
|
const T = {
|
||||||
home: { title: "Home", },
|
home: { title: "Home", },
|
||||||
logs: { title: "Logs", },
|
chain: { title: "Chain", },
|
||||||
networking: { title: "Networking", },
|
transactions: { title: "Transactions", },
|
||||||
txpool: { title: "Txpool", },
|
network: { title: "Network", },
|
||||||
blockchain: { title: "Blockchain", },
|
|
||||||
system: { title: "System", },
|
system: { title: "System", },
|
||||||
|
logs: { title: "Logs", },
|
||||||
};
|
};
|
||||||
// Using the key is circumstantial in some cases, so it is better to insert it also as a value.
|
// Using the key is circumstantial in some cases, so it is better to insert it also as a value.
|
||||||
// This way the mistyping is prevented.
|
// This way the mistyping is prevented.
|
||||||
|
|
@ -25,5 +40,13 @@ export const TAGS = (() => {
|
||||||
return T;
|
return T;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
export const DATA_KEYS = (() => {
|
||||||
|
const DK = {};
|
||||||
|
["memory", "traffic", "logs"].map(key => {
|
||||||
|
DK[key] = key;
|
||||||
|
});
|
||||||
|
return DK;
|
||||||
|
})();
|
||||||
|
|
||||||
// Temporary - taken from Material-UI
|
// Temporary - taken from Material-UI
|
||||||
export const DRAWER_WIDTH = 240;
|
export const DRAWER_WIDTH = 240;
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {withStyles} from 'material-ui/styles';
|
import {withStyles} from 'material-ui/styles';
|
||||||
|
|
@ -5,7 +21,7 @@ import {withStyles} from 'material-ui/styles';
|
||||||
import SideBar from './SideBar.jsx';
|
import SideBar from './SideBar.jsx';
|
||||||
import Header from './Header.jsx';
|
import Header from './Header.jsx';
|
||||||
import Main from "./Main.jsx";
|
import Main from "./Main.jsx";
|
||||||
import {isNullOrUndefined, defaultZero, MEMORY_SAMPLE_LIMIT, TAGS} from "./Common.jsx";
|
import {isNullOrUndefined, LIMIT, TAGS, DATA_KEYS,} from "./Common.jsx";
|
||||||
|
|
||||||
// Styles for the Dashboard component.
|
// Styles for the Dashboard component.
|
||||||
const styles = theme => ({
|
const styles = theme => ({
|
||||||
|
|
@ -14,12 +30,11 @@ const styles = theme => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: '#303030',
|
background: theme.palette.background.default,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dashboard is the main component, which renders the whole page,
|
// Dashboard is the main component, which renders the whole page, makes connection with the server and listens for messages.
|
||||||
// makes connection with the server and listens for messages.
|
|
||||||
// When there is an incoming message, updates the page's content correspondingly.
|
// When there is an incoming message, updates the page's content correspondingly.
|
||||||
class Dashboard extends Component {
|
class Dashboard extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
@ -30,6 +45,7 @@ class Dashboard extends Component {
|
||||||
memory: [],
|
memory: [],
|
||||||
traffic: [],
|
traffic: [],
|
||||||
logs: [],
|
logs: [],
|
||||||
|
shouldUpdate: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +57,7 @@ class Dashboard extends Component {
|
||||||
// reconnect establishes a websocket connection with the server, listens for incoming messages
|
// reconnect establishes a websocket connection with the server, listens for incoming messages
|
||||||
// and tries to reconnect on connection loss.
|
// and tries to reconnect on connection loss.
|
||||||
reconnect = () => {
|
reconnect = () => {
|
||||||
const server = new WebSocket("ws://" + location.host + "/api");
|
const server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api");
|
||||||
|
|
||||||
server.onmessage = event => {
|
server.onmessage = event => {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
|
|
@ -58,50 +74,56 @@ class Dashboard extends Component {
|
||||||
|
|
||||||
// update analyzes the incoming message, and updates the charts' content correspondingly.
|
// update analyzes the incoming message, and updates the charts' content correspondingly.
|
||||||
update = msg => {
|
update = msg => {
|
||||||
// (Re)initialize the state with the past data. metrics is set only in the first msg,
|
console.log(msg);
|
||||||
// after the connection is established.
|
this.setState(prevState => {
|
||||||
if (!isNullOrUndefined(msg.metrics)) {
|
let newState = [];
|
||||||
let memory = [];
|
newState.shouldUpdate = {};
|
||||||
let traffic = [];
|
const insert = (key, values, limit) => {
|
||||||
if (!isNullOrUndefined(msg.metrics.memory)) {
|
newState[key] = [...prevState[key], ...values];
|
||||||
memory = msg.metrics.memory.map(elem => ({memory: elem.value}));
|
while (newState[key].length > limit) {
|
||||||
traffic = msg.metrics.processor.map(elem => ({traffic: defaultZero(elem.value)})) // TODO (kurkomisi): traffic != processor!!!
|
newState[key].shift();
|
||||||
|
}
|
||||||
|
newState.shouldUpdate[key] = true;
|
||||||
|
};
|
||||||
|
// (Re)initialize the state with the past data.
|
||||||
|
if (!isNullOrUndefined(msg.history)) {
|
||||||
|
const memory = DATA_KEYS.memory;
|
||||||
|
const traffic = DATA_KEYS.traffic;
|
||||||
|
newState[memory] = [];
|
||||||
|
newState[traffic] = [];
|
||||||
|
if (!isNullOrUndefined(msg.history.memorySamples)) {
|
||||||
|
newState[memory] = msg.history.memorySamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value);
|
||||||
|
while (newState[memory].length > LIMIT.memory) {
|
||||||
|
newState[memory].shift();
|
||||||
|
}
|
||||||
|
newState.shouldUpdate[memory] = true;
|
||||||
|
}
|
||||||
|
if (!isNullOrUndefined(msg.history.trafficSamples)) {
|
||||||
|
newState[traffic] = msg.history.trafficSamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value);
|
||||||
|
while (newState[traffic].length > LIMIT.traffic) {
|
||||||
|
newState[traffic].shift();
|
||||||
|
}
|
||||||
|
newState.shouldUpdate[traffic] = true;
|
||||||
}
|
}
|
||||||
this.setState({
|
|
||||||
memory: memory,
|
|
||||||
traffic: traffic,
|
|
||||||
logs: [],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the new data samples.
|
// Insert the new data samples.
|
||||||
isNullOrUndefined(msg.memory) || this.setState(prevState => {
|
if (!isNullOrUndefined(msg.memory)) {
|
||||||
let memory = prevState.memory;
|
insert(DATA_KEYS.memory, [isNullOrUndefined(msg.memory.value) ? 0 : msg.memory.value], LIMIT.memory);
|
||||||
let traffic = prevState.traffic;
|
}
|
||||||
// Remove the first elements in case the samples' amount exceeds the limit.
|
if (!isNullOrUndefined(msg.traffic)) {
|
||||||
if (memory.length === MEMORY_SAMPLE_LIMIT) {
|
insert(DATA_KEYS.traffic, [isNullOrUndefined(msg.traffic.value) ? 0 : msg.traffic.value], LIMIT.traffic);
|
||||||
memory.shift();
|
}
|
||||||
traffic.shift();
|
if (!isNullOrUndefined(msg.log)) {
|
||||||
|
insert(DATA_KEYS.logs, [msg.log], LIMIT.log);
|
||||||
}
|
}
|
||||||
return ({
|
|
||||||
memory: [...memory, {memory: msg.memory.value}],
|
|
||||||
traffic: [...traffic, {traffic: defaultZero(msg.processor.value)}],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Insert the new log.
|
return newState;
|
||||||
isNullOrUndefined(msg.log) || this.setState(prevState => {
|
|
||||||
let logs = prevState.logs;
|
|
||||||
if(logs.length > 20) {
|
|
||||||
logs.shift();
|
|
||||||
}
|
|
||||||
return {logs: [...logs, msg.log]};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// The change of the active label on the SideBar component will trigger a new render in the Main component.
|
// The change of the active label on the SideBar component will trigger a new render in the Main component.
|
||||||
changeContent = active => {
|
changeContent = active => {
|
||||||
this.state.active === active || this.setState({active: active});
|
this.setState(prevState => prevState.active !== active ? {active: active} : {});
|
||||||
};
|
};
|
||||||
|
|
||||||
openSideBar = () => {
|
openSideBar = () => {
|
||||||
|
|
@ -133,6 +155,7 @@ class Dashboard extends Component {
|
||||||
memory={this.state.memory}
|
memory={this.state.memory}
|
||||||
traffic={this.state.traffic}
|
traffic={this.state.traffic}
|
||||||
logs={this.state.logs}
|
logs={this.state.logs}
|
||||||
|
shouldUpdate={this.state.shouldUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
|
||||||
89
dashboard/assets/components/Home.jsx
Normal file
89
dashboard/assets/components/Home.jsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Grid from 'material-ui/Grid';
|
||||||
|
import {LineChart, AreaChart, Area, YAxis, CartesianGrid, Line, ResponsiveContainer} from 'recharts';
|
||||||
|
import {withTheme} from 'material-ui/styles';
|
||||||
|
|
||||||
|
import {isNullOrUndefined, DATA_KEYS} from "./Common.jsx";
|
||||||
|
|
||||||
|
// ChartGrid renders a grid container for responsive charts.
|
||||||
|
// The children are Recharts components extended with the Material-UI's xs property.
|
||||||
|
class ChartGrid extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={this.props.spacing}>
|
||||||
|
{
|
||||||
|
React.Children.map(this.props.children, child => (
|
||||||
|
<Grid item xs={child.props.xs}>
|
||||||
|
<ResponsiveContainer width="100%" height={child.props.height}>
|
||||||
|
{React.cloneElement(child, {data: child.props.values.map(value => ({value: value}))})}
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Grid>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartGrid.propTypes = {
|
||||||
|
spacing: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Home renders the home component.
|
||||||
|
class Home extends Component {
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
return !isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.memory]) ||
|
||||||
|
!isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.traffic]);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {theme} = this.props;
|
||||||
|
const memoryColor = theme.palette.primary[300];
|
||||||
|
const trafficColor = theme.palette.secondary[300];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartGrid spacing={24}>
|
||||||
|
<AreaChart xs={6} height={300} values={this.props.memory}>
|
||||||
|
<YAxis />
|
||||||
|
<Area type="monotone" dataKey="value" stroke={memoryColor} fill={memoryColor} />
|
||||||
|
</AreaChart>
|
||||||
|
<LineChart xs={6} height={300} values={this.props.traffic}>
|
||||||
|
<Line type="monotone" dataKey="value" stroke={trafficColor} dot={false} />
|
||||||
|
</LineChart>
|
||||||
|
<LineChart xs={6} height={300} values={this.props.memory}>
|
||||||
|
<YAxis />
|
||||||
|
<CartesianGrid stroke="#eee" strokeDasharray="5 5" />
|
||||||
|
<Line type="monotone" dataKey="value" stroke={memoryColor} dot={false} />
|
||||||
|
</LineChart>
|
||||||
|
<AreaChart xs={6} height={300} values={this.props.traffic}>
|
||||||
|
<CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
|
||||||
|
<Area type="monotone" dataKey="value" stroke={trafficColor} fill={trafficColor} />
|
||||||
|
</AreaChart>
|
||||||
|
</ChartGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Home.propTypes = {
|
||||||
|
theme: PropTypes.object.isRequired,
|
||||||
|
shouldUpdate: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTheme()(Home);
|
||||||
|
|
@ -1,96 +1,44 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {withStyles} from 'material-ui/styles';
|
import {withStyles} from 'material-ui/styles';
|
||||||
import Grid from 'material-ui/Grid';
|
|
||||||
import {LineChart, AreaChart, Area, YAxis, CartesianGrid, Line, ResponsiveContainer} from 'recharts';
|
|
||||||
|
|
||||||
import {TAGS, DRAWER_WIDTH} from "./Common.jsx";
|
import {TAGS, DRAWER_WIDTH} from "./Common.jsx";
|
||||||
|
import Home from './Home.jsx';
|
||||||
// ChartGrid renders a grid container for responsive charts.
|
|
||||||
// The children are Recharts components extended with the Material-UI's xs property.
|
|
||||||
class ChartGrid extends Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Grid container spacing={this.props.spacing}>
|
|
||||||
{
|
|
||||||
React.Children.map(this.props.children, child => (
|
|
||||||
<Grid item xs={child.props.xs}>
|
|
||||||
<ResponsiveContainer width="100%" height={child.props.height}>
|
|
||||||
{child}
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Grid>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChartGrid.propTypes = {
|
|
||||||
spacing: PropTypes.number.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ContentSwitch chooses and renders the proper page content.
|
// ContentSwitch chooses and renders the proper page content.
|
||||||
class ContentSwitch extends Component {
|
class ContentSwitch extends Component {
|
||||||
render() {
|
render() {
|
||||||
switch(this.props.active) {
|
switch(this.props.active) {
|
||||||
case TAGS.home.id:
|
case TAGS.home.id:
|
||||||
return (
|
return <Home memory={this.props.memory} traffic={this.props.traffic} shouldUpdate={this.props.shouldUpdate} />;
|
||||||
<ChartGrid spacing={24}>
|
case TAGS.chain.id:
|
||||||
<AreaChart xs={6} height={300} data={this.props.memory}>
|
return null;
|
||||||
<YAxis />
|
case TAGS.transactions.id:
|
||||||
<Area type="monotone" dataKey="memory" stroke="#8884d8" fill="#8884d8" />
|
return null;
|
||||||
</AreaChart>
|
case TAGS.network.id:
|
||||||
<LineChart xs={6} height={300} data={this.props.traffic}>
|
// Only for testing.
|
||||||
<Line type="monotone" dataKey="traffic" dot={false} />
|
return null;
|
||||||
</LineChart>
|
case TAGS.system.id:
|
||||||
<LineChart xs={6} height={300} data={this.props.memory}>
|
return null;
|
||||||
<YAxis />
|
|
||||||
<CartesianGrid stroke="#eee" strokeDasharray="5 5" />
|
|
||||||
<Line type="monotone" dataKey="memory" stroke="#8884d8" dot={false} />
|
|
||||||
</LineChart>
|
|
||||||
<AreaChart xs={6} height={300} data={this.props.traffic}>
|
|
||||||
<CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
|
|
||||||
<Area type="monotone" dataKey="traffic" />
|
|
||||||
</AreaChart>
|
|
||||||
</ChartGrid>
|
|
||||||
);
|
|
||||||
case TAGS.logs.id:
|
case TAGS.logs.id:
|
||||||
return <div>{this.props.logs.map((log, index) => <div key={index}>{log}</div>)}</div>;
|
return <div>{this.props.logs.map((log, index) => <div key={index}>{log}</div>)}</div>;
|
||||||
case TAGS.networking.id:
|
|
||||||
// Only for testing.
|
|
||||||
return (
|
|
||||||
<Grid container spacing={24}>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<LineChart data={this.props.traffic}>
|
|
||||||
<Line type="monotone" dataKey="traffic" dot={false} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<AreaChart data={this.props.memory}>
|
|
||||||
<YAxis />
|
|
||||||
<Area type="monotone" dataKey="memory" stroke="#8884d8" fill="#8884d8" />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<AreaChart data={this.props.traffic}>
|
|
||||||
<CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
|
|
||||||
<Area type="monotone" dataKey="traffic" />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
);
|
|
||||||
case TAGS.txpool.id:
|
|
||||||
case TAGS.blockchain.id:
|
|
||||||
case TAGS.system.id:
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -98,9 +46,10 @@ class ContentSwitch extends Component {
|
||||||
|
|
||||||
ContentSwitch.propTypes = {
|
ContentSwitch.propTypes = {
|
||||||
active: PropTypes.string.isRequired,
|
active: PropTypes.string.isRequired,
|
||||||
|
shouldUpdate: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Styles for the Main component.
|
// styles contains the styles for the Main component.
|
||||||
const styles = theme => ({
|
const styles = theme => ({
|
||||||
content: {
|
content: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -143,6 +92,7 @@ class Main extends Component {
|
||||||
memory={this.props.memory}
|
memory={this.props.memory}
|
||||||
traffic={this.props.traffic}
|
traffic={this.props.traffic}
|
||||||
logs={this.props.logs}
|
logs={this.props.logs}
|
||||||
|
shouldUpdate={this.props.shouldUpdate}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
@ -153,6 +103,7 @@ Main.propTypes = {
|
||||||
classes: PropTypes.object.isRequired,
|
classes: PropTypes.object.isRequired,
|
||||||
opened: PropTypes.bool.isRequired,
|
opened: PropTypes.bool.isRequired,
|
||||||
active: PropTypes.string.isRequired,
|
active: PropTypes.string.isRequired,
|
||||||
|
shouldUpdate: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withStyles(styles)(Main);
|
export default withStyles(styles)(Main);
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {withStyles} from 'material-ui/styles';
|
import {withStyles} from 'material-ui/styles';
|
||||||
|
|
@ -39,8 +55,9 @@ class SideBar extends Component {
|
||||||
this.clickOn = {};
|
this.clickOn = {};
|
||||||
for(let key in TAGS) {
|
for(let key in TAGS) {
|
||||||
const id = TAGS[key].id;
|
const id = TAGS[key].id;
|
||||||
this.clickOn[id] = e => {
|
this.clickOn[id] = event => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
|
console.log(event.target.key);
|
||||||
this.props.changeContent(id);
|
this.props.changeContent(id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +86,7 @@ class SideBar extends Component {
|
||||||
<ListItem button key={tag.id} onClick={this.clickOn[tag.id]}>
|
<ListItem button key={tag.id} onClick={this.clickOn[tag.id]}>
|
||||||
<ListItemText primary={tag.title} />
|
<ListItemText primary={tag.title} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</List>
|
</List>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {hydrate} from 'react-dom';
|
import {hydrate} from 'react-dom';
|
||||||
import {createMuiTheme, MuiThemeProvider} from 'material-ui/styles';
|
import {createMuiTheme, MuiThemeProvider} from 'material-ui/styles';
|
||||||
|
|
@ -17,4 +33,4 @@ hydrate(
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</MuiThemeProvider>,
|
</MuiThemeProvider>,
|
||||||
document.getElementById('dashboard')
|
document.getElementById('dashboard')
|
||||||
); // server-side rendering
|
);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ type Config struct {
|
||||||
// for ephemeral nodes).
|
// for ephemeral nodes).
|
||||||
Port int `toml:",omitempty"`
|
Port int `toml:",omitempty"`
|
||||||
|
|
||||||
// Refresh is the refresh rate of the data updates, the data will be collected this often.
|
// Refresh is the refresh rate of the data updates, the chartEntry will be collected this often.
|
||||||
Refresh time.Duration `toml:",omitempty"`
|
Refresh time.Duration `toml:",omitempty"`
|
||||||
|
|
||||||
// Assets offers a possibility to manually set the dashboard website's location on the server side.
|
// Assets offers a possibility to manually set the dashboard website's location on the server side.
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ package dashboard
|
||||||
//go:generate go-bindata -nometadata -o assets.go -prefix assets -pkg dashboard assets/public/...
|
//go:generate go-bindata -nometadata -o assets.go -prefix assets -pkg dashboard assets/public/...
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
|
@ -29,62 +28,65 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
processorSampleLimit = 200
|
memorySampleLimit = 200 // Maximum number of memory data samples
|
||||||
memorySampleLimit = 200
|
trafficSampleLimit = 200 // Maximum number of traffic data samples
|
||||||
trafficSampleLimit = 200
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var nextId uint32 // Next connection id
|
||||||
nextId uint32 // Next connection id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// dashboard contains the dashboard internals.
|
||||||
type dashboard struct {
|
type dashboard struct {
|
||||||
config *Config
|
config *Config
|
||||||
|
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
conns map[uint32]*client // Currently live websocket connections
|
conns map[uint32]*client // Currently live websocket connections
|
||||||
Metrics *metricSamples `json:"metrics,omitempty"`
|
charts charts // The collected data samples to plot
|
||||||
Stats *status `json:"stats,omitempty"`
|
|
||||||
lock sync.RWMutex // Lock protecting the dashboard's internals
|
lock sync.RWMutex // Lock protecting the dashboard's internals
|
||||||
|
|
||||||
closing chan chan error // Channel used for graceful exit
|
quit chan chan error // Channel used for graceful exit
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// message embraces the data samples of a client message.
|
||||||
|
type message struct {
|
||||||
|
History *charts `json:"history,omitempty"` // Past data samples
|
||||||
|
Memory *chartEntry `json:"memory,omitempty"` // One memory sample
|
||||||
|
Traffic *chartEntry `json:"traffic,omitempty"` // One traffic sample
|
||||||
|
Log string `json:"log,omitempty"` // One log
|
||||||
|
}
|
||||||
|
|
||||||
|
// client represents active websocket connection with a remote browser.
|
||||||
type client struct {
|
type client struct {
|
||||||
conn *websocket.Conn // Particular live websocket connection
|
conn *websocket.Conn // Particular live websocket connection
|
||||||
msg chan *map[string]interface{} // Message queue for the update messages
|
msg chan message // Message queue for the update messages
|
||||||
logger log.Logger // Logger for the particular live websocket connection
|
logger log.Logger // Logger for the particular live websocket connection
|
||||||
}
|
}
|
||||||
|
|
||||||
type metricSamples struct {
|
// charts contains the collected data samples.
|
||||||
Processor []*data `json:"processor,omitempty"`
|
type charts struct {
|
||||||
Memory []*data `json:"memory,omitempty"`
|
Memory []*chartEntry `json:"memorySamples,omitempty"`
|
||||||
|
Traffic []*chartEntry `json:"trafficSamples,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type data struct {
|
// chartEntry represents one data sample
|
||||||
|
type chartEntry struct {
|
||||||
Time time.Time `json:"time,omitempty"`
|
Time time.Time `json:"time,omitempty"`
|
||||||
Value float64 `json:"value,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.
|
// New creates a new dashboard instance with the given configuration.
|
||||||
func New(config *Config) (*dashboard, error) {
|
func New(config *Config) (*dashboard, error) {
|
||||||
return &dashboard{
|
return &dashboard{
|
||||||
conns: make(map[uint32]*client),
|
conns: make(map[uint32]*client),
|
||||||
config: config,
|
config: config,
|
||||||
Metrics: &metricSamples{},
|
quit: make(chan chan error),
|
||||||
closing: make(chan chan error),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,18 +118,20 @@ func (db *dashboard) Start(server *p2p.Server) error {
|
||||||
|
|
||||||
// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
|
// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
|
||||||
func (db *dashboard) Stop() error {
|
func (db *dashboard) Stop() error {
|
||||||
var err error
|
// Close the connection listener.
|
||||||
// Close the connection listener
|
var errs []error
|
||||||
if err = db.listener.Close(); err != nil {
|
if err := db.listener.Close(); err != nil {
|
||||||
log.Warn("Failed to close listener", "err", err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
// Close the collectors.
|
||||||
errc := make(chan error)
|
errc := make(chan error, 1)
|
||||||
db.closing <- errc // collectData
|
for i := 0; i < 2; i++ {
|
||||||
<-errc
|
db.quit <- errc
|
||||||
db.closing <- errc // collectLogs
|
if err := <-errc; err != nil {
|
||||||
<-errc
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Close the connections.
|
||||||
db.lock.Lock()
|
db.lock.Lock()
|
||||||
for _, c := range db.conns {
|
for _, c := range db.conns {
|
||||||
if err := c.conn.Close(); err != nil {
|
if err := c.conn.Close(); err != nil {
|
||||||
|
|
@ -136,18 +140,16 @@ func (db *dashboard) Stop() error {
|
||||||
}
|
}
|
||||||
db.lock.Unlock()
|
db.lock.Unlock()
|
||||||
|
|
||||||
|
// Wait until every goroutine terminates.
|
||||||
db.wg.Wait()
|
db.wg.Wait()
|
||||||
log.Info("Dashboard stopped")
|
log.Info("Dashboard stopped")
|
||||||
|
|
||||||
return err
|
var err error
|
||||||
|
if len(errs) > 0 {
|
||||||
|
err = fmt.Errorf("%v", errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func join(strings ...string) *bytes.Buffer {
|
return err
|
||||||
var buffer bytes.Buffer
|
|
||||||
for _, s := range strings {
|
|
||||||
buffer.WriteString(s)
|
|
||||||
}
|
|
||||||
return &buffer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// webHandler handles all non-api requests, simply flattening and returning the dashboard website.
|
// webHandler handles all non-api requests, simply flattening and returning the dashboard website.
|
||||||
|
|
@ -158,26 +160,24 @@ func (db *dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
path = "/dashboard.html"
|
path = "/dashboard.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the path of the assets is manually set
|
// If the path of the assets is manually set
|
||||||
if db.config.Assets != "" {
|
if db.config.Assets != "" {
|
||||||
file, err := ioutil.ReadFile(join(db.config.Assets, path).String())
|
blob, err := ioutil.ReadFile(filepath.Join(db.config.Assets, path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to read file", "err", err)
|
log.Warn("Failed to read file", "path", path, "err", err)
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write(file)
|
w.Write(blob)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
blob, err := Asset(filepath.Join("public", path))
|
||||||
webapp, err := Asset(join("public", path).String())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to load the asset", "path", path, "err", err)
|
log.Warn("Failed to load the asset", "path", path, "err", err)
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write(webapp)
|
w.Write(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiHandler handles requests for the dashboard.
|
// apiHandler handles requests for the dashboard.
|
||||||
|
|
@ -185,11 +185,10 @@ func (db *dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
id := atomic.AddUint32(&nextId, 1)
|
id := atomic.AddUint32(&nextId, 1)
|
||||||
client := &client{
|
client := &client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
msg: make(chan *map[string]interface{}, 128),
|
msg: make(chan message, 128),
|
||||||
logger: log.New("id", id),
|
logger: log.New("id", id),
|
||||||
}
|
}
|
||||||
|
done := make(chan struct{}) // Buffered channel as sender may exit early
|
||||||
loss := make(chan bool, 1) // Buffered channel as sender may exit early
|
|
||||||
|
|
||||||
// Start listening for messages to send.
|
// Start listening for messages to send.
|
||||||
db.wg.Add(1)
|
db.wg.Add(1)
|
||||||
|
|
@ -198,22 +197,21 @@ func (db *dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-loss:
|
case <-done:
|
||||||
return
|
return
|
||||||
case msg := <-client.msg:
|
case msg := <-client.msg:
|
||||||
if err := websocket.JSON.Send(client.conn, msg); err != nil {
|
if err := websocket.JSON.Send(client.conn, msg); err != nil {
|
||||||
client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
|
client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
|
||||||
|
client.conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Send the past data.
|
// Send the past data.
|
||||||
client.msg <- &map[string]interface{}{
|
client.msg <- message{
|
||||||
"metrics": db.Metrics,
|
History: &db.charts,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start tracking the connection and drop at connection loss.
|
// Start tracking the connection and drop at connection loss.
|
||||||
db.lock.Lock()
|
db.lock.Lock()
|
||||||
db.conns[id] = client
|
db.conns[id] = client
|
||||||
|
|
@ -223,11 +221,10 @@ func (db *dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
delete(db.conns, id)
|
delete(db.conns, id)
|
||||||
db.lock.Unlock()
|
db.lock.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
fail := []byte{}
|
fail := []byte{}
|
||||||
if _, err := conn.Read(fail); err != nil {
|
if _, err := conn.Read(fail); err != nil {
|
||||||
loss <- true
|
close(done)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Ignore all messages
|
// Ignore all messages
|
||||||
|
|
@ -240,24 +237,37 @@ func (db *dashboard) collectData() {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case errc := <-db.closing:
|
case errc := <-db.quit:
|
||||||
errc <- nil
|
errc <- nil
|
||||||
return
|
return
|
||||||
case <-time.After(db.config.Refresh):
|
case <-time.After(db.config.Refresh):
|
||||||
now := time.Now()
|
inboundTraffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1()
|
||||||
traffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1()
|
|
||||||
memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1()
|
memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1()
|
||||||
traff := &data{
|
now := time.Now()
|
||||||
Time: now,
|
memory := &chartEntry{
|
||||||
Value: traffic,
|
|
||||||
}
|
|
||||||
memory := &data{
|
|
||||||
Time: now,
|
Time: now,
|
||||||
Value: memoryInUse,
|
Value: memoryInUse,
|
||||||
}
|
}
|
||||||
|
traffic := &chartEntry{
|
||||||
|
Time: now,
|
||||||
|
Value: inboundTraffic,
|
||||||
|
}
|
||||||
|
// Remove the first elements in case the samples' amount exceeds the limit.
|
||||||
|
first := 0
|
||||||
|
if len(db.charts.Memory) == memorySampleLimit {
|
||||||
|
first = 1
|
||||||
|
}
|
||||||
|
db.charts.Memory = append(db.charts.Memory[first:], memory)
|
||||||
|
first = 0
|
||||||
|
if len(db.charts.Traffic) == trafficSampleLimit {
|
||||||
|
first = 1
|
||||||
|
}
|
||||||
|
db.charts.Traffic = append(db.charts.Traffic[first:], traffic)
|
||||||
|
|
||||||
// TODO (kurkomisi): do not mix traffic with processor!
|
db.sendToAll(&message{
|
||||||
db.update(traff, memory)
|
Memory: memory,
|
||||||
|
Traffic: traffic,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,45 +279,25 @@ func (db *dashboard) collectLogs() {
|
||||||
// TODO (kurkomisi): log collection comes here.
|
// TODO (kurkomisi): log collection comes here.
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case errc := <-db.closing:
|
case errc := <-db.quit:
|
||||||
errc <- nil
|
errc <- nil
|
||||||
return
|
return
|
||||||
case <-time.After(db.config.Refresh):
|
case <-time.After(db.config.Refresh / 2):
|
||||||
db.sendToAll(&map[string]interface{}{
|
db.sendToAll(&message{
|
||||||
"log": "This is a fake log.",
|
Log: "This is a fake log.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update updates the dashboards through the live websocket connections.
|
// sendToAll sends the given message to the active dashboards.
|
||||||
func (db *dashboard) update(processor *data, memory *data) {
|
func (db *dashboard) sendToAll(msg *message) {
|
||||||
// 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)
|
|
||||||
|
|
||||||
db.sendToAll(&map[string]interface{}{
|
|
||||||
"processor": processor,
|
|
||||||
"memory": memory,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends the given message to the active dashboards.
|
|
||||||
func (db *dashboard) sendToAll(msg *map[string]interface{}) {
|
|
||||||
db.lock.Lock()
|
db.lock.Lock()
|
||||||
for _, c := range db.conns {
|
for _, c := range db.conns {
|
||||||
select {
|
select {
|
||||||
case c.msg <- msg:
|
case c.msg <- *msg:
|
||||||
default:
|
default:
|
||||||
c.logger.Warn("Client message queue is full")
|
c.conn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
db.lock.Unlock()
|
db.lock.Unlock()
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
|
|
||||||
// MetricsEnabledFlag is the CLI flag name to use to enable metrics collections.
|
// MetricsEnabledFlag is the CLI flag name to use to enable metrics collections.
|
||||||
const MetricsEnabledFlag = "metrics"
|
const MetricsEnabledFlag = "metrics"
|
||||||
|
const DashboardEnabledFlag = "dashboard"
|
||||||
|
|
||||||
// Enabled is the flag specifying if metrics are enable or not.
|
// Enabled is the flag specifying if metrics are enable or not.
|
||||||
var Enabled = false
|
var Enabled = false
|
||||||
|
|
@ -39,7 +40,7 @@ var Enabled = false
|
||||||
// and peek into the command line args for the metrics flag.
|
// and peek into the command line args for the metrics flag.
|
||||||
func init() {
|
func init() {
|
||||||
for _, arg := range os.Args {
|
for _, arg := range os.Args {
|
||||||
if strings.TrimLeft(arg, "-") == MetricsEnabledFlag {
|
if flag := strings.TrimLeft(arg, "-"); flag == MetricsEnabledFlag || flag == DashboardEnabledFlag {
|
||||||
log.Info("Enabling metrics collection")
|
log.Info("Enabling metrics collection")
|
||||||
Enabled = true
|
Enabled = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue