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)
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
shhEnabled := enableWhisper(ctx)
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 != "" {
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
}

View File

@ -1561,9 +1561,18 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
// RegisterDashboardService adds a dashboard to the stack.
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) {
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return dashboard.New(cfg, commit, ctx.ResolvePath("logs")), nil
err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
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.

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 {inserter as logInserter, SAME} from 'Logs';
import {inserter as peerInserter} from 'Network';
import {inserter as chainInserter} from 'Chain';
import {MENU} from '../common';
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).
const defaultContent: () => Content = () => ({
general: {
version: null,
commit: null,
version: null,
genesis: '',
},
home: {},
chain: {
currentBlock: {
number: 0,
timestamp: 0,
},
},
home: {},
chain: {},
txpool: {},
network: {
peers: {
bundles: {},
},
diff: [],
diff: [],
activePeerCount: 0,
},
system: {
activeMemory: [],
@ -121,9 +129,10 @@ const updaters = {
general: {
version: replacer,
commit: replacer,
genesis: replacer,
},
home: null,
chain: null,
chain: chainInserter(),
txpool: null,
network: peerInserter(200),
system: {
@ -241,6 +250,7 @@ class Dashboard extends Component<Props, State> {
<div className={this.props.classes.dashboard} style={styles.dashboard}>
<Header
switchSideBar={this.switchSideBar}
content={this.state.content}
/>
<Body
opened={this.state.sideBar}

View File

@ -32,6 +32,9 @@ import ChartRow from 'ChartRow';
import CustomTooltip, {bytePlotter, bytePerSecPlotter, percentPlotter, multiplier} from 'CustomTooltip';
import {chartStrokeWidth, styles as commonStyles} from '../common';
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';
@ -154,6 +157,23 @@ class Footer extends Component<Props, State> {
render() {
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 (
<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>
</Typography>
)}
<Typography style={styles.headerText}>
<span style={commonStyles.light}>Network</span> {network}
</Typography>
</Grid>
</Grid>
);

View File

@ -23,16 +23,25 @@ import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
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 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.
const styles = {
header: {
height: '8%',
},
headerText: {
marginRight: 15,
},
toolbar: {
height: '100%',
height: '100%',
minHeight: 'unset',
},
};
@ -50,16 +59,52 @@ const themeStyles = (theme: Object) => ({
title: {
paddingLeft: theme.spacing.unit,
fontSize: 3 * theme.spacing.unit,
flex: 1,
},
});
export type Props = {
classes: Object, // injected by withStyles()
classes: Object, // injected by withStyles()
switchSideBar: () => void,
content: Content,
networkID: number,
};
type State = {
since: string,
}
// 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() {
const {classes} = this.props;
@ -72,6 +117,15 @@ class Header extends Component<Props> {
<Typography type='title' color='inherit' noWrap className={classes.title}>
Go Ethereum Dashboard
</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>
</AppBar>
);

View File

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

View File

@ -18,6 +18,7 @@
import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
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 {AreaChart, Area, Tooltip, YAxis} from 'recharts';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCircle as fasCircle} from '@fortawesome/free-solid-svg-icons';
import {faCircle as farCircle} from '@fortawesome/free-regular-svg-icons';
import {faCircle as fasCircle} from '@fortawesome/free-solid-svg-icons'; // More icons at fontawesome.com/icons
import {faCircle as farCircle, faClipboard as farClipboard} from '@fortawesome/free-regular-svg-icons';
import convert from 'color-convert';
import {Scrollbars} from 'react-custom-scrollbars';
import CustomTooltip, {bytePlotter, multiplier} from 'CustomTooltip';
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.
const trafficChartHeight = 18;
const trafficChartWidth = 400;
const trafficChartHeight = 15;
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.
const setMaxIngress = (peer, value) => {
@ -120,6 +127,58 @@ const setEgressChartAttributes = (peer) => {
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.
export const inserter = (sampleLimit: number) => (update: NetworkType, prev: NetworkType) => {
// The first message contains the metered peer history.
@ -134,84 +193,104 @@ export const inserter = (sampleLimit: number) => (update: NetworkType, prev: Net
if (!peer.maxEgress) {
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)) {
update.diff.forEach((event: PeerEvent) => {
if (!event.ip) {
console.error('Peer event without IP', event);
if (!event.addr) {
console.error('Peer event without TCP address', event);
return;
}
switch (event.remove) {
case 'bundle': {
delete prev.peers.bundles[event.ip];
delete prev.peers.bundles[event.addr];
return;
}
case 'known': {
if (!event.id) {
console.error('Remove known peer event without ID', event.ip);
if (!event.enode) {
console.error('Remove known peer event without node URL', event.addr);
return;
}
const bundle = prev.peers.bundles[event.ip];
if (!bundle || !bundle.knownPeers || !bundle.knownPeers[event.id]) {
console.error('No known peer to remove', event.ip, event.id);
const bundle = prev.peers.bundles[event.addr];
if (!bundle || !bundle.knownPeers || !bundle.knownPeers[event.enode]) {
console.error('No known peer to remove', event.addr, event.enode);
return;
}
delete bundle.knownPeers[event.id];
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);
delete bundle.knownPeers[event.enode];
return;
}
}
if (!prev.peers.bundles[event.ip]) {
prev.peers.bundles[event.ip] = {
if (!prev.peers.bundles[event.addr]) {
prev.peers.bundles[event.addr] = {
location: {
country: '',
city: '',
latitude: 0,
longitude: 0,
},
shortLocation: '',
knownPeers: {},
attempts: [],
attempts: 0,
};
}
const bundle = prev.peers.bundles[event.ip];
const bundle = prev.peers.bundles[event.addr];
if (event.location) {
bundle.location = event.location;
bundle.shortLocation = shortLocation(bundle.location);
return;
}
if (!event.id) {
if (!bundle.attempts) {
bundle.attempts = [];
}
bundle.attempts.push({
connected: event.connected,
disconnected: event.disconnected,
});
if (!event.enode) {
bundle.attempts++;
return;
}
if (!bundle.knownPeers) {
bundle.knownPeers = {};
}
if (!bundle.knownPeers[event.id]) {
bundle.knownPeers[event.id] = {
if (!bundle.knownPeers[event.enode]) {
bundle.knownPeers[event.enode] = {
connected: [],
disconnected: [],
ingress: [],
egress: [],
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) {
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;
};
// styles contains the constant styles of the component.
const styles = {
title: {
marginLeft: 5,
},
table: {
borderCollapse: 'unset',
padding: 5,
},
tableHead: {
height: 'auto',
},
@ -317,13 +414,39 @@ const styles = {
paddingBottom: 0,
paddingLeft: 5,
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 = {
container: Object,
content: NetworkType,
shouldUpdate: Object,
classes: Object, // injected by withStyles()
container: Object,
content: NetworkType,
shouldUpdate: Object,
};
type State = {};
@ -351,179 +474,385 @@ class Network extends Component<Props, State> {
return `${month}/${date}/${hours}:${minutes}:${seconds}`;
};
copyToClipboard = (id) => (event) => {
copyToClipboard = (text: string) => (event) => {
event.preventDefault();
navigator.clipboard.writeText(id).then(() => {}, () => {
console.error("Failed to copy node id", id);
navigator.clipboard.writeText(text).then(() => {}, () => {
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 egressValues = peer.egress.map(({value}) => ({egress: -value || -0.001}));
return (
<TableRow key={`known_${ip}_${id}`} style={styles.tableRow}>
<TableRow key={`known_${addr}_${enode}`} style={styles.tableRow}>
<TableCell style={styles.tableCell}>
{peer.active
? <FontAwesomeIcon icon={fasCircle} color='green' />
: <FontAwesomeIcon icon={farCircle} style={commonStyles.light} />
: <FontAwesomeIcon icon={farCircle} />
}
</TableCell>
<TableCell style={{fontFamily: 'monospace', cursor: 'copy', ...styles.tableCell, ...commonStyles.light}} onClick={this.copyToClipboard(id)}>
{id.substring(0, 10)}
<TableCell
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 style={styles.tableCell}>
{bundle.location ? (() => {
const l = bundle.location;
return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''}`;
})() : ''}
</TableCell>
<TableCell style={styles.tableCell}>
<AreaChart
width={trafficChartWidth}
height={trafficChartHeight}
data={ingressValues}
margin={{top: 5, right: 5, bottom: 0, left: 5}}
syncId={`peerIngress_${ip}_${id}`}
>
<defs>
<linearGradient id={`ingressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'>
{peer.ingressGradient
&& peer.ingressGradient.map(({offset, color}, i) => (
<stop
key={`ingressStop_${ip}_${id}_${i}`}
offset={`${offset}%`}
stopColor={color}
/>
))}
</linearGradient>
</defs>
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Download')} />} />
<YAxis hide scale='sqrt' domain={[0.001, dataMax => Math.max(dataMax, 0)]} />
<Area
dataKey='ingress'
isAnimationActive={false}
type='monotone'
fill={`url(#ingressGradient_${ip}_${id})`}
stroke={peer.ingressGradient[peer.ingressGradient.length - 1].color}
strokeWidth={chartStrokeWidth}
/>
</AreaChart>
<AreaChart
width={trafficChartWidth}
height={trafficChartHeight}
data={egressValues}
margin={{top: 0, right: 5, bottom: 5, left: 5}}
syncId={`peerIngress_${ip}_${id}`}
>
<defs>
<linearGradient id={`egressGradient_${ip}_${id}`} x1='0' y1='1' x2='0' y2='0'>
{peer.egressGradient
&& peer.egressGradient.map(({offset, color}, i) => (
<stop
key={`egressStop_${ip}_${id}_${i}`}
offset={`${offset}%`}
stopColor={color}
/>
))}
</linearGradient>
</defs>
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Upload', multiplier(-1))} />} />
<YAxis hide scale='sqrt' domain={[dataMin => Math.min(dataMin, 0), -0.001]} />
<Area
dataKey='egress'
isAnimationActive={false}
type='monotone'
fill={`url(#egressGradient_${ip}_${id})`}
stroke={peer.egressGradient[0].color}
strokeWidth={chartStrokeWidth}
/>
</AreaChart>
{showTraffic ? (
<>
<AreaChart
width={trafficChartWidth}
height={trafficChartHeight}
data={ingressValues}
margin={{top: 5, right: 5, bottom: 0, left: 5}}
syncId={`peerIngress_${addr}_${enode}`}
>
<defs>
<linearGradient id={`ingressGradient_${addr}_${enode}`} x1='0' y1='1' x2='0' y2='0'>
{peer.ingressGradient
&& peer.ingressGradient.map(({offset, color}, i) => (
<stop
key={`ingressStop_${addr}_${enode}_${i}`}
offset={`${offset}%`}
stopColor={color}
/>
))}
</linearGradient>
</defs>
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Download')} />} />
<YAxis hide scale='sqrt' domain={[0.001, dataMax => Math.max(dataMax, 0)]} />
<Area
dataKey='ingress'
isAnimationActive={false}
type='monotone'
fill={`url(#ingressGradient_${addr}_${enode})`}
stroke={peer.ingressGradient[peer.ingressGradient.length - 1].color}
strokeWidth={chartStrokeWidth}
/>
</AreaChart>
<AreaChart
width={trafficChartWidth}
height={trafficChartHeight}
data={egressValues}
margin={{top: 0, right: 5, bottom: 5, left: 5}}
syncId={`peerIngress_${addr}_${enode}`}
>
<defs>
<linearGradient id={`egressGradient_${addr}_${enode}`} x1='0' y1='1' x2='0' y2='0'>
{peer.egressGradient
&& peer.egressGradient.map(({offset, color}, i) => (
<stop
key={`egressStop_${addr}_${enode}_${i}`}
offset={`${offset}%`}
stopColor={color}
/>
))}
</linearGradient>
</defs>
<Tooltip cursor={false} content={<CustomTooltip tooltip={bytePlotter('Upload', multiplier(-1))} />} />
<YAxis hide scale='sqrt' domain={[dataMin => Math.min(dataMin, 0), -0.001]} />
<Area
dataKey='egress'
isAnimationActive={false}
type='monotone'
fill={`url(#egressGradient_${addr}_${enode})`}
stroke={peer.egressGradient[0].color}
strokeWidth={chartStrokeWidth}
/>
</AreaChart>
</>
) : null}
</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>
);
};
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() {
const {classes} = this.props;
return (
<Grid container direction='row' justify='space-between'>
<Grid item>
<Table>
<TableHead style={styles.tableHead}>
<TableRow style={styles.tableRow}>
<TableCell style={styles.tableCell} />
<TableCell style={styles.tableCell}>Node ID</TableCell>
<TableCell style={styles.tableCell}>Location</TableCell>
<TableCell style={styles.tableCell}>Traffic</TableCell>
</TableRow>
</TableHead>
<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 === false) {
return null;
}
return this.peerTableRow(ip, id, bundle, peer);
});
})}
</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>
<Grid container direction='row' spacing={3}>
<Grid item style={{width: '40%'}}>
<div className={classes.table} style={styles.table}>
<Typography variant='subtitle1' gutterBottom className={classes.title} style={styles.title}>
Full peers
<FontAwesomeIcon
icon={farClipboard}
onClick={this.copyToClipboard(JSON.stringify(this.ethList()))}
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>
);
})}
</TableBody>
</Table>
</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.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>
);
}
}
export default Network;
export default withStyles(themeStyles)(Network);

View File

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

View File

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

View File

@ -35,6 +35,7 @@ export type ChartEntry = {
export type General = {
version: ?string,
commit: ?string,
genesis: ?string,
};
export type Home = {
@ -42,21 +43,29 @@ export type Home = {
};
export type Chain = {
/* TODO (kurkomisi) */
currentBlock: Block,
};
export type Block = {
number: number,
timestamp: number,
}
export type TxPool = {
/* TODO (kurkomisi) */
};
export type Network = {
peers: Peers,
diff: Array<PeerEvent>
peers: Peers,
diff: Array<PeerEvent>,
activePeerCount: number,
};
export type PeerEvent = {
ip: string,
id: string,
name: string,
addr: string,
enode: string,
protocols: {[string]: Object},
remove: string,
location: GeoLocation,
connected: Date,
@ -71,9 +80,9 @@ export type Peers = {
};
export type PeerBundle = {
location: GeoLocation,
knownPeers: {[string]: KnownPeer},
attempts: Array<UnknownPeer>,
location: GeoLocation,
knownPeers: {[string]: KnownPeer},
attempts: number,
};
export type KnownPeer = {
@ -81,14 +90,12 @@ export type KnownPeer = {
disconnected: Array<Date>,
ingress: Array<ChartEntries>,
egress: Array<ChartEntries>,
name: string,
enode: string,
protocols: {[string]: Object},
active: boolean,
};
export type UnknownPeer = {
connected: Date,
disconnected: Date,
};
export type GeoLocation = {
country: 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 (
"fmt"
"io"
"net"
"net/http"
"sync"
"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/p2p"
"github.com/ethereum/go-ethereum/params"
@ -44,7 +46,8 @@ import (
)
const (
sampleLimit = 200 // Maximum number of data samples
sampleLimit = 200 // Maximum number of data samples
dataCollectorCount = 4
)
// Dashboard contains the dashboard internals.
@ -57,16 +60,23 @@ type Dashboard struct {
history *Message // Stored historical data
lock sync.Mutex // Lock protecting the dashboard's internals
sysLock sync.RWMutex // Lock protecting the stored system data
peerLock sync.RWMutex // Lock protecting the stored peer data
logLock sync.RWMutex // Lock protecting the stored log data
lock sync.Mutex // Lock protecting the dashboard's internals
chainLock sync.RWMutex // Lock protecting the stored blockchain data
sysLock sync.RWMutex // Lock protecting the stored system 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
logdir string // Directory containing the log files
quit chan chan error // Channel used for graceful exit
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.
@ -77,12 +87,23 @@ type client struct {
}
// New creates a new dashboard instance with the given configuration.
func New(config *Config, commit string, logdir string) *Dashboard {
now := time.Now()
func New(config *Config, ethServ *eth.Ethereum, lesServ *les.LightEthereum, commit string, logdir string) *Dashboard {
// 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 := ""
if len(params.VersionMeta) > 0 {
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{
conns: make(map[uint32]*client),
config: config,
@ -91,24 +112,29 @@ func New(config *Config, commit string, logdir string) *Dashboard {
General: &GeneralMessage{
Commit: commit,
Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
Genesis: genesis,
},
System: &SystemMessage{
ActiveMemory: emptyChartEntries(now, sampleLimit),
VirtualMemory: emptyChartEntries(now, sampleLimit),
NetworkIngress: emptyChartEntries(now, sampleLimit),
NetworkEgress: emptyChartEntries(now, sampleLimit),
ProcessCPU: emptyChartEntries(now, sampleLimit),
SystemCPU: emptyChartEntries(now, sampleLimit),
DiskRead: emptyChartEntries(now, sampleLimit),
DiskWrite: emptyChartEntries(now, sampleLimit),
ActiveMemory: emptyChartEntries(sampleLimit),
VirtualMemory: emptyChartEntries(sampleLimit),
NetworkIngress: emptyChartEntries(sampleLimit),
NetworkEgress: emptyChartEntries(sampleLimit),
ProcessCPU: emptyChartEntries(sampleLimit),
SystemCPU: emptyChartEntries(sampleLimit),
DiskRead: emptyChartEntries(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.
func emptyChartEntries(t time.Time, limit int) ChartEntries {
func emptyChartEntries(limit int) ChartEntries {
ce := make(ChartEntries, limit)
for i := 0; i < limit; i++ {
ce[i] = new(ChartEntry)
@ -127,7 +153,8 @@ func (db *Dashboard) APIs() []rpc.API { return nil }
func (db *Dashboard) Start(server *p2p.Server) error {
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.streamLogs()
go db.collectPeerData()
@ -141,7 +168,11 @@ func (db *Dashboard) Start(server *p2p.Server) error {
}
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
}
@ -155,8 +186,8 @@ func (db *Dashboard) Stop() error {
errs = append(errs, err)
}
// Close the collectors.
errc := make(chan error, 1)
for i := 0; i < 3; i++ {
errc := make(chan error, dataCollectorCount)
for i := 0; i < dataCollectorCount; i++ {
db.quit <- errc
if err := <-errc; err != nil {
errs = append(errs, err)
@ -230,20 +261,21 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
}()
// Send the past data.
db.chainLock.RLock()
db.sysLock.RLock()
db.peerLock.RLock()
db.logLock.RLock()
h := deepcopy.Copy(db.history).(*Message)
db.chainLock.RUnlock()
db.sysLock.RUnlock()
db.peerLock.RUnlock()
db.logLock.RUnlock()
client.msg <- h
// Start tracking the connection and drop at connection loss.
db.lock.Lock()
client.msg <- h
db.conns[id] = client
db.lock.Unlock()
defer func() {

View File

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

View File

@ -18,6 +18,7 @@ package dashboard
import (
"container/list"
"reflect"
"strings"
"time"
@ -28,9 +29,7 @@ import (
)
const (
eventBufferLimit = 128 // Maximum number of buffered peer events.
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.
knownPeerLimit = 100 // Maximum number of stored peers, which successfully made the handshake.
// eventLimit is the maximum number of the dashboard's custom peer events,
// 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 *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 *geoDB
}
@ -100,7 +91,6 @@ func newPeerContainer(geodb *geoDB) *peerContainer {
return &peerContainer{
Bundles: make(map[string]*peerBundle),
inactivePeers: list.New(),
attemptOrder: make([]string, 0, attemptLimit),
geodb: geodb,
}
}
@ -110,48 +100,62 @@ func newPeerContainer(geodb *geoDB) *peerContainer {
// 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
// the initialization.
func (pc *peerContainer) bundle(ip string) (*peerBundle, []*peerEvent) {
func (pc *peerContainer) bundle(addr string) (*peerBundle, []*peerEvent) {
var events []*peerEvent
if _, ok := pc.Bundles[ip]; !ok {
location := pc.geodb.location(ip)
if _, ok := pc.Bundles[addr]; !ok {
i := strings.IndexByte(addr, ':')
if i < 0 {
i = len(addr)
}
location := pc.geodb.location(addr[:i])
events = append(events, &peerEvent{
IP: ip,
Addr: addr,
Location: location,
})
pc.Bundles[ip] = &peerBundle{
pc.Bundles[addr] = &peerBundle{
Location: location,
KnownPeers: make(map[string]*knownPeer),
}
}
return pc.Bundles[ip], events
return pc.Bundles[addr], events
}
// extendKnown handles the events of the successfully connected peers.
// Returns the events occurring during the extension.
func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
bundle, events := pc.bundle(event.IP)
peer, peerEvents := bundle.knownPeer(event.IP, event.ID)
bundle, events := pc.bundle(event.Addr)
peer, peerEvents := bundle.knownPeer(event.Addr, event.Enode)
events = append(events, peerEvents...)
// Append the connect and the disconnect events to
// the corresponding arrays keeping the limit.
switch {
case event.Connected != nil:
case event.Connected != nil: // Handshake succeeded
peer.Connected = append(peer.Connected, event.Connected)
if first := len(peer.Connected) - sampleLimit; first > 0 {
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
events = append(events, &peerEvent{
Activity: Active,
IP: peer.ip,
ID: peer.id,
})
e := &peerEvent{
Activity: Active,
Name: info.Name,
Addr: peer.addr,
Enode: peer.enode,
Protocols: peer.Protocols,
}
events = append(events, e)
pc.activeCount++
if peer.listElement != nil {
_ = pc.inactivePeers.Remove(peer.listElement)
peer.listElement = nil
}
case event.Disconnected != nil:
case event.Disconnected != nil: // Peer disconnected
peer.Disconnected = append(peer.Disconnected, event.Disconnected)
if first := len(peer.Disconnected) - sampleLimit; first > 0 {
peer.Disconnected = peer.Disconnected[first:]
@ -159,8 +163,8 @@ func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
peer.Active = false
events = append(events, &peerEvent{
Activity: Inactive,
IP: peer.ip,
ID: peer.id,
Addr: peer.addr,
Enode: peer.enode,
})
pc.activeCount--
if peer.listElement != nil {
@ -169,12 +173,14 @@ func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
}
// Insert the peer into the list.
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 {
// 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.
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 {
log.Warn("Failed to parse the removed peer")
}
@ -185,25 +191,6 @@ func (pc *peerContainer) extendKnown(event *peerEvent) []*peerEvent {
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.
type peerBundle struct {
// 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.
KnownPeers map[string]*knownPeer `json:"knownPeers,omitempty"`
// Attempts contains the failed connection attempts of the
// peers belonging to a given IP address in chronological order.
Attempts []*peerAttempt `json:"attempts,omitempty"`
// Attempts contains the count of the failed connection
// attempts of the peers belonging to a given IP address.
Attempts uint `json:"attempts,omitempty"`
}
// removeKnown removes the known peer belonging to the
// 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.
if bundle, ok := pc.Bundles[ip]; ok {
if _, ok := bundle.KnownPeers[id]; ok {
if bundle, ok := pc.Bundles[addr]; ok {
if _, ok := bundle.KnownPeers[enode]; ok {
events = append(events, &peerEvent{
Remove: RemoveKnown,
IP: ip,
ID: id,
Addr: addr,
Enode: enode,
})
delete(bundle.KnownPeers, id)
delete(bundle.KnownPeers, enode)
} 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{
Remove: RemoveBundle,
IP: ip,
Addr: addr,
})
delete(pc.Bundles, ip)
delete(pc.Bundles, addr)
}
} else {
log.Warn("No bundle to remove", "ip", ip)
}
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)
}
log.Warn("No bundle to remove", "addr", addr)
}
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
// belonging to the given IP and ID as well as the events occurring during the
// initialization.
func (bundle *peerBundle) knownPeer(ip, id string) (*knownPeer, []*peerEvent) {
func (bundle *peerBundle) knownPeer(addr, enode string) (*knownPeer, []*peerEvent) {
var events []*peerEvent
if _, ok := bundle.KnownPeers[id]; !ok {
now := time.Now()
ingress := emptyChartEntries(now, sampleLimit)
egress := emptyChartEntries(now, sampleLimit)
if _, ok := bundle.KnownPeers[enode]; !ok {
ingress := emptyChartEntries(sampleLimit)
egress := emptyChartEntries(sampleLimit)
events = append(events, &peerEvent{
IP: ip,
ID: id,
Addr: addr,
Enode: enode,
Ingress: append([]*ChartEntry{}, ingress...),
Egress: append([]*ChartEntry{}, egress...),
})
bundle.KnownPeers[id] = &knownPeer{
ip: ip,
id: id,
bundle.KnownPeers[enode] = &knownPeer{
addr: addr,
enode: enode,
Ingress: ingress,
Egress: egress,
}
}
return bundle.KnownPeers[id], events
return bundle.KnownPeers[enode], events
}
// knownPeer contains the metered data of a particular peer.
@ -312,31 +276,26 @@ type knownPeer struct {
Ingress ChartEntries `json:"ingress,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.
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
prevEgress float64
}
// peerAttempt contains a failed peer connection attempt's attributes.
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"`
peer *p2p.Peer // Connected remote node instance
}
type RemovedPeerType string
type ActivityType string
const (
RemoveKnown RemovedPeerType = "known"
RemoveAttempt RemovedPeerType = "attempt"
RemoveBundle RemovedPeerType = "bundle"
RemoveKnown RemovedPeerType = "known"
RemoveBundle RemovedPeerType = "bundle"
Active ActivityType = "active"
Inactive ActivityType = "inactive"
@ -344,15 +303,19 @@ const (
// peerEvent contains the attributes of a peer event.
type peerEvent struct {
IP string `json:"ip,omitempty"` // IP address of the peer.
ID string `json:"id,omitempty"` // Node ID of the peer.
Remove RemovedPeerType `json:"remove,omitempty"` // Type of the peer that is to be removed.
Location *geoLocation `json:"location,omitempty"` // Geographical location of the peer.
Connected *time.Time `json:"connected,omitempty"` // Timestamp of the connection moment.
Disconnected *time.Time `json:"disconnected,omitempty"` // Timestamp of the disonnection moment.
Ingress ChartEntries `json:"ingress,omitempty"` // Ingress samples.
Egress ChartEntries `json:"egress,omitempty"` // Egress samples.
Activity ActivityType `json:"activity,omitempty"` // Connection status change.
Name string `json:"name,omitempty"` // Name of the node, including client type, version, OS, custom data
Addr string `json:"addr,omitempty"` // TCP address of the peer.
Enode string `json:"enode,omitempty"` // Node URL
Protocols map[string]interface{} `json:"protocols,omitempty"` // Sub-protocol specific metadata fields
Remove RemovedPeerType `json:"remove,omitempty"` // Type of the peer that is to be removed.
Location *geoLocation `json:"location,omitempty"` // Geographical location of the peer.
Connected *time.Time `json:"connected,omitempty"` // Timestamp of the connection moment.
Disconnected *time.Time `json:"disconnected,omitempty"` // Timestamp of the disonnection moment.
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.
@ -376,14 +339,12 @@ func (db *Dashboard) collectPeerData() {
db.geodb, err = openGeoDB()
if err != nil {
log.Warn("Failed to open geodb", "err", err)
errc := <-db.quit
errc <- nil
return
}
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)
defer ticker.Stop()
@ -400,11 +361,11 @@ func (db *Dashboard) collectPeerData() {
// The function which can be passed to the registry.
return func(name string, i interface{}) {
if m, ok := i.(metrics.Meter); ok {
// The name of the meter has the format: <common traffic prefix><IP>/<ID>
if k := strings.Split(strings.TrimPrefix(name, prefix), "/"); len(k) == 2 {
traffic.insert(k[0], k[1], float64(m.Count()))
enode := strings.TrimPrefix(name, prefix)
if addr := strings.Split(enode, "@"); len(addr) == 2 {
traffic.insert(addr[1], enode, float64(m.Count()))
} else {
log.Warn("Invalid meter name", "name", name, "prefix", prefix)
log.Warn("Invalid enode", "enode", enode)
}
} else {
log.Warn("Invalid meter type", "name", name)
@ -428,23 +389,32 @@ func (db *Dashboard) collectPeerData() {
ingress, egress := new(trafficMap), new(trafficMap)
*ingress, *egress = make(trafficMap), make(trafficMap)
defer db.subPeer.Unsubscribe()
for {
select {
case event := <-peerCh:
case event := <-db.peerCh:
now := time.Now()
switch event.Type {
case p2p.PeerConnected:
case p2p.PeerHandshakeFailed:
connected := now.Add(-event.Elapsed)
newPeerEvents = append(newPeerEvents, &peerEvent{
IP: event.IP.String(),
ID: event.ID.String(),
Addr: event.Addr,
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,
})
case p2p.PeerDisconnected:
ip, id := event.IP.String(), event.ID.String()
addr, enode := event.Addr, event.Peer.Node().String()
newPeerEvents = append(newPeerEvents, &peerEvent{
IP: ip,
ID: id,
Addr: addr,
Enode: enode,
Disconnected: &now,
})
// 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
// all the samples to the traffic arrays would shift the metering,
// so only the last metering is stored, overwriting the previous one.
ingress.insert(ip, id, float64(event.Ingress))
egress.insert(ip, id, float64(event.Egress))
case p2p.PeerHandshakeFailed:
connected := now.Add(-event.Elapsed)
newPeerEvents = append(newPeerEvents, &peerEvent{
IP: event.IP.String(),
Connected: &connected,
Disconnected: &now,
})
ingress.insert(addr, enode, float64(event.Ingress))
egress.insert(addr, enode, float64(event.Egress))
default:
log.Error("Unknown metered peer event type", "type", event.Type)
}
@ -475,7 +438,7 @@ func (db *Dashboard) collectPeerData() {
var diff []*peerEvent
for i := 0; i < len(newPeerEvents); i++ {
if newPeerEvents[i].IP == "" {
if newPeerEvents[i].Addr == "" {
log.Warn("Peer event without IP", "event", *newPeerEvents[i])
continue
}
@ -487,18 +450,20 @@ func (db *Dashboard) collectPeerData() {
//
// The extension can produce additional peer events, such
// as remove, location and initial samples events.
if newPeerEvents[i].ID == "" {
diff = append(diff, peers.handleAttempt(newPeerEvents[i])...)
if newPeerEvents[i].Enode == "" {
bundle, events := peers.bundle(newPeerEvents[i].Addr)
bundle.Attempts++
diff = append(diff, events...)
continue
}
diff = append(diff, peers.extendKnown(newPeerEvents[i])...)
}
// Update the peer tree using the traffic maps.
for ip, bundle := range peers.Bundles {
for id, peer := range bundle.KnownPeers {
for addr, bundle := range peers.Bundles {
for enode, peer := range bundle.KnownPeers {
// Value is 0 if the traffic map doesn't have the
// 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
if deltaIngress >= peer.prevIngress {
deltaIngress -= peer.prevIngress
@ -523,11 +488,22 @@ func (db *Dashboard) collectPeerData() {
}
// Creating the traffic sample events.
diff = append(diff, &peerEvent{
IP: ip,
ID: id,
Addr: addr,
Enode: enode,
Ingress: ChartEntries{i},
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()
@ -541,8 +517,10 @@ func (db *Dashboard) collectPeerData() {
// prepare them for the next metering.
*ingress, *egress = make(trafficMap), make(trafficMap)
newPeerEvents = newPeerEvents[:0]
case err := <-subPeer.Err():
case err := <-db.subPeer.Err():
log.Warn("Peer subscription error", "err", err)
errc := <-db.quit
errc <- nil
return
case errc := <-db.quit:
errc <- nil

View File

@ -304,7 +304,7 @@ func (t *dialTask) dial(srv *Server, dest *enode.Node) error {
if err != nil {
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)
}

View File

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

View File

@ -779,6 +779,9 @@ running:
if p.Inbound() {
inboundCount++
}
if conn, ok := c.fd.(*meteredConn); ok {
conn.handshakeDone(p)
}
}
// The dialer logic relies on the assumption that
// dial tasks complete after the peer has been added or
@ -902,9 +905,13 @@ func (srv *Server) listenLoop() {
continue
}
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() {
srv.SetupConn(fd, inboundConn, nil)
slots <- struct{}{}
@ -974,9 +981,6 @@ func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *enode.Node) erro
} else {
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)
err = srv.checkpoint(c, srv.checkpointPostHandshake)
if err != nil {