dashboard: send current block to the dashboard client (#19762)

This adds all dashboard changes from the last couple months.
We're about to remove the dashboard, but decided that we should
get all the recent work in first in case anyone wants to pick up this
project later on.

* cmd, dashboard, eth, p2p: send peer info to the dashboard
* dashboard: update npm packages, improve UI, rebase
* dashboard, p2p: remove println, change doc
* cmd, dashboard, eth, p2p: cleanup after review
* dashboard: send current block to the dashboard client
This commit is contained in:
Kurkó Mihály 2019-11-13 13:13:13 +02:00 committed by Felix Lange
parent 6f1a600f6c
commit 4ea9b62b5c
20 changed files with 11929 additions and 10886 deletions

View File

@ -156,9 +156,6 @@ 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, gitCommit)
}
// 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.DeveloperFlag.Name) shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DeveloperFlag.Name)
@ -182,6 +179,12 @@ func makeFullNode(ctx *cli.Context) *node.Node {
if cfg.Ethstats.URL != "" { if cfg.Ethstats.URL != "" {
utils.RegisterEthStatsService(stack, cfg.Ethstats.URL) utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
} }
// Add dashboard daemon if requested. This should be the last registered service
// in order to be able to collect information about the other services.
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
}
return stack return stack
} }

View File

@ -1561,9 +1561,18 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
// RegisterDashboardService adds a dashboard to the stack. // RegisterDashboardService adds a dashboard to the stack.
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) { func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) {
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return dashboard.New(cfg, commit, ctx.ResolvePath("logs")), nil var (
ethServ *eth.Ethereum
lesServ *les.LightEthereum
)
_ = ctx.Service(&ethServ)
_ = ctx.Service(&lesServ)
return dashboard.New(cfg, ethServ, lesServ, commit, ctx.ResolvePath("logs")), nil
}) })
if err != nil {
Fatalf("Failed to register the dashboard service: %v", err)
}
} }
// RegisterShhService configures Whisper and adds it to the given node. // RegisterShhService configures Whisper and adds it to the given node.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,53 @@
// @flow
// Copyright 2019 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 type {Chain as ChainType} from '../types/content';
export const inserter = () => (update: ChainType, prev: ChainType) => {
if (!update.currentBlock) {
return;
}
if (!prev.currentBlock) {
prev.currentBlock = {};
}
prev.currentBlock.number = update.currentBlock.number;
prev.currentBlock.timestamp = update.currentBlock.timestamp;
return prev;
};
// styles contains the constant styles of the component.
const styles = {};
// themeStyles returns the styles generated from the theme for the component.
const themeStyles = theme => ({});
export type Props = {
content: Content,
};
type State = {};
// Logs renders the log page.
class Chain extends Component<Props, State> {
render() {
return <></>;
}
}
export default Chain;

View File

@ -25,6 +25,7 @@ import Header from 'Header';
import Body from 'Body'; import Body from 'Body';
import {inserter as logInserter, SAME} from 'Logs'; import {inserter as logInserter, SAME} from 'Logs';
import {inserter as peerInserter} from 'Network'; import {inserter as peerInserter} from 'Network';
import {inserter as chainInserter} from 'Chain';
import {MENU} from '../common'; import {MENU} from '../common';
import type {Content} from '../types/content'; import type {Content} from '../types/content';
@ -83,17 +84,24 @@ const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, pre
// the execution of unnecessary operations (e.g. copy of the log array). // the execution of unnecessary operations (e.g. copy of the log array).
const defaultContent: () => Content = () => ({ const defaultContent: () => Content = () => ({
general: { general: {
version: null,
commit: null, commit: null,
version: null,
genesis: '',
},
home: {},
chain: {
currentBlock: {
number: 0,
timestamp: 0,
},
}, },
home: {},
chain: {},
txpool: {}, txpool: {},
network: { network: {
peers: { peers: {
bundles: {}, bundles: {},
}, },
diff: [], diff: [],
activePeerCount: 0,
}, },
system: { system: {
activeMemory: [], activeMemory: [],
@ -121,9 +129,10 @@ const updaters = {
general: { general: {
version: replacer, version: replacer,
commit: replacer, commit: replacer,
genesis: replacer,
}, },
home: null, home: null,
chain: null, chain: chainInserter(),
txpool: null, txpool: null,
network: peerInserter(200), network: peerInserter(200),
system: { system: {
@ -241,6 +250,7 @@ class Dashboard extends Component<Props, State> {
<div className={this.props.classes.dashboard} style={styles.dashboard}> <div className={this.props.classes.dashboard} style={styles.dashboard}>
<Header <Header
switchSideBar={this.switchSideBar} switchSideBar={this.switchSideBar}
content={this.state.content}
/> />
<Body <Body
opened={this.state.sideBar} opened={this.state.sideBar}

View File

@ -32,6 +32,9 @@ import ChartRow from 'ChartRow';
import CustomTooltip, {bytePlotter, bytePerSecPlotter, percentPlotter, multiplier} from 'CustomTooltip'; import CustomTooltip, {bytePlotter, bytePerSecPlotter, percentPlotter, multiplier} from 'CustomTooltip';
import {chartStrokeWidth, styles as commonStyles} from '../common'; import {chartStrokeWidth, styles as commonStyles} from '../common';
import type {General, System} from '../types/content'; import type {General, System} from '../types/content';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faNetworkWired} from "@fortawesome/free-solid-svg-icons";
import Toolbar from "@material-ui/core/Toolbar";
const FOOTER_SYNC_ID = 'footerSyncId'; const FOOTER_SYNC_ID = 'footerSyncId';
@ -154,6 +157,23 @@ class Footer extends Component<Props, State> {
render() { render() {
const {general, system} = this.props; const {general, system} = this.props;
let network = '';
switch (general.genesis) {
case '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3':
network = 'main';
break;
case '0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d':
network = 'ropsten';
break;
case '0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177':
network = 'rinkeby';
break;
case '0xbf7e331f7f7c1dd2e05159666b3bf8bc7a8a3a9eb1d518969eab529dd9b88c1a':
network = 'görli';
break;
default:
network = `unknown (${general.genesis.substring(0, 8)})`;
}
return ( return (
<Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}> <Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}>
@ -202,6 +222,9 @@ class Footer extends Component<Props, State> {
</a> </a>
</Typography> </Typography>
)} )}
<Typography style={styles.headerText}>
<span style={commonStyles.light}>Network</span> {network}
</Typography>
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@ -23,16 +23,25 @@ import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faBars} from '@fortawesome/free-solid-svg-icons'; import {faBars, faSortAmountUp, faClock, faUsers, faSync} from '@fortawesome/free-solid-svg-icons';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import type {Content} from '../types/content';
const magnitude = [31536000, 604800, 86400, 3600, 60, 1];
const label = ['y', 'w', 'd', 'h', 'm', 's'];
// styles contains the constant styles of the component. // styles contains the constant styles of the component.
const styles = { const styles = {
header: { header: {
height: '8%', height: '8%',
}, },
headerText: {
marginRight: 15,
},
toolbar: { toolbar: {
height: '100%', height: '100%',
minHeight: 'unset',
}, },
}; };
@ -50,16 +59,52 @@ const themeStyles = (theme: Object) => ({
title: { title: {
paddingLeft: theme.spacing.unit, paddingLeft: theme.spacing.unit,
fontSize: 3 * theme.spacing.unit, fontSize: 3 * theme.spacing.unit,
flex: 1,
}, },
}); });
export type Props = { export type Props = {
classes: Object, // injected by withStyles() classes: Object, // injected by withStyles()
switchSideBar: () => void, switchSideBar: () => void,
content: Content,
networkID: number,
}; };
type State = {
since: string,
}
// Header renders the header of the dashboard. // Header renders the header of the dashboard.
class Header extends Component<Props> { class Header extends Component<Props, State> {
constructor(props) {
super(props);
this.state = {since: ''};
}
componentDidMount() {
this.interval = setInterval(() => this.setState(() => {
// time (seconds) since last block.
let timeDiff = Math.floor((Date.now() - this.props.content.chain.currentBlock.timestamp * 1000) / 1000);
let since = '';
let i = 0;
for (; i < magnitude.length && timeDiff < magnitude[i]; i++);
for (let j = 2; i < magnitude.length && j > 0; j--, i++) {
const t = Math.floor(timeDiff / magnitude[i]);
if (t > 0) {
since += `${t}${label[i]} `;
timeDiff %= magnitude[i];
}
}
if (since === '') {
since = 'now';
}
this.setState({since: since});
}), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() { render() {
const {classes} = this.props; const {classes} = this.props;
@ -72,6 +117,15 @@ class Header extends Component<Props> {
<Typography type='title' color='inherit' noWrap className={classes.title}> <Typography type='title' color='inherit' noWrap className={classes.title}>
Go Ethereum Dashboard Go Ethereum Dashboard
</Typography> </Typography>
<Typography style={styles.headerText}>
<FontAwesomeIcon icon={faSortAmountUp} /> {this.props.content.chain.currentBlock.number}
</Typography>
<Typography style={styles.headerText}>
<FontAwesomeIcon icon={faClock} /> {this.state.since}
</Typography>
<Typography style={styles.headerText}>
<FontAwesomeIcon icon={faUsers} /> {this.props.content.network.activePeerCount}
</Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
); );

View File

@ -20,6 +20,7 @@ import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles'; import withStyles from '@material-ui/core/styles/withStyles';
import Chain from 'Chain';
import Network from 'Network'; import Network from 'Network';
import Logs from 'Logs'; import Logs from 'Logs';
import Footer from 'Footer'; import Footer from 'Footer';
@ -95,7 +96,9 @@ class Main extends Component<Props, State> {
children = <div>Work in progress.</div>; children = <div>Work in progress.</div>;
break; break;
case MENU.get('chain').id: case MENU.get('chain').id:
children = <div>Work in progress.</div>; children = <Chain
content={this.props.content.chain}
/>;
break; break;
case MENU.get('txpool').id: case MENU.get('txpool').id:
children = <div>Work in progress.</div>; children = <div>Work in progress.</div>;

View File

@ -18,6 +18,7 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Table from '@material-ui/core/Table'; import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead'; import TableHead from '@material-ui/core/TableHead';
import TableBody from '@material-ui/core/TableBody'; import TableBody from '@material-ui/core/TableBody';
@ -27,17 +28,23 @@ import Grid from '@material-ui/core/Grid/Grid';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import {AreaChart, Area, Tooltip, YAxis} from 'recharts'; import {AreaChart, Area, Tooltip, YAxis} from 'recharts';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCircle as fasCircle} from '@fortawesome/free-solid-svg-icons'; import {faCircle as fasCircle} from '@fortawesome/free-solid-svg-icons'; // More icons at fontawesome.com/icons
import {faCircle as farCircle} from '@fortawesome/free-regular-svg-icons'; import {faCircle as farCircle, faClipboard as farClipboard} from '@fortawesome/free-regular-svg-icons';
import convert from 'color-convert'; import convert from 'color-convert';
import {Scrollbars} from 'react-custom-scrollbars';
import CustomTooltip, {bytePlotter, multiplier} from 'CustomTooltip'; import CustomTooltip, {bytePlotter, multiplier} from 'CustomTooltip';
import type {Network as NetworkType, PeerEvent} from '../types/content'; import type {Network as NetworkType, PeerEvent} from '../types/content';
import {styles as commonStyles, chartStrokeWidth, hues, hueScale} from '../common'; import {chartStrokeWidth, hues, hueScale} from '../common';
// Peer chart dimensions. // Peer chart dimensions.
const trafficChartHeight = 18; const trafficChartHeight = 15;
const trafficChartWidth = 400; const trafficChartWidth = 200;
// attemptSeparator separates the peer connection attempts
// such as the peers from the addresses with more attempts
// go to the beginning of the table, and the rest go to the end.
const attemptSeparator = 9;
// setMaxIngress adjusts the peer chart's gradient values based on the given value. // setMaxIngress adjusts the peer chart's gradient values based on the given value.
const setMaxIngress = (peer, value) => { const setMaxIngress = (peer, value) => {
@ -120,6 +127,58 @@ const setEgressChartAttributes = (peer) => {
setMaxEgress(peer, max); setMaxEgress(peer, max);
}; };
// shortName adds some heuristics to the node name in order to make it look meaningful.
const shortName = (name: string) => {
const parts = name.split('/');
if (parts[0].substring(0, 'parity'.length).toLowerCase() === 'parity') {
// Merge Parity and Parity-Ethereum under the same name.
parts[0] = 'Parity';
}
if (parts.length < 2) {
console.error('Incorrect node name', name);
return parts[0];
}
const versionRE = RegExp(/^v?\d+\.\d+\.\d+.*/);
// Drop optional custom identifier.
if (!versionRE.test(parts[1])) {
if (parts.length < 3 || !versionRE.test(parts[2])) {
console.error('Incorrect node name', name);
return parts[0];
}
parts[1] = parts[2];
}
// Cutting anything from the version after the first - or +.
parts[1] = parts[1].split('-')[0].split('+')[0];
return `${parts[0]}/${parts[1]}`;
};
// shortLocation returns a shortened version of the given location object.
const shortLocation = (location: Object) => {
if (!location) {
return '';
}
return `${location.city ? `${location.city}/` : ''}${location.country ? location.country : ''}`;
};
// protocol returns a shortened version of the eth protocol values.
const protocol = (p: Object) => {
if (!p) {
return '';
}
if (typeof p === 'string') {
return p;
}
if (!(p instanceof Object)) {
console.error('Wrong protocol type', p, typeof p);
return '';
}
if (!p.hasOwnProperty('version') || !p.hasOwnProperty('difficulty') || !p.hasOwnProperty('head')) {
console.error('Missing protocol attributes', p);
return '';
}
return `h=${p.head.substring(0, 10)} v=${p.version} td=${p.difficulty}`;
};
// inserter is a state updater function for the main component, which handles the peers. // inserter is a state updater function for the main component, which handles the peers.
export const inserter = (sampleLimit: number) => (update: NetworkType, prev: NetworkType) => { export const inserter = (sampleLimit: number) => (update: NetworkType, prev: NetworkType) => {
// The first message contains the metered peer history. // The first message contains the metered peer history.
@ -134,84 +193,104 @@ export const inserter = (sampleLimit: number) => (update: NetworkType, prev: Net
if (!peer.maxEgress) { if (!peer.maxEgress) {
setEgressChartAttributes(peer); setEgressChartAttributes(peer);
} }
if (!peer.name) {
peer.name = '';
peer.shortName = '';
} else if (!peer.shortName) {
peer.shortName = shortName(peer.name);
}
if (!peer.enode) {
peer.enode = '';
}
if (!peer.protocols) {
peer.protocols = {};
}
peer.eth = protocol(peer.protocols.eth);
peer.les = protocol(peer.protocols.les);
}); });
} }
bundle.shortLocation = shortLocation(bundle.location);
}); });
} }
if (Array.isArray(update.diff)) { if (Array.isArray(update.diff)) {
update.diff.forEach((event: PeerEvent) => { update.diff.forEach((event: PeerEvent) => {
if (!event.ip) { if (!event.addr) {
console.error('Peer event without IP', event); console.error('Peer event without TCP address', event);
return; return;
} }
switch (event.remove) { switch (event.remove) {
case 'bundle': { case 'bundle': {
delete prev.peers.bundles[event.ip]; delete prev.peers.bundles[event.addr];
return; return;
} }
case 'known': { case 'known': {
if (!event.id) { if (!event.enode) {
console.error('Remove known peer event without ID', event.ip); console.error('Remove known peer event without node URL', event.addr);
return; return;
} }
const bundle = prev.peers.bundles[event.ip]; const bundle = prev.peers.bundles[event.addr];
if (!bundle || !bundle.knownPeers || !bundle.knownPeers[event.id]) { if (!bundle || !bundle.knownPeers || !bundle.knownPeers[event.enode]) {
console.error('No known peer to remove', event.ip, event.id); console.error('No known peer to remove', event.addr, event.enode);
return; return;
} }
delete bundle.knownPeers[event.id]; delete bundle.knownPeers[event.enode];
return;
}
case 'attempt': {
const bundle = prev.peers.bundles[event.ip];
if (!bundle || !Array.isArray(bundle.attempts) || bundle.attempts.length < 1) {
console.error('No unknown peer to remove', event.ip);
return;
}
bundle.attempts.splice(0, 1);
return; return;
} }
} }
if (!prev.peers.bundles[event.ip]) { if (!prev.peers.bundles[event.addr]) {
prev.peers.bundles[event.ip] = { prev.peers.bundles[event.addr] = {
location: { location: {
country: '', country: '',
city: '', city: '',
latitude: 0, latitude: 0,
longitude: 0, longitude: 0,
}, },
shortLocation: '',
knownPeers: {}, knownPeers: {},
attempts: [], attempts: 0,
}; };
} }
const bundle = prev.peers.bundles[event.ip]; const bundle = prev.peers.bundles[event.addr];
if (event.location) { if (event.location) {
bundle.location = event.location; bundle.location = event.location;
bundle.shortLocation = shortLocation(bundle.location);
return; return;
} }
if (!event.id) { if (!event.enode) {
if (!bundle.attempts) { bundle.attempts++;
bundle.attempts = [];
}
bundle.attempts.push({
connected: event.connected,
disconnected: event.disconnected,
});
return; return;
} }
if (!bundle.knownPeers) { if (!bundle.knownPeers) {
bundle.knownPeers = {}; bundle.knownPeers = {};
} }
if (!bundle.knownPeers[event.id]) { if (!bundle.knownPeers[event.enode]) {
bundle.knownPeers[event.id] = { bundle.knownPeers[event.enode] = {
connected: [], connected: [],
disconnected: [], disconnected: [],
ingress: [], ingress: [],
egress: [], egress: [],
active: false, active: false,
name: '',
shortName: '',
enode: '',
protocols: {},
eth: '',
les: '',
}; };
} }
const peer = bundle.knownPeers[event.id]; const peer = bundle.knownPeers[event.enode];
if (event.name) {
peer.name = event.name;
peer.shortName = shortName(event.name);
}
if (event.enode) {
peer.enode = event.enode;
}
if (event.protocols) {
peer.protocols = event.protocols;
peer.eth = protocol(peer.protocols.eth);
peer.les = protocol(peer.protocols.les);
}
if (!peer.maxIngress) { if (!peer.maxIngress) {
setIngressChartAttributes(peer); setIngressChartAttributes(peer);
} }
@ -300,11 +379,29 @@ export const inserter = (sampleLimit: number) => (update: NetworkType, prev: Net
} }
}); });
} }
prev.activePeerCount = 0;
Object.entries(prev.peers.bundles).forEach(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return;
}
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
if (peer.active === true) {
prev.activePeerCount++;
}
});
});
return prev; return prev;
}; };
// styles contains the constant styles of the component. // styles contains the constant styles of the component.
const styles = { const styles = {
title: {
marginLeft: 5,
},
table: {
borderCollapse: 'unset',
padding: 5,
},
tableHead: { tableHead: {
height: 'auto', height: 'auto',
}, },
@ -317,13 +414,39 @@ const styles = {
paddingBottom: 0, paddingBottom: 0,
paddingLeft: 5, paddingLeft: 5,
border: 'none', border: 'none',
fontFamily: 'monospace',
fontSize: 10,
},
content: {
height: '800px',
}, },
}; };
// themeStyles returns the styles generated from the theme for the component.
const themeStyles = theme => ({
title: {
color: theme.palette.common.white,
},
table: {
background: theme.palette.grey[900],
},
});
// limitedWidthStyle returns a style object which cuts the long text with three dots.
const limitedWidthStyle = (width) => {
return {
textOverflow: 'ellipsis',
maxWidth: width,
overflow: 'hidden',
whiteSpace: 'nowrap',
};
};
export type Props = { export type Props = {
container: Object, classes: Object, // injected by withStyles()
content: NetworkType, container: Object,
shouldUpdate: Object, content: NetworkType,
shouldUpdate: Object,
}; };
type State = {}; type State = {};
@ -351,179 +474,385 @@ class Network extends Component<Props, State> {
return `${month}/${date}/${hours}:${minutes}:${seconds}`; return `${month}/${date}/${hours}:${minutes}:${seconds}`;
}; };
copyToClipboard = (id) => (event) => { copyToClipboard = (text: string) => (event) => {
event.preventDefault(); event.preventDefault();
navigator.clipboard.writeText(id).then(() => {}, () => { navigator.clipboard.writeText(text).then(() => {}, () => {
console.error("Failed to copy node id", id); console.error("Failed to copy", text);
}); });
}; };
peerTableRow = (ip, id, bundle, peer) => { lesList = () => {
const list = [];
Object.values(this.props.content.peers.bundles).forEach((bundle) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return;
}
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
if (peer.les === '' || peer.eth !== '') {
return;
}
list.push({enode, name: peer.name, location: bundle.location, protocols: peer.protocols});
});
});
return list;
};
ethList = () => {
const list = [];
Object.values(this.props.content.peers.bundles).forEach((bundle) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return;
}
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
if (peer.eth === '' && peer.les !== '') {
return;
}
list.push({enode, name: peer.name, location: bundle.location, protocols: peer.protocols});
});
});
return list;
};
attemptList = () => {
const list = [];
Object.entries(this.props.content.peers.bundles).forEach(([addr, bundle]) => {
if (!bundle.attempts) {
return;
}
list.push({addr, location: bundle.location, attempts: bundle.attempts});
});
return list;
};
knownPeerTableRow = (addr, enode, bundle, peer, showTraffic, proto) => {
const ingressValues = peer.ingress.map(({value}) => ({ingress: value || 0.001})); const ingressValues = peer.ingress.map(({value}) => ({ingress: value || 0.001}));
const egressValues = peer.egress.map(({value}) => ({egress: -value || -0.001})); const egressValues = peer.egress.map(({value}) => ({egress: -value || -0.001}));
return ( return (
<TableRow key={`known_${ip}_${id}`} style={styles.tableRow}> <TableRow key={`known_${addr}_${enode}`} style={styles.tableRow}>
<TableCell style={styles.tableCell}> <TableCell style={styles.tableCell}>
{peer.active {peer.active
? <FontAwesomeIcon icon={fasCircle} color='green' /> ? <FontAwesomeIcon icon={fasCircle} color='green' />
: <FontAwesomeIcon icon={farCircle} style={commonStyles.light} /> : <FontAwesomeIcon icon={farCircle} />
} }
</TableCell> </TableCell>
<TableCell style={{fontFamily: 'monospace', cursor: 'copy', ...styles.tableCell, ...commonStyles.light}} onClick={this.copyToClipboard(id)}> <TableCell
{id.substring(0, 10)} style={{
cursor: 'copy',
...styles.tableCell,
...limitedWidthStyle(80),
}}
onClick={this.copyToClipboard(enode)}
>
{enode.substring(8)}
</TableCell>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
...limitedWidthStyle(80),
}}
onClick={this.copyToClipboard(peer.name)}
>
{peer.shortName}
</TableCell>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
...limitedWidthStyle(100),
}}
onClick={this.copyToClipboard(JSON.stringify(bundle.location))}
>
{bundle.shortLocation}
</TableCell> </TableCell>
<TableCell style={styles.tableCell}> <TableCell style={styles.tableCell}>
{bundle.location ? (() => { {showTraffic ? (
const l = bundle.location; <>
return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''}`; <AreaChart
})() : ''} width={trafficChartWidth}
</TableCell> height={trafficChartHeight}
<TableCell style={styles.tableCell}> data={ingressValues}
<AreaChart margin={{top: 5, right: 5, bottom: 0, left: 5}}
width={trafficChartWidth} syncId={`peerIngress_${addr}_${enode}`}
height={trafficChartHeight} >
data={ingressValues} <defs>
margin={{top: 5, right: 5, bottom: 0, left: 5}} <linearGradient id={`ingressGradient_${addr}_${enode}`} x1='0' y1='1' x2='0' y2='0'>
syncId={`peerIngress_${ip}_${id}`} {peer.ingressGradient
> && peer.ingressGradient.map(({offset, color}, i) => (
<defs> <stop
<linearGradient id={`ingressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'> key={`ingressStop_${addr}_${enode}_${i}`}
{peer.ingressGradient offset={`${offset}%`}
&& peer.ingressGradient.map(({offset, color}, i) => ( stopColor={color}
<stop />
key={`ingressStop_${ip}_${id}_${i}`} ))}
offset={`${offset}%`} </linearGradient>
stopColor={color} </defs>
/> <Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Download')} />} />
))} <YAxis hide scale='sqrt' domain={[0.001, dataMax => Math.max(dataMax, 0)]} />
</linearGradient> <Area
</defs> dataKey='ingress'
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Download')} />} /> isAnimationActive={false}
<YAxis hide scale='sqrt' domain={[0.001, dataMax => Math.max(dataMax, 0)]} /> type='monotone'
<Area fill={`url(#ingressGradient_${addr}_${enode})`}
dataKey='ingress' stroke={peer.ingressGradient[peer.ingressGradient.length - 1].color}
isAnimationActive={false} strokeWidth={chartStrokeWidth}
type='monotone' />
fill={`url(#ingressGradient_${ip}_${id})`} </AreaChart>
stroke={peer.ingressGradient[peer.ingressGradient.length - 1].color} <AreaChart
strokeWidth={chartStrokeWidth} width={trafficChartWidth}
/> height={trafficChartHeight}
</AreaChart> data={egressValues}
<AreaChart margin={{top: 0, right: 5, bottom: 5, left: 5}}
width={trafficChartWidth} syncId={`peerIngress_${addr}_${enode}`}
height={trafficChartHeight} >
data={egressValues} <defs>
margin={{top: 0, right: 5, bottom: 5, left: 5}} <linearGradient id={`egressGradient_${addr}_${enode}`} x1='0' y1='1' x2='0' y2='0'>
syncId={`peerIngress_${ip}_${id}`} {peer.egressGradient
> && peer.egressGradient.map(({offset, color}, i) => (
<defs> <stop
<linearGradient id={`egressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'> key={`egressStop_${addr}_${enode}_${i}`}
{peer.egressGradient offset={`${offset}%`}
&& peer.egressGradient.map(({offset, color}, i) => ( stopColor={color}
<stop />
key={`egressStop_${ip}_${id}_${i}`} ))}
offset={`${offset}%`} </linearGradient>
stopColor={color} </defs>
/> <Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Upload', multiplier(-1))} />} />
))} <YAxis hide scale='sqrt' domain={[dataMin => Math.min(dataMin, 0), -0.001]} />
</linearGradient> <Area
</defs> dataKey='egress'
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Upload', multiplier(-1))} />} /> isAnimationActive={false}
<YAxis hide scale='sqrt' domain={[dataMin => Math.min(dataMin, 0), -0.001]} /> type='monotone'
<Area fill={`url(#egressGradient_${addr}_${enode})`}
dataKey='egress' stroke={peer.egressGradient[0].color}
isAnimationActive={false} strokeWidth={chartStrokeWidth}
type='monotone' />
fill={`url(#egressGradient_${ip}_${id})`} </AreaChart>
stroke={peer.egressGradient[0].color} </>
strokeWidth={chartStrokeWidth} ) : null}
/>
</AreaChart>
</TableCell> </TableCell>
{typeof proto === 'object' ? (
<>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
...limitedWidthStyle(80),
}}
onClick={this.copyToClipboard(JSON.stringify(proto.head))}
>
{proto.head}
</TableCell>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
}}
onClick={this.copyToClipboard(JSON.stringify(proto.difficulty))}
>
{proto.difficulty}
</TableCell>
<TableCell
style={{
cursor: 'copy',
...styles.tableCell,
}}
onClick={this.copyToClipboard(JSON.stringify(proto.version))}
>
{proto.version}
</TableCell>
</>
) : null }
</TableRow> </TableRow>
); );
}; };
connectionAttemptTableRow = (addr, bundle) => (
<TableRow key={`attempt_${addr}`} style={styles.tableRow}>
<TableCell
style={{cursor: 'copy', ...styles.tableCell}}
onClick={this.copyToClipboard(addr)}
>
{addr}
</TableCell>
<TableCell
style={{cursor: 'copy', ...limitedWidthStyle(100), ...styles.tableCell}}
onClick={this.copyToClipboard(JSON.stringify(bundle.location))}
>
{bundle.shortLocation}
</TableCell>
<TableCell style={styles.tableCell}>
{bundle.attempts}
</TableCell>
</TableRow>
);
render() { render() {
const {classes} = this.props;
return ( return (
<Grid container direction='row' justify='space-between'> <Grid container direction='row' spacing={3}>
<Grid item> <Grid item style={{width: '40%'}}>
<Table> <div className={classes.table} style={styles.table}>
<TableHead style={styles.tableHead}> <Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
<TableRow style={styles.tableRow}> Full peers
<TableCell style={styles.tableCell} /> <FontAwesomeIcon
<TableCell style={styles.tableCell}>Node ID</TableCell> icon={farClipboard}
<TableCell style={styles.tableCell}>Location</TableCell> onClick={this.copyToClipboard(JSON.stringify(this.ethList()))}
<TableCell style={styles.tableCell}>Traffic</TableCell> style={{float: 'right'}}
</TableRow> />
</TableHead> </Typography>
<TableBody> <Scrollbars style={styles.content}>
{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => { <Table>
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) { <TableHead style={styles.tableHead}>
return null; <TableRow style={styles.tableRow}>
} <TableCell style={styles.tableCell} />
return Object.entries(bundle.knownPeers).map(([id, peer]) => { <TableCell style={styles.tableCell}>Node URL</TableCell>
if (peer.active === false) { <TableCell style={styles.tableCell}>Name</TableCell>
return null; <TableCell style={styles.tableCell}>Location</TableCell>
} <TableCell style={styles.tableCell}>Traffic</TableCell>
return this.peerTableRow(ip, id, bundle, peer); <TableCell style={styles.tableCell}>Head</TableCell>
}); <TableCell style={styles.tableCell}>TD</TableCell>
})} <TableCell style={styles.tableCell}>V</TableCell>
</TableBody>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([id, peer]) => {
if (peer.active === true) {
return null;
}
return this.peerTableRow(ip, id, bundle, peer);
});
})}
</TableBody>
</Table>
</Grid>
<Grid item>
<Typography variant='subtitle1' gutterBottom>
Connection attempts
</Typography>
<Table>
<TableHead style={styles.tableHead}>
<TableRow style={styles.tableRow}>
<TableCell style={styles.tableCell}>IP</TableCell>
<TableCell style={styles.tableCell}>Location</TableCell>
<TableCell style={styles.tableCell}>Nr</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([ip, bundle]) => {
if (!bundle.attempts || bundle.attempts.length < 1) {
return null;
}
return (
<TableRow key={`attempt_${ip}`} style={styles.tableRow}>
<TableCell style={styles.tableCell}>{ip}</TableCell>
<TableCell style={styles.tableCell}>
{bundle.location ? (() => {
const l = bundle.location;
return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''}`;
})() : ''}
</TableCell>
<TableCell style={styles.tableCell}>
{Object.values(bundle.attempts).length}
</TableCell>
</TableRow> </TableRow>
); </TableHead>
})} <TableBody>
</TableBody> {Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
</Table> if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
if (peer.active === false) {
return null;
}
if (peer.eth === '' && peer.les !== '') {
return null;
}
return this.knownPeerTableRow(addr, enode, bundle, peer, true, peer.protocols.eth);
});
})}
</TableBody>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
if (peer.active === true) {
return null;
}
if (peer.eth === '' && peer.les !== '') {
return null;
}
return this.knownPeerTableRow(addr, enode, bundle, peer, false, peer.protocols.eth);
});
})}
</TableBody>
</Table>
</Scrollbars>
</div>
</Grid>
<Grid item style={{width: '40%'}}>
<div className={classes.table} style={styles.table}>
<Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
Light peers
<FontAwesomeIcon
icon={farClipboard}
onClick={this.copyToClipboard(JSON.stringify(this.lesList()))}
style={{float: 'right'}}
/>
</Typography>
<Scrollbars style={styles.content}>
<Table>
<TableHead style={styles.tableHead}>
<TableRow style={styles.tableRow}>
<TableCell style={styles.tableCell} />
<TableCell style={styles.tableCell}>Node URL</TableCell>
<TableCell style={styles.tableCell}>Name</TableCell>
<TableCell style={styles.tableCell}>Location</TableCell>
<TableCell style={styles.tableCell}>Traffic</TableCell>
<TableCell style={styles.tableCell}>Head</TableCell>
<TableCell style={styles.tableCell}>TD</TableCell>
<TableCell style={styles.tableCell}>V</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
if (peer.active === false) {
return null;
}
if (peer.les === '' || peer.eth !== '') {
return null;
}
return this.knownPeerTableRow(addr, enode, bundle, peer, true, peer.protocols.les);
});
})}
</TableBody>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return null;
}
return Object.entries(bundle.knownPeers).map(([enode, peer]) => {
if (peer.active === true) {
return null;
}
if (peer.les === '' || peer.eth !== '') {
return null;
}
return this.knownPeerTableRow(addr, enode, bundle, peer, false, peer.protocols.les);
});
})}
</TableBody>
</Table>
</Scrollbars>
</div>
</Grid>
<Grid item xs>
<div className={classes.table} style={styles.table}>
<Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
Connection attempts
<FontAwesomeIcon
icon={farClipboard}
onClick={this.copyToClipboard(JSON.stringify(this.attemptList()))}
style={{float: 'right'}}
/>
</Typography>
<Scrollbars style={styles.content}>
<Table>
<TableHead style={styles.tableHead}>
<TableRow style={styles.tableRow}>
<TableCell style={styles.tableCell}>TCP address</TableCell>
<TableCell style={styles.tableCell}>Location</TableCell>
<TableCell style={styles.tableCell}>Nr</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.attempts || bundle.attempts <= attemptSeparator) {
return null;
}
return this.connectionAttemptTableRow(addr, bundle);
})}
</TableBody>
<TableBody>
{Object.entries(this.props.content.peers.bundles).map(([addr, bundle]) => {
if (!bundle.attempts || bundle.attempts < 1 || bundle.attempts > attemptSeparator) {
return null;
}
return this.connectionAttemptTableRow(addr, bundle);
})}
</TableBody>
</Table>
</Scrollbars>
</div>
</Grid> </Grid>
</Grid> </Grid>
); );
} }
} }
export default Network; export default withStyles(themeStyles)(Network);

View File

@ -47,10 +47,11 @@ const themeStyles = theme => ({
background: theme.palette.grey[900], background: theme.palette.grey[900],
}, },
listItem: { listItem: {
minWidth: theme.spacing.unit * 7, minWidth: theme.spacing(7),
color: theme.palette.common.white,
}, },
icon: { icon: {
fontSize: theme.spacing.unit * 3, fontSize: theme.spacing(3),
overflow: 'unset', overflow: 'unset',
}, },
}); });

View File

@ -1,54 +1,56 @@
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"@babel/core": "7.3.4", "@babel/core": "7.4.5",
"@babel/plugin-proposal-class-properties": "7.3.4", "@babel/plugin-proposal-class-properties": "7.4.4",
"@babel/plugin-proposal-function-bind": "7.2.0", "@babel/plugin-proposal-function-bind": "7.2.0",
"@babel/plugin-transform-flow-strip-types": "7.3.4", "@babel/plugin-transform-flow-strip-types": "7.4.4",
"@babel/preset-env": "7.3.4", "@babel/preset-env": "7.4.5",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/preset-stage-0": "^7.0.0", "@babel/preset-stage-0": "^7.0.0",
"@fortawesome/fontawesome-free-regular": "^5.0.13", "@fortawesome/fontawesome-free-regular": "^5.0.13",
"@fortawesome/fontawesome-svg-core": "^1.2.15", "@fortawesome/fontawesome-svg-core": "1.2.18",
"@fortawesome/free-regular-svg-icons": "^5.7.2", "@fortawesome/free-regular-svg-icons": "5.8.2",
"@fortawesome/free-solid-svg-icons": "^5.7.2", "@fortawesome/free-solid-svg-icons": "5.8.2",
"@fortawesome/react-fontawesome": "^0.1.4", "@fortawesome/react-fontawesome": "^0.1.4",
"@material-ui/core": "3.9.2", "@material-ui/core": "4.0.1",
"@material-ui/icons": "3.0.2", "@material-ui/icons": "4.0.1",
"babel-eslint": "10.0.1", "babel-eslint": "10.0.1",
"babel-loader": "8.0.5", "babel-loader": "8.0.6",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"color-convert": "^2.0.0", "color-convert": "^2.0.0",
"css-loader": "2.1.1", "css-loader": "2.1.1",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"eslint": "5.15.1", "eslint": "5.16.0",
"eslint-config-airbnb": "^17.0.0", "eslint-config-airbnb": "^17.0.0",
"eslint-loader": "2.1.2", "eslint-loader": "2.1.2",
"eslint-plugin-flowtype": "3.4.2", "eslint-plugin-flowtype": "3.9.1",
"eslint-plugin-import": "2.16.0", "eslint-plugin-import": "2.17.3",
"eslint-plugin-jsx-a11y": "6.2.1", "eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-node": "8.0.1", "eslint-plugin-node": "9.1.0",
"eslint-plugin-promise": "4.0.1", "eslint-plugin-promise": "4.1.1",
"eslint-plugin-react": "7.12.4", "eslint-plugin-react": "7.13.0",
"file-loader": "3.0.1", "file-loader": "3.0.1",
"flow-bin": "0.94.0", "flow-bin": "0.98.1",
"flow-bin-loader": "^1.0.3", "flow-bin-loader": "^1.0.3",
"flow-typed": "^2.5.1", "flow-typed": "2.5.2",
"js-beautify": "1.9.0", "js-beautify": "1.10.0",
"path": "^0.12.7", "path": "^0.12.7",
"react": "16.8.4", "react": "16.8.6",
"react-dom": "16.8.4", "react-custom-scrollbars": "^4.2.1",
"react-hot-loader": "4.8.0", "react-dom": "16.8.6",
"react-transition-group": "2.6.1", "react-hot-loader": "4.8.8",
"recharts": "1.5.0", "react-scrollbar": "0.5.6",
"react-transition-group": "4.0.1",
"recharts": "1.6.2",
"style-loader": "0.23.1", "style-loader": "0.23.1",
"terser-webpack-plugin": "^1.2.3", "terser-webpack-plugin": "1.3.0",
"url": "^0.11.0", "url": "^0.11.0",
"url-loader": "1.1.2", "url-loader": "1.1.2",
"webpack": "4.29.6", "webpack": "4.32.2",
"webpack-cli": "3.2.3", "webpack-cli": "3.3.2",
"webpack-dashboard": "3.0.0", "webpack-dashboard": "3.0.7",
"webpack-dev-server": "3.2.1", "webpack-dev-server": "3.4.1",
"webpack-merge": "4.2.1" "webpack-merge": "4.2.1"
}, },
"scripts": { "scripts": {

View File

@ -35,6 +35,7 @@ export type ChartEntry = {
export type General = { export type General = {
version: ?string, version: ?string,
commit: ?string, commit: ?string,
genesis: ?string,
}; };
export type Home = { export type Home = {
@ -42,21 +43,29 @@ export type Home = {
}; };
export type Chain = { export type Chain = {
/* TODO (kurkomisi) */ currentBlock: Block,
}; };
export type Block = {
number: number,
timestamp: number,
}
export type TxPool = { export type TxPool = {
/* TODO (kurkomisi) */ /* TODO (kurkomisi) */
}; };
export type Network = { export type Network = {
peers: Peers, peers: Peers,
diff: Array<PeerEvent> diff: Array<PeerEvent>,
activePeerCount: number,
}; };
export type PeerEvent = { export type PeerEvent = {
ip: string, name: string,
id: string, addr: string,
enode: string,
protocols: {[string]: Object},
remove: string, remove: string,
location: GeoLocation, location: GeoLocation,
connected: Date, connected: Date,
@ -71,9 +80,9 @@ export type Peers = {
}; };
export type PeerBundle = { export type PeerBundle = {
location: GeoLocation, location: GeoLocation,
knownPeers: {[string]: KnownPeer}, knownPeers: {[string]: KnownPeer},
attempts: Array<UnknownPeer>, attempts: number,
}; };
export type KnownPeer = { export type KnownPeer = {
@ -81,14 +90,12 @@ export type KnownPeer = {
disconnected: Array<Date>, disconnected: Array<Date>,
ingress: Array<ChartEntries>, ingress: Array<ChartEntries>,
egress: Array<ChartEntries>, egress: Array<ChartEntries>,
name: string,
enode: string,
protocols: {[string]: Object},
active: boolean, active: boolean,
}; };
export type UnknownPeer = {
connected: Date,
disconnected: Date,
};
export type GeoLocation = { export type GeoLocation = {
country: string, country: string,
city: string, city: string,

File diff suppressed because it is too large Load Diff

77
dashboard/chain.go Normal file
View File

@ -0,0 +1,77 @@
package dashboard
import (
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
)
type block struct {
Number int64 `json:"number,omitempty"`
Time uint64 `json:"timestamp,omitempty"`
}
func (db *Dashboard) collectChainData() {
defer db.wg.Done()
var (
currentBlock *block
chainCh chan core.ChainHeadEvent
chainSub event.Subscription
)
switch {
case db.ethServ != nil:
chain := db.ethServ.BlockChain()
currentBlock = &block{
Number: chain.CurrentHeader().Number.Int64(),
Time: chain.CurrentHeader().Time,
}
chainCh = make(chan core.ChainHeadEvent)
chainSub = chain.SubscribeChainHeadEvent(chainCh)
case db.lesServ != nil:
chain := db.lesServ.BlockChain()
currentBlock = &block{
Number: chain.CurrentHeader().Number.Int64(),
Time: chain.CurrentHeader().Time,
}
chainCh = make(chan core.ChainHeadEvent)
chainSub = chain.SubscribeChainHeadEvent(chainCh)
default:
errc := <-db.quit
errc <- nil
return
}
defer chainSub.Unsubscribe()
db.chainLock.Lock()
db.history.Chain = &ChainMessage{
CurrentBlock: currentBlock,
}
db.chainLock.Unlock()
db.sendToAll(&Message{Chain: &ChainMessage{CurrentBlock: currentBlock}})
for {
select {
case e := <-chainCh:
currentBlock := &block{
Number: e.Block.Number().Int64(),
Time: e.Block.Time(),
}
db.chainLock.Lock()
db.history.Chain = &ChainMessage{
CurrentBlock: currentBlock,
}
db.chainLock.Unlock()
db.sendToAll(&Message{Chain: &ChainMessage{CurrentBlock: currentBlock}})
case err := <-chainSub.Err():
log.Warn("Chain subscription error", "err", err)
errc := <-db.quit
errc <- nil
return
case errc := <-db.quit:
errc <- nil
return
}
}
}

View File

@ -27,14 +27,16 @@ package dashboard
import ( import (
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
"io"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/les"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
@ -44,7 +46,8 @@ import (
) )
const ( const (
sampleLimit = 200 // Maximum number of data samples sampleLimit = 200 // Maximum number of data samples
dataCollectorCount = 4
) )
// Dashboard contains the dashboard internals. // Dashboard contains the dashboard internals.
@ -57,16 +60,23 @@ type Dashboard struct {
history *Message // Stored historical data history *Message // Stored historical data
lock sync.Mutex // Lock protecting the dashboard's internals lock sync.Mutex // Lock protecting the dashboard's internals
sysLock sync.RWMutex // Lock protecting the stored system data chainLock sync.RWMutex // Lock protecting the stored blockchain data
peerLock sync.RWMutex // Lock protecting the stored peer data sysLock sync.RWMutex // Lock protecting the stored system data
logLock sync.RWMutex // Lock protecting the stored log data peerLock sync.RWMutex // Lock protecting the stored peer data
logLock sync.RWMutex // Lock protecting the stored log data
geodb *geoDB // geoip database instance for IP to geographical information conversions geodb *geoDB // geoip database instance for IP to geographical information conversions
logdir string // Directory containing the log files logdir string // Directory containing the log files
quit chan chan error // Channel used for graceful exit quit chan chan error // Channel used for graceful exit
wg sync.WaitGroup // Wait group used to close the data collector threads wg sync.WaitGroup // Wait group used to close the data collector threads
peerCh chan p2p.MeteredPeerEvent // Peer event channel.
subPeer event.Subscription // Peer event subscription.
ethServ *eth.Ethereum // Ethereum object serving internals.
lesServ *les.LightEthereum // LightEthereum object serving internals.
} }
// client represents active websocket connection with a remote browser. // client represents active websocket connection with a remote browser.
@ -77,12 +87,23 @@ type client struct {
} }
// New creates a new dashboard instance with the given configuration. // New creates a new dashboard instance with the given configuration.
func New(config *Config, commit string, logdir string) *Dashboard { func New(config *Config, ethServ *eth.Ethereum, lesServ *les.LightEthereum, commit string, logdir string) *Dashboard {
now := time.Now() // There is a data race between the network layer and the dashboard, which
// can cause some lost peer events, therefore some peers might not appear
// on the dashboard.
// In order to solve this problem, the peer event subscription is registered
// here, before the network layer starts.
peerCh := make(chan p2p.MeteredPeerEvent, p2p.MeteredPeerLimit)
versionMeta := "" versionMeta := ""
if len(params.VersionMeta) > 0 { if len(params.VersionMeta) > 0 {
versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta) versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta)
} }
var genesis common.Hash
if ethServ != nil {
genesis = ethServ.BlockChain().Genesis().Hash()
} else if lesServ != nil {
genesis = lesServ.BlockChain().Genesis().Hash()
}
return &Dashboard{ return &Dashboard{
conns: make(map[uint32]*client), conns: make(map[uint32]*client),
config: config, config: config,
@ -91,24 +112,29 @@ func New(config *Config, commit string, logdir string) *Dashboard {
General: &GeneralMessage{ General: &GeneralMessage{
Commit: commit, Commit: commit,
Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta), Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
Genesis: genesis,
}, },
System: &SystemMessage{ System: &SystemMessage{
ActiveMemory: emptyChartEntries(now, sampleLimit), ActiveMemory: emptyChartEntries(sampleLimit),
VirtualMemory: emptyChartEntries(now, sampleLimit), VirtualMemory: emptyChartEntries(sampleLimit),
NetworkIngress: emptyChartEntries(now, sampleLimit), NetworkIngress: emptyChartEntries(sampleLimit),
NetworkEgress: emptyChartEntries(now, sampleLimit), NetworkEgress: emptyChartEntries(sampleLimit),
ProcessCPU: emptyChartEntries(now, sampleLimit), ProcessCPU: emptyChartEntries(sampleLimit),
SystemCPU: emptyChartEntries(now, sampleLimit), SystemCPU: emptyChartEntries(sampleLimit),
DiskRead: emptyChartEntries(now, sampleLimit), DiskRead: emptyChartEntries(sampleLimit),
DiskWrite: emptyChartEntries(now, sampleLimit), DiskWrite: emptyChartEntries(sampleLimit),
}, },
}, },
logdir: logdir, logdir: logdir,
peerCh: peerCh,
subPeer: p2p.SubscribeMeteredPeerEvent(peerCh),
ethServ: ethServ,
lesServ: lesServ,
} }
} }
// emptyChartEntries returns a ChartEntry array containing limit number of empty samples. // emptyChartEntries returns a ChartEntry array containing limit number of empty samples.
func emptyChartEntries(t time.Time, limit int) ChartEntries { func emptyChartEntries(limit int) ChartEntries {
ce := make(ChartEntries, limit) ce := make(ChartEntries, limit)
for i := 0; i < limit; i++ { for i := 0; i < limit; i++ {
ce[i] = new(ChartEntry) ce[i] = new(ChartEntry)
@ -127,7 +153,8 @@ func (db *Dashboard) APIs() []rpc.API { return nil }
func (db *Dashboard) Start(server *p2p.Server) error { func (db *Dashboard) Start(server *p2p.Server) error {
log.Info("Starting dashboard", "url", fmt.Sprintf("http://%s:%d", db.config.Host, db.config.Port)) log.Info("Starting dashboard", "url", fmt.Sprintf("http://%s:%d", db.config.Host, db.config.Port))
db.wg.Add(3) db.wg.Add(dataCollectorCount)
go db.collectChainData()
go db.collectSystemData() go db.collectSystemData()
go db.streamLogs() go db.streamLogs()
go db.collectPeerData() go db.collectPeerData()
@ -141,7 +168,11 @@ func (db *Dashboard) Start(server *p2p.Server) error {
} }
db.listener = listener db.listener = listener
go http.Serve(listener, nil) go func() {
if err := http.Serve(listener, nil); err != http.ErrServerClosed {
log.Warn("Could not accept incoming HTTP connections", "err", err)
}
}()
return nil return nil
} }
@ -155,8 +186,8 @@ func (db *Dashboard) Stop() error {
errs = append(errs, err) errs = append(errs, err)
} }
// Close the collectors. // Close the collectors.
errc := make(chan error, 1) errc := make(chan error, dataCollectorCount)
for i := 0; i < 3; i++ { for i := 0; i < dataCollectorCount; i++ {
db.quit <- errc db.quit <- errc
if err := <-errc; err != nil { if err := <-errc; err != nil {
errs = append(errs, err) errs = append(errs, err)
@ -230,20 +261,21 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
}() }()
// Send the past data. // Send the past data.
db.chainLock.RLock()
db.sysLock.RLock() db.sysLock.RLock()
db.peerLock.RLock() db.peerLock.RLock()
db.logLock.RLock() db.logLock.RLock()
h := deepcopy.Copy(db.history).(*Message) h := deepcopy.Copy(db.history).(*Message)
db.chainLock.RUnlock()
db.sysLock.RUnlock() db.sysLock.RUnlock()
db.peerLock.RUnlock() db.peerLock.RUnlock()
db.logLock.RUnlock() db.logLock.RUnlock()
client.msg <- h
// Start tracking the connection and drop at connection loss. // Start tracking the connection and drop at connection loss.
db.lock.Lock() db.lock.Lock()
client.msg <- h
db.conns[id] = client db.conns[id] = client
db.lock.Unlock() db.lock.Unlock()
defer func() { defer func() {

View File

@ -18,6 +18,8 @@ package dashboard
import ( import (
"encoding/json" "encoding/json"
"github.com/ethereum/go-ethereum/common"
) )
type Message struct { type Message struct {
@ -37,8 +39,9 @@ type ChartEntry struct {
} }
type GeneralMessage struct { type GeneralMessage struct {
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
Commit string `json:"commit,omitempty"` Commit string `json:"commit,omitempty"`
Genesis common.Hash `json:"genesis,omitempty"`
} }
type HomeMessage struct { type HomeMessage struct {
@ -46,7 +49,7 @@ type HomeMessage struct {
} }
type ChainMessage struct { type ChainMessage struct {
/* TODO (kurkomisi) */ CurrentBlock *block `json:"currentBlock,omitempty"`
} }
type TxPoolMessage struct { type TxPoolMessage struct {

View File

@ -18,6 +18,7 @@ package dashboard
import ( import (
"container/list" "container/list"
"reflect"
"strings" "strings"
"time" "time"
@ -28,9 +29,7 @@ import (
) )
const ( const (
eventBufferLimit = 128 // Maximum number of buffered peer events. knownPeerLimit = 100 // Maximum number of stored peers, which successfully made the handshake.
knownPeerLimit = 100 // Maximum number of stored peers, which successfully made the handshake.
attemptLimit = 200 // Maximum number of stored peers, which failed to make the handshake.
// eventLimit is the maximum number of the dashboard's custom peer events, // eventLimit is the maximum number of the dashboard's custom peer events,
// that are collected between two metering period and sent to the clients // that are collected between two metering period and sent to the clients
@ -83,14 +82,6 @@ type peerContainer struct {
// inactivePeers contains the peers with closed connection in chronological order. // inactivePeers contains the peers with closed connection in chronological order.
inactivePeers *list.List inactivePeers *list.List
// attemptOrder is the super array containing the IP addresses, from which
// the peers attempted to connect then failed before/during the handshake.
// Its values are appended in chronological order, which means that the
// oldest attempt is at the beginning of the array. When the first element
// is removed, the first element of the related bundle's attempt array is
// removed too, ensuring that always the latest attempts are stored.
attemptOrder []string
// geodb is the geoip database used to retrieve the peers' geographical location. // geodb is the geoip database used to retrieve the peers' geographical location.
geodb *geoDB geodb *geoDB
} }
@ -100,7 +91,6 @@ func newPeerContainer(geodb *geoDB) *peerContainer {
return &peerContainer{ return &peerContainer{
Bundles: make(map[string]*peerBundle), Bundles: make(map[string]*peerBundle),
inactivePeers: list.New(), inactivePeers: list.New(),
attemptOrder: make([]string, 0, attemptLimit),
geodb: geodb, geodb: geodb,
} }
} }
@ -110,48 +100,62 @@ func newPeerContainer(geodb *geoDB) *peerContainer {
// the IP address from the database and creates a corresponding peer event. // the IP address from the database and creates a corresponding peer event.
// Returns the bundle belonging to the given IP and the events occurring during // Returns the bundle belonging to the given IP and the events occurring during
// the initialization. // the initialization.
func (pc *peerContainer) bundle(ip string) (*peerBundle, []*peerEvent) { func (pc *peerContainer) bundle(addr string) (*peerBundle, []*peerEvent) {
var events []*peerEvent var events []*peerEvent
if _, ok := pc.Bundles[ip]; !ok { if _, ok := pc.Bundles[addr]; !ok {
location := pc.geodb.location(ip) i := strings.IndexByte(addr, ':')
if i < 0 {
i = len(addr)
}
location := pc.geodb.location(addr[:i])
events = append(events, &peerEvent{ events = append(events, &peerEvent{
IP: ip, Addr: addr,
Location: location, Location: location,
}) })
pc.Bundles[ip] = &peerBundle{ pc.Bundles[addr] = &peerBundle{
Location: location, Location: location,
KnownPeers: make(map[string]*knownPeer), KnownPeers: make(map[string]*knownPeer),
} }
} }
return pc.Bundles[ip], events return pc.Bundles[addr], events
} }
// extendKnown handles the events of the successfully connected peers. // extendKnown handles the events of the successfully connected peers.
// Returns the events occurring during the extension. // Returns the events occurring during the extension.
func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent { func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
bundle, events := pc.bundle(event.IP) bundle, events := pc.bundle(event.Addr)
peer, peerEvents := bundle.knownPeer(event.IP, event.ID) peer, peerEvents := bundle.knownPeer(event.Addr, event.Enode)
events = append(events, peerEvents...) events = append(events, peerEvents...)
// Append the connect and the disconnect events to // Append the connect and the disconnect events to
// the corresponding arrays keeping the limit. // the corresponding arrays keeping the limit.
switch { switch {
case event.Connected != nil: case event.Connected != nil: // Handshake succeeded
peer.Connected = append(peer.Connected, event.Connected) peer.Connected = append(peer.Connected, event.Connected)
if first := len(peer.Connected) - sampleLimit; first > 0 { if first := len(peer.Connected) - sampleLimit; first > 0 {
peer.Connected = peer.Connected[first:] peer.Connected = peer.Connected[first:]
} }
if event.peer == nil {
log.Warn("Peer handshake succeeded event without peer instance", "addr", event.Addr, "enode", event.Enode)
}
peer.peer = event.peer
info := event.peer.Info()
peer.Name = info.Name
peer.Protocols = info.Protocols
peer.Active = true peer.Active = true
events = append(events, &peerEvent{ e := &peerEvent{
Activity: Active, Activity: Active,
IP: peer.ip, Name: info.Name,
ID: peer.id, Addr: peer.addr,
}) Enode: peer.enode,
Protocols: peer.Protocols,
}
events = append(events, e)
pc.activeCount++ pc.activeCount++
if peer.listElement != nil { if peer.listElement != nil {
_ = pc.inactivePeers.Remove(peer.listElement) _ = pc.inactivePeers.Remove(peer.listElement)
peer.listElement = nil peer.listElement = nil
} }
case event.Disconnected != nil: case event.Disconnected != nil: // Peer disconnected
peer.Disconnected = append(peer.Disconnected, event.Disconnected) peer.Disconnected = append(peer.Disconnected, event.Disconnected)
if first := len(peer.Disconnected) - sampleLimit; first > 0 { if first := len(peer.Disconnected) - sampleLimit; first > 0 {
peer.Disconnected = peer.Disconnected[first:] peer.Disconnected = peer.Disconnected[first:]
@ -159,8 +163,8 @@ func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
peer.Active = false peer.Active = false
events = append(events, &peerEvent{ events = append(events, &peerEvent{
Activity: Inactive, Activity: Inactive,
IP: peer.ip, Addr: peer.addr,
ID: peer.id, Enode: peer.enode,
}) })
pc.activeCount-- pc.activeCount--
if peer.listElement != nil { if peer.listElement != nil {
@ -169,12 +173,14 @@ func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
} }
// Insert the peer into the list. // Insert the peer into the list.
peer.listElement = pc.inactivePeers.PushBack(peer) peer.listElement = pc.inactivePeers.PushBack(peer)
default:
log.Warn("Unexpected known peer event", "event", *event)
} }
for pc.inactivePeers.Len() > 0 && pc.activeCount+pc.inactivePeers.Len() > knownPeerLimit { for pc.inactivePeers.Len() > 0 && pc.activeCount+pc.inactivePeers.Len() > knownPeerLimit {
// While the count of the known peers is greater than the limit, // While the count of the known peers is greater than the limit,
// remove the first element from the inactive peer list and from the map. // remove the first element from the inactive peer list and from the map.
if removedPeer, ok := pc.inactivePeers.Remove(pc.inactivePeers.Front()).(*knownPeer); ok { if removedPeer, ok := pc.inactivePeers.Remove(pc.inactivePeers.Front()).(*knownPeer); ok {
events = append(events, pc.removeKnown(removedPeer.ip, removedPeer.id)...) events = append(events, pc.removeKnown(removedPeer.addr, removedPeer.enode)...)
} else { } else {
log.Warn("Failed to parse the removed peer") log.Warn("Failed to parse the removed peer")
} }
@ -185,25 +191,6 @@ func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
return events return events
} }
// handleAttempt handles the events of the peers failing before/during the handshake.
// Returns the events occurring during the extension.
func (pc *peerContainer) handleAttempt(event *peerEvent) []*peerEvent {
bundle, events := pc.bundle(event.IP)
bundle.Attempts = append(bundle.Attempts, &peerAttempt{
Connected: *event.Connected,
Disconnected: *event.Disconnected,
})
pc.attemptOrder = append(pc.attemptOrder, event.IP)
for len(pc.attemptOrder) > attemptLimit {
// While the length of the connection attempt order array is greater
// than the limit, remove the first element from the involved peer's
// array and also from the super array.
events = append(events, pc.removeAttempt(pc.attemptOrder[0])...)
pc.attemptOrder = pc.attemptOrder[1:]
}
return events
}
// peerBundle contains the peers belonging to a given IP address. // peerBundle contains the peers belonging to a given IP address.
type peerBundle struct { type peerBundle struct {
// Location contains the geographical location based on the bundle's IP address. // Location contains the geographical location based on the bundle's IP address.
@ -213,57 +200,35 @@ type peerBundle struct {
// maintainer data structure using the node ID as key. // maintainer data structure using the node ID as key.
KnownPeers map[string]*knownPeer `json:"knownPeers,omitempty"` KnownPeers map[string]*knownPeer `json:"knownPeers,omitempty"`
// Attempts contains the failed connection attempts of the // Attempts contains the count of the failed connection
// peers belonging to a given IP address in chronological order. // attempts of the peers belonging to a given IP address.
Attempts []*peerAttempt `json:"attempts,omitempty"` Attempts uint `json:"attempts,omitempty"`
} }
// removeKnown removes the known peer belonging to the // removeKnown removes the known peer belonging to the
// given IP address and node ID from the peer tree. // given IP address and node ID from the peer tree.
func (pc *peerContainer) removeKnown(ip, id string) (events []*peerEvent) { func (pc *peerContainer) removeKnown(addr, enode string) (events []*peerEvent) {
// TODO (kurkomisi): Remove peers that don't have traffic samples anymore. // TODO (kurkomisi): Remove peers that don't have traffic samples anymore.
if bundle, ok := pc.Bundles[ip]; ok { if bundle, ok := pc.Bundles[addr]; ok {
if _, ok := bundle.KnownPeers[id]; ok { if _, ok := bundle.KnownPeers[enode]; ok {
events = append(events, &peerEvent{ events = append(events, &peerEvent{
Remove: RemoveKnown, Remove: RemoveKnown,
IP: ip, Addr: addr,
ID: id, Enode: enode,
}) })
delete(bundle.KnownPeers, id) delete(bundle.KnownPeers, enode)
} else { } else {
log.Warn("No peer to remove", "ip", ip, "id", id) log.Warn("No peer to remove", "addr", addr, "enode", enode)
} }
if len(bundle.KnownPeers) < 1 && len(bundle.Attempts) < 1 { if len(bundle.KnownPeers) < 1 && bundle.Attempts < 1 {
events = append(events, &peerEvent{ events = append(events, &peerEvent{
Remove: RemoveBundle, Remove: RemoveBundle,
IP: ip, Addr: addr,
}) })
delete(pc.Bundles, ip) delete(pc.Bundles, addr)
} }
} else { } else {
log.Warn("No bundle to remove", "ip", ip) log.Warn("No bundle to remove", "addr", addr)
}
return events
}
// removeAttempt removes the peer attempt belonging to the
// given IP address and node ID from the peer tree.
func (pc *peerContainer) removeAttempt(ip string) (events []*peerEvent) {
if bundle, ok := pc.Bundles[ip]; ok {
if len(bundle.Attempts) > 0 {
events = append(events, &peerEvent{
Remove: RemoveAttempt,
IP: ip,
})
bundle.Attempts = bundle.Attempts[1:]
}
if len(bundle.Attempts) < 1 && len(bundle.KnownPeers) < 1 {
events = append(events, &peerEvent{
Remove: RemoveBundle,
IP: ip,
})
delete(pc.Bundles, ip)
}
} }
return events return events
} }
@ -272,26 +237,25 @@ func (pc *peerContainer) removeAttempt(ip string) (events []*peerEvent) {
// to the given IP address and node ID wasn't metered so far. Returns the peer // to the given IP address and node ID wasn't metered so far. Returns the peer
// belonging to the given IP and ID as well as the events occurring during the // belonging to the given IP and ID as well as the events occurring during the
// initialization. // initialization.
func (bundle *peerBundle) knownPeer(ip, id string) (*knownPeer, []*peerEvent) { func (bundle *peerBundle) knownPeer(addr, enode string) (*knownPeer, []*peerEvent) {
var events []*peerEvent var events []*peerEvent
if _, ok := bundle.KnownPeers[id]; !ok { if _, ok := bundle.KnownPeers[enode]; !ok {
now := time.Now() ingress := emptyChartEntries(sampleLimit)
ingress := emptyChartEntries(now, sampleLimit) egress := emptyChartEntries(sampleLimit)
egress := emptyChartEntries(now, sampleLimit)
events = append(events, &peerEvent{ events = append(events, &peerEvent{
IP: ip, Addr: addr,
ID: id, Enode: enode,
Ingress: append([]*ChartEntry{}, ingress...), Ingress: append([]*ChartEntry{}, ingress...),
Egress: append([]*ChartEntry{}, egress...), Egress: append([]*ChartEntry{}, egress...),
}) })
bundle.KnownPeers[id] = &knownPeer{ bundle.KnownPeers[enode] = &knownPeer{
ip: ip, addr: addr,
id: id, enode: enode,
Ingress: ingress, Ingress: ingress,
Egress: egress, Egress: egress,
} }
} }
return bundle.KnownPeers[id], events return bundle.KnownPeers[enode], events
} }
// knownPeer contains the metered data of a particular peer. // knownPeer contains the metered data of a particular peer.
@ -312,31 +276,26 @@ type knownPeer struct {
Ingress ChartEntries `json:"ingress,omitempty"` Ingress ChartEntries `json:"ingress,omitempty"`
Egress ChartEntries `json:"egress,omitempty"` Egress ChartEntries `json:"egress,omitempty"`
Name string `json:"name,omitempty"` // Name of the node, including client type, version, OS, custom data
Enode string `json:"enode,omitempty"` // Node URL
Protocols map[string]interface{} `json:"protocols,omitempty"` // Sub-protocol specific metadata fields
Active bool `json:"active"` // Denotes if the peer is still connected. Active bool `json:"active"` // Denotes if the peer is still connected.
listElement *list.Element // Pointer to the peer element in the list. listElement *list.Element // Pointer to the peer element in the list.
ip, id string // The IP and the ID by which the peer can be accessed in the tree. addr, enode string // The IP and the ID by which the peer can be accessed in the tree.
prevIngress float64 prevIngress float64
prevEgress float64 prevEgress float64
}
// peerAttempt contains a failed peer connection attempt's attributes. peer *p2p.Peer // Connected remote node instance
type peerAttempt struct {
// Connected contains the timestamp of the connection attempt's moment.
Connected time.Time `json:"connected"`
// Disconnected contains the timestamp of the
// moment when the connection attempt failed.
Disconnected time.Time `json:"disconnected"`
} }
type RemovedPeerType string type RemovedPeerType string
type ActivityType string type ActivityType string
const ( const (
RemoveKnown RemovedPeerType = "known" RemoveKnown RemovedPeerType = "known"
RemoveAttempt RemovedPeerType = "attempt" RemoveBundle RemovedPeerType = "bundle"
RemoveBundle RemovedPeerType = "bundle"
Active ActivityType = "active" Active ActivityType = "active"
Inactive ActivityType = "inactive" Inactive ActivityType = "inactive"
@ -344,15 +303,19 @@ const (
// peerEvent contains the attributes of a peer event. // peerEvent contains the attributes of a peer event.
type peerEvent struct { type peerEvent struct {
IP string `json:"ip,omitempty"` // IP address of the peer. Name string `json:"name,omitempty"` // Name of the node, including client type, version, OS, custom data
ID string `json:"id,omitempty"` // Node ID of the peer. Addr string `json:"addr,omitempty"` // TCP address of the peer.
Remove RemovedPeerType `json:"remove,omitempty"` // Type of the peer that is to be removed. Enode string `json:"enode,omitempty"` // Node URL
Location *geoLocation `json:"location,omitempty"` // Geographical location of the peer. Protocols map[string]interface{} `json:"protocols,omitempty"` // Sub-protocol specific metadata fields
Connected *time.Time `json:"connected,omitempty"` // Timestamp of the connection moment. Remove RemovedPeerType `json:"remove,omitempty"` // Type of the peer that is to be removed.
Disconnected *time.Time `json:"disconnected,omitempty"` // Timestamp of the disonnection moment. Location *geoLocation `json:"location,omitempty"` // Geographical location of the peer.
Ingress ChartEntries `json:"ingress,omitempty"` // Ingress samples. Connected *time.Time `json:"connected,omitempty"` // Timestamp of the connection moment.
Egress ChartEntries `json:"egress,omitempty"` // Egress samples. Disconnected *time.Time `json:"disconnected,omitempty"` // Timestamp of the disonnection moment.
Activity ActivityType `json:"activity,omitempty"` // Connection status change. Ingress ChartEntries `json:"ingress,omitempty"` // Ingress samples.
Egress ChartEntries `json:"egress,omitempty"` // Egress samples.
Activity ActivityType `json:"activity,omitempty"` // Connection status change.
peer *p2p.Peer // Connected remote node instance.
} }
// trafficMap is a container for the periodically collected peer traffic. // trafficMap is a container for the periodically collected peer traffic.
@ -376,14 +339,12 @@ func (db *Dashboard) collectPeerData() {
db.geodb, err = openGeoDB() db.geodb, err = openGeoDB()
if err != nil { if err != nil {
log.Warn("Failed to open geodb", "err", err) log.Warn("Failed to open geodb", "err", err)
errc := <-db.quit
errc <- nil
return return
} }
defer db.geodb.close() defer db.geodb.close()
peerCh := make(chan p2p.MeteredPeerEvent, eventBufferLimit) // Peer event channel.
subPeer := p2p.SubscribeMeteredPeerEvent(peerCh) // Subscribe to peer events.
defer subPeer.Unsubscribe() // Unsubscribe at the end.
ticker := time.NewTicker(db.config.Refresh) ticker := time.NewTicker(db.config.Refresh)
defer ticker.Stop() defer ticker.Stop()
@ -400,11 +361,11 @@ func (db *Dashboard) collectPeerData() {
// The function which can be passed to the registry. // The function which can be passed to the registry.
return func(name string, i interface{}) { return func(name string, i interface{}) {
if m, ok := i.(metrics.Meter); ok { if m, ok := i.(metrics.Meter); ok {
// The name of the meter has the format: <common traffic prefix><IP>/<ID> enode := strings.TrimPrefix(name, prefix)
if k := strings.Split(strings.TrimPrefix(name, prefix), "/"); len(k) == 2 { if addr := strings.Split(enode, "@"); len(addr) == 2 {
traffic.insert(k[0], k[1], float64(m.Count())) traffic.insert(addr[1], enode, float64(m.Count()))
} else { } else {
log.Warn("Invalid meter name", "name", name, "prefix", prefix) log.Warn("Invalid enode", "enode", enode)
} }
} else { } else {
log.Warn("Invalid meter type", "name", name) log.Warn("Invalid meter type", "name", name)
@ -428,23 +389,32 @@ func (db *Dashboard) collectPeerData() {
ingress, egress := new(trafficMap), new(trafficMap) ingress, egress := new(trafficMap), new(trafficMap)
*ingress, *egress = make(trafficMap), make(trafficMap) *ingress, *egress = make(trafficMap), make(trafficMap)
defer db.subPeer.Unsubscribe()
for { for {
select { select {
case event := <-peerCh: case event := <-db.peerCh:
now := time.Now() now := time.Now()
switch event.Type { switch event.Type {
case p2p.PeerConnected: case p2p.PeerHandshakeFailed:
connected := now.Add(-event.Elapsed) connected := now.Add(-event.Elapsed)
newPeerEvents = append(newPeerEvents, &peerEvent{ newPeerEvents = append(newPeerEvents, &peerEvent{
IP: event.IP.String(), Addr: event.Addr,
ID: event.ID.String(), Connected: &connected,
Disconnected: &now,
})
case p2p.PeerHandshakeSucceeded:
connected := now.Add(-event.Elapsed)
newPeerEvents = append(newPeerEvents, &peerEvent{
Addr: event.Addr,
Enode: event.Peer.Node().String(),
peer: event.Peer,
Connected: &connected, Connected: &connected,
}) })
case p2p.PeerDisconnected: case p2p.PeerDisconnected:
ip, id := event.IP.String(), event.ID.String() addr, enode := event.Addr, event.Peer.Node().String()
newPeerEvents = append(newPeerEvents, &peerEvent{ newPeerEvents = append(newPeerEvents, &peerEvent{
IP: ip, Addr: addr,
ID: id, Enode: enode,
Disconnected: &now, Disconnected: &now,
}) })
// The disconnect event comes with the last metered traffic count, // The disconnect event comes with the last metered traffic count,
@ -453,15 +423,8 @@ func (db *Dashboard) collectPeerData() {
// period the same peer disconnects multiple times, and appending // period the same peer disconnects multiple times, and appending
// all the samples to the traffic arrays would shift the metering, // all the samples to the traffic arrays would shift the metering,
// so only the last metering is stored, overwriting the previous one. // so only the last metering is stored, overwriting the previous one.
ingress.insert(ip, id, float64(event.Ingress)) ingress.insert(addr, enode, float64(event.Ingress))
egress.insert(ip, id, float64(event.Egress)) egress.insert(addr, enode, float64(event.Egress))
case p2p.PeerHandshakeFailed:
connected := now.Add(-event.Elapsed)
newPeerEvents = append(newPeerEvents, &peerEvent{
IP: event.IP.String(),
Connected: &connected,
Disconnected: &now,
})
default: default:
log.Error("Unknown metered peer event type", "type", event.Type) log.Error("Unknown metered peer event type", "type", event.Type)
} }
@ -475,7 +438,7 @@ func (db *Dashboard) collectPeerData() {
var diff []*peerEvent var diff []*peerEvent
for i := 0; i < len(newPeerEvents); i++ { for i := 0; i < len(newPeerEvents); i++ {
if newPeerEvents[i].IP == "" { if newPeerEvents[i].Addr == "" {
log.Warn("Peer event without IP", "event", *newPeerEvents[i]) log.Warn("Peer event without IP", "event", *newPeerEvents[i])
continue continue
} }
@ -487,18 +450,20 @@ func (db *Dashboard) collectPeerData() {
// //
// The extension can produce additional peer events, such // The extension can produce additional peer events, such
// as remove, location and initial samples events. // as remove, location and initial samples events.
if newPeerEvents[i].ID == "" { if newPeerEvents[i].Enode == "" {
diff = append(diff, peers.handleAttempt(newPeerEvents[i])...) bundle, events := peers.bundle(newPeerEvents[i].Addr)
bundle.Attempts++
diff = append(diff, events...)
continue continue
} }
diff = append(diff, peers.extendKnown(newPeerEvents[i])...) diff = append(diff, peers.extendKnown(newPeerEvents[i])...)
} }
// Update the peer tree using the traffic maps. // Update the peer tree using the traffic maps.
for ip, bundle := range peers.Bundles { for addr, bundle := range peers.Bundles {
for id, peer := range bundle.KnownPeers { for enode, peer := range bundle.KnownPeers {
// Value is 0 if the traffic map doesn't have the // Value is 0 if the traffic map doesn't have the
// entry corresponding to the given IP and ID. // entry corresponding to the given IP and ID.
curIngress, curEgress := (*ingress)[ip][id], (*egress)[ip][id] curIngress, curEgress := (*ingress)[addr][enode], (*egress)[addr][enode]
deltaIngress, deltaEgress := curIngress, curEgress deltaIngress, deltaEgress := curIngress, curEgress
if deltaIngress >= peer.prevIngress { if deltaIngress >= peer.prevIngress {
deltaIngress -= peer.prevIngress deltaIngress -= peer.prevIngress
@ -523,11 +488,22 @@ func (db *Dashboard) collectPeerData() {
} }
// Creating the traffic sample events. // Creating the traffic sample events.
diff = append(diff, &peerEvent{ diff = append(diff, &peerEvent{
IP: ip, Addr: addr,
ID: id, Enode: enode,
Ingress: ChartEntries{i}, Ingress: ChartEntries{i},
Egress: ChartEntries{e}, Egress: ChartEntries{e},
}) })
if peer.peer != nil {
info := peer.peer.Info()
if !reflect.DeepEqual(peer.Protocols, info.Protocols) {
peer.Protocols = info.Protocols
diff = append(diff, &peerEvent{
Addr: addr,
Enode: enode,
Protocols: peer.Protocols,
})
}
}
} }
} }
db.peerLock.Unlock() db.peerLock.Unlock()
@ -541,8 +517,10 @@ func (db *Dashboard) collectPeerData() {
// prepare them for the next metering. // prepare them for the next metering.
*ingress, *egress = make(trafficMap), make(trafficMap) *ingress, *egress = make(trafficMap), make(trafficMap)
newPeerEvents = newPeerEvents[:0] newPeerEvents = newPeerEvents[:0]
case err := <-subPeer.Err(): case err := <-db.subPeer.Err():
log.Warn("Peer subscription error", "err", err) log.Warn("Peer subscription error", "err", err)
errc := <-db.quit
errc <- nil
return return
case errc := <-db.quit: case errc := <-db.quit:
errc <- nil errc <- nil

View File

@ -304,7 +304,7 @@ func (t *dialTask) dial(srv *Server, dest *enode.Node) error {
if err != nil { if err != nil {
return &dialError{err} return &dialError{err}
} }
mfd := newMeteredConn(fd, false, dest.IP()) mfd := newMeteredConn(fd, false, &net.TCPAddr{IP: dest.IP(), Port: dest.TCP()})
return srv.SetupConn(mfd, t.flags, dest) return srv.SetupConn(mfd, t.flags, dest)
} }

View File

@ -19,7 +19,6 @@
package p2p package p2p
import ( import (
"fmt"
"net" "net"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -28,7 +27,6 @@ import (
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p/enode"
) )
const ( const (
@ -58,24 +56,24 @@ var (
type MeteredPeerEventType int type MeteredPeerEventType int
const ( const (
// PeerConnected is the type of event emitted when a peer successfully // PeerHandshakeSucceeded is the type of event
// made the handshake. // emitted when a peer successfully makes the handshake.
PeerConnected MeteredPeerEventType = iota PeerHandshakeSucceeded MeteredPeerEventType = iota
// PeerHandshakeFailed is the type of event emitted when a peer fails to
// make the handshake or disconnects before it.
PeerHandshakeFailed
// PeerDisconnected is the type of event emitted when a peer disconnects. // PeerDisconnected is the type of event emitted when a peer disconnects.
PeerDisconnected PeerDisconnected
// PeerHandshakeFailed is the type of event emitted when a peer fails to
// make the handshake or disconnects before the handshake.
PeerHandshakeFailed
) )
// MeteredPeerEvent is an event emitted when peers connect or disconnect. // MeteredPeerEvent is an event emitted when peers connect or disconnect.
type MeteredPeerEvent struct { type MeteredPeerEvent struct {
Type MeteredPeerEventType // Type of peer event Type MeteredPeerEventType // Type of peer event
IP net.IP // IP address of the peer Addr string // TCP address of the peer
ID enode.ID // NodeID of the peer
Elapsed time.Duration // Time elapsed between the connection and the handshake/disconnection Elapsed time.Duration // Time elapsed between the connection and the handshake/disconnection
Peer *Peer // Connected remote node instance
Ingress uint64 // Ingress count at the moment of the event Ingress uint64 // Ingress count at the moment of the event
Egress uint64 // Egress count at the moment of the event Egress uint64 // Egress count at the moment of the event
} }
@ -91,9 +89,9 @@ func SubscribeMeteredPeerEvent(ch chan<- MeteredPeerEvent) event.Subscription {
type meteredConn struct { type meteredConn struct {
net.Conn // Network connection to wrap with metering net.Conn // Network connection to wrap with metering
connected time.Time // Connection time of the peer connected time.Time // Connection time of the peer
ip net.IP // IP address of the peer addr *net.TCPAddr // TCP address of the peer
id enode.ID // NodeID of the peer peer *Peer // Peer instance
// trafficMetered denotes if the peer is registered in the traffic registries. // trafficMetered denotes if the peer is registered in the traffic registries.
// Its value is true if the metered peer count doesn't reach the limit in the // Its value is true if the metered peer count doesn't reach the limit in the
@ -109,13 +107,13 @@ type meteredConn struct {
// connection meter and also increases the metered peer count. If the metrics // connection meter and also increases the metered peer count. If the metrics
// system is disabled or the IP address is unspecified, this function returns // system is disabled or the IP address is unspecified, this function returns
// the original object. // the original object.
func newMeteredConn(conn net.Conn, ingress bool, ip net.IP) net.Conn { func newMeteredConn(conn net.Conn, ingress bool, addr *net.TCPAddr) net.Conn {
// Short circuit if metrics are disabled // Short circuit if metrics are disabled
if !metrics.Enabled { if !metrics.Enabled {
return conn return conn
} }
if ip.IsUnspecified() { if addr == nil || addr.IP.IsUnspecified() {
log.Warn("Peer IP is unspecified") log.Warn("Peer address is unspecified")
return conn return conn
} }
// Bump the connection counters and wrap the connection // Bump the connection counters and wrap the connection
@ -128,7 +126,7 @@ func newMeteredConn(conn net.Conn, ingress bool, ip net.IP) net.Conn {
return &meteredConn{ return &meteredConn{
Conn: conn, Conn: conn,
ip: ip, addr: addr,
connected: time.Now(), connected: time.Now(),
} }
} }
@ -159,30 +157,27 @@ func (c *meteredConn) Write(b []byte) (n int, err error) {
return n, err return n, err
} }
// handshakeDone is called when a peer handshake is done. Registers the peer to // handshakeDone is called after the connection passes the handshake.
// the ingress and the egress traffic registries using the peer's IP and node ID, func (c *meteredConn) handshakeDone(peer *Peer) {
// also emits connect event.
func (c *meteredConn) handshakeDone(id enode.ID) {
// TODO (kurkomisi): use the node URL instead of the pure node ID. (the String() method of *Node)
if atomic.AddInt32(&meteredPeerCount, 1) >= MeteredPeerLimit { if atomic.AddInt32(&meteredPeerCount, 1) >= MeteredPeerLimit {
// Don't register the peer in the traffic registries. // Don't register the peer in the traffic registries.
atomic.AddInt32(&meteredPeerCount, -1) atomic.AddInt32(&meteredPeerCount, -1)
c.lock.Lock() c.lock.Lock()
c.id, c.trafficMetered = id, false c.peer, c.trafficMetered = peer, false
c.lock.Unlock() c.lock.Unlock()
log.Warn("Metered peer count reached the limit") log.Warn("Metered peer count reached the limit")
} else { } else {
key := fmt.Sprintf("%s/%s", c.ip, id.String()) enode := peer.Node().String()
c.lock.Lock() c.lock.Lock()
c.id, c.trafficMetered = id, true c.peer, c.trafficMetered = peer, true
c.ingressMeter = metrics.NewRegisteredMeter(key, PeerIngressRegistry) c.ingressMeter = metrics.NewRegisteredMeter(enode, PeerIngressRegistry)
c.egressMeter = metrics.NewRegisteredMeter(key, PeerEgressRegistry) c.egressMeter = metrics.NewRegisteredMeter(enode, PeerEgressRegistry)
c.lock.Unlock() c.lock.Unlock()
} }
meteredPeerFeed.Send(MeteredPeerEvent{ meteredPeerFeed.Send(MeteredPeerEvent{
Type: PeerConnected, Type: PeerHandshakeSucceeded,
IP: c.ip, Addr: c.addr.String(),
ID: id, Peer: peer,
Elapsed: time.Since(c.connected), Elapsed: time.Since(c.connected),
}) })
} }
@ -192,44 +187,43 @@ func (c *meteredConn) handshakeDone(id enode.ID) {
func (c *meteredConn) Close() error { func (c *meteredConn) Close() error {
err := c.Conn.Close() err := c.Conn.Close()
c.lock.RLock() c.lock.RLock()
if c.id == (enode.ID{}) { if c.peer == nil {
// If the peer disconnects before the handshake. // If the peer disconnects before/during the handshake.
c.lock.RUnlock() c.lock.RUnlock()
meteredPeerFeed.Send(MeteredPeerEvent{ meteredPeerFeed.Send(MeteredPeerEvent{
Type: PeerHandshakeFailed, Type: PeerHandshakeFailed,
IP: c.ip, Addr: c.addr.String(),
Elapsed: time.Since(c.connected), Elapsed: time.Since(c.connected),
}) })
activePeerGauge.Dec(1) activePeerGauge.Dec(1)
return err return err
} }
id := c.id peer := c.peer
if !c.trafficMetered { if !c.trafficMetered {
// If the peer isn't registered in the traffic registries. // If the peer isn't registered in the traffic registries.
c.lock.RUnlock() c.lock.RUnlock()
meteredPeerFeed.Send(MeteredPeerEvent{ meteredPeerFeed.Send(MeteredPeerEvent{
Type: PeerDisconnected, Type: PeerDisconnected,
IP: c.ip, Addr: c.addr.String(),
ID: id, Peer: peer,
}) })
activePeerGauge.Dec(1) activePeerGauge.Dec(1)
return err return err
} }
ingress, egress := uint64(c.ingressMeter.Count()), uint64(c.egressMeter.Count()) ingress, egress, enode := uint64(c.ingressMeter.Count()), uint64(c.egressMeter.Count()), c.peer.Node().String()
c.lock.RUnlock() c.lock.RUnlock()
// Decrement the metered peer count // Decrement the metered peer count
atomic.AddInt32(&meteredPeerCount, -1) atomic.AddInt32(&meteredPeerCount, -1)
// Unregister the peer from the traffic registries // Unregister the peer from the traffic registries
key := fmt.Sprintf("%s/%s", c.ip, id) PeerIngressRegistry.Unregister(enode)
PeerIngressRegistry.Unregister(key) PeerEgressRegistry.Unregister(enode)
PeerEgressRegistry.Unregister(key)
meteredPeerFeed.Send(MeteredPeerEvent{ meteredPeerFeed.Send(MeteredPeerEvent{
Type: PeerDisconnected, Type: PeerDisconnected,
IP: c.ip, Addr: c.addr.String(),
ID: id, Peer: peer,
Ingress: ingress, Ingress: ingress,
Egress: egress, Egress: egress,
}) })

View File

@ -779,6 +779,9 @@ running:
if p.Inbound() { if p.Inbound() {
inboundCount++ inboundCount++
} }
if conn, ok := c.fd.(*meteredConn); ok {
conn.handshakeDone(p)
}
} }
// The dialer logic relies on the assumption that // The dialer logic relies on the assumption that
// dial tasks complete after the peer has been added or // dial tasks complete after the peer has been added or
@ -902,9 +905,13 @@ func (srv *Server) listenLoop() {
continue continue
} }
if remoteIP != nil { if remoteIP != nil {
fd = newMeteredConn(fd, true, remoteIP) var addr *net.TCPAddr
if tcp, ok := fd.RemoteAddr().(*net.TCPAddr); ok {
addr = tcp
}
fd = newMeteredConn(fd, true, addr)
srv.log.Trace("Accepted connection", "addr", fd.RemoteAddr())
} }
srv.log.Trace("Accepted connection", "addr", fd.RemoteAddr())
go func() { go func() {
srv.SetupConn(fd, inboundConn, nil) srv.SetupConn(fd, inboundConn, nil)
slots <- struct{}{} slots <- struct{}{}
@ -974,9 +981,6 @@ func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *enode.Node) erro
} else { } else {
c.node = nodeFromConn(remotePubkey, c.fd) c.node = nodeFromConn(remotePubkey, c.fd)
} }
if conn, ok := c.fd.(*meteredConn); ok {
conn.handshakeDone(c.node.ID())
}
clog := srv.log.New("id", c.node.ID(), "addr", c.fd.RemoteAddr(), "conn", c.flags) clog := srv.log.New("id", c.node.ID(), "addr", c.fd.RemoteAddr(), "conn", c.flags)
err = srv.checkpoint(c, srv.checkpointPostHandshake) err = srv.checkpoint(c, srv.checkpointPostHandshake)
if err != nil { if err != nil {