Unverified Commit afe0b654 authored by Felix Lange's avatar Felix Lange Committed by GitHub

dashboard: remove the dashboard (#20279)

This removes the dashboard project. The dashboard was an experimental
browser UI for geth which displayed metrics and chain information in
real time. We are removing it because it has marginal utility and nobody
on the team can maintain it.

Removing the dashboard removes a lot of dependency code and shaves
6 MB off the geth binary size.
parent 987648b0
......@@ -28,7 +28,6 @@ import (
cli "gopkg.in/urfave/cli.v1"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/dashboard"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
......@@ -75,11 +74,10 @@ type ethstatsConfig struct {
}
type gethConfig struct {
Eth eth.Config
Shh whisper.Config
Node node.Config
Ethstats ethstatsConfig
Dashboard dashboard.Config
Eth eth.Config
Shh whisper.Config
Node node.Config
Ethstats ethstatsConfig
}
func loadConfig(file string, cfg *gethConfig) error {
......@@ -110,10 +108,9 @@ func defaultNodeConfig() node.Config {
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
// Load defaults.
cfg := gethConfig{
Eth: eth.DefaultConfig,
Shh: whisper.DefaultConfig,
Node: defaultNodeConfig(),
Dashboard: dashboard.DefaultConfig,
Eth: eth.DefaultConfig,
Shh: whisper.DefaultConfig,
Node: defaultNodeConfig(),
}
// Load config file.
......@@ -134,7 +131,6 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name)
}
utils.SetShhConfig(ctx, stack, &cfg.Shh)
utils.SetDashboardConfig(ctx, &cfg.Dashboard)
return stack, cfg
}
......@@ -179,12 +175,6 @@ 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
}
......
......@@ -70,10 +70,6 @@ var (
utils.NoUSBFlag,
utils.SmartCardDaemonPathFlag,
utils.OverrideIstanbulFlag,
utils.DashboardEnabledFlag,
utils.DashboardAddrFlag,
utils.DashboardPortFlag,
utils.DashboardRefreshFlag,
utils.EthashCacheDirFlag,
utils.EthashCachesInMemoryFlag,
utils.EthashCachesOnDiskFlag,
......@@ -236,16 +232,8 @@ func init() {
app.Flags = append(app.Flags, metricsFlags...)
app.Before = func(ctx *cli.Context) error {
logdir := ""
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
logdir = (&node.Config{DataDir: utils.MakeDataDir(ctx)}).ResolvePath("logs")
}
if err := debug.Setup(ctx, logdir); err != nil {
return err
}
return nil
return debug.Setup(ctx, "")
}
app.After = func(ctx *cli.Context) error {
debug.Exit()
console.Stdin.Close() // Resets terminal mode.
......
......@@ -22,8 +22,6 @@ import (
"io"
"sort"
"strings"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/internal/debug"
cli "gopkg.in/urfave/cli.v1"
......@@ -116,16 +114,6 @@ var AppHelpFlagGroups = []flagGroup{
utils.EthashDatasetsOnDiskFlag,
},
},
//{
// Name: "DASHBOARD",
// Flags: []cli.Flag{
// utils.DashboardEnabledFlag,
// utils.DashboardAddrFlag,
// utils.DashboardPortFlag,
// utils.DashboardRefreshFlag,
// utils.DashboardAssetsFlag,
// },
//},
{
Name: "TRANSACTION POOL",
Flags: []cli.Flag{
......@@ -324,9 +312,6 @@ func init() {
var uncategorized []cli.Flag
for _, flag := range data.(*cli.App).Flags {
if _, ok := categorized[flag.String()]; !ok {
if strings.HasPrefix(flag.GetName(), "dashboard") {
continue
}
uncategorized = append(uncategorized, flag)
}
}
......
......@@ -42,7 +42,6 @@ import (
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/dashboard"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/gasprice"
......@@ -272,26 +271,6 @@ var (
Name: "ulc.onlyannounce",
Usage: "Ultra light server sends announcements only",
}
// Dashboard settings
DashboardEnabledFlag = cli.BoolFlag{
Name: "dashboard",
Usage: "Enable the dashboard",
}
DashboardAddrFlag = cli.StringFlag{
Name: "dashboard.addr",
Usage: "Dashboard listening interface",
Value: dashboard.DefaultConfig.Host,
}
DashboardPortFlag = cli.IntFlag{
Name: "dashboard.host",
Usage: "Dashboard listening port",
Value: dashboard.DefaultConfig.Port,
}
DashboardRefreshFlag = cli.DurationFlag{
Name: "dashboard.refresh",
Usage: "Dashboard metrics collection refresh rate",
Value: dashboard.DefaultConfig.Refresh,
}
// Ethash settings
EthashCacheDirFlag = DirectoryFlag{
Name: "ethash.cachedir",
......@@ -1530,13 +1509,6 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
}
}
// SetDashboardConfig applies dashboard related command line flags to the config.
func SetDashboardConfig(ctx *cli.Context, cfg *dashboard.Config) {
cfg.Host = ctx.GlobalString(DashboardAddrFlag.Name)
cfg.Port = ctx.GlobalInt(DashboardPortFlag.Name)
cfg.Refresh = ctx.GlobalDuration(DashboardRefreshFlag.Name)
}
// RegisterEthService adds an Ethereum client to the stack.
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
var err error
......@@ -1559,22 +1531,6 @@ 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) {
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.
func RegisterShhService(stack *node.Node, cfg *whisper.Config) {
if err := stack.Register(func(n *node.ServiceContext) (node.Service, error) {
......
## Go Ethereum Dashboard
The dashboard is a data visualizer integrated into geth, intended to collect and visualize useful information of an Ethereum node. It consists of two parts:
* The client visualizes the collected data.
* The server collects the data, and updates the clients.
The client's UI uses [React][React] with JSX syntax, which is validated by the [ESLint][ESLint] linter mostly according to the [Airbnb React/JSX Style Guide][Airbnb]. The style is defined in the `.eslintrc` configuration file. The resources are bundled into a single `bundle.js` file using [Webpack][Webpack], which relies on the `webpack.config.js`. The bundled file is referenced from `dashboard.html` and takes part in the `assets.go` too. The necessary dependencies for the module bundler are gathered by [Node.js][Node.js].
### Development and bundling
As the dashboard depends on certain NPM packages (which are not included in the `go-ethereum` repo), these need to be installed first:
```
$ (cd dashboard/assets && yarn install && yarn flow)
```
Normally the dashboard assets are bundled into Geth via `go-bindata` to avoid external dependencies. Rebuilding Geth after each UI modification however is not feasible from a developer perspective. Instead, we can run `yarn dev` to watch for file system changes and refresh the browser automatically.
```
$ geth --dashboard --vmodule=dashboard=5
$ (cd dashboard/assets && yarn dev)
```
To bundle up the final UI into Geth, run `go generate`:
```
$ (cd dashboard && go generate)
```
### Static type checking
Since JavaScript doesn't provide type safety, [Flow][Flow] is used to check types. These are only useful during development, so at the end of the process Babel will strip them.
To take advantage of static type checking, your IDE needs to be prepared for it. In case of [Atom][Atom] a configuration guide can be found [here][Atom config]: Install the [Nuclide][Nuclide] package for Flow support, making sure it installs all of its support packages by enabling `Install Recommended Packages on Startup`, and set the path of the `flow-bin` which were installed previously by `yarn`.
For more IDE support install the `linter-eslint` package too, which finds the `.eslintrc` file, and provides real-time linting. Atom warns, that these two packages are incompatible, but they seem to work well together. For third-party library errors and auto-completion [flow-typed][flow-typed] is used.
### Have fun
[Webpack][Webpack] offers handy tools for visualizing the bundle's dependency tree and space usage.
* Generate the bundle's profile running `yarn stats`
* For the _dependency tree_ go to [Webpack Analyze][WA], and import `stats.json`
* For the _space usage_ go to [Webpack Visualizer][WV], and import `stats.json`
[React]: https://reactjs.org/
[ESLint]: https://eslint.org/
[Airbnb]: https://github.com/airbnb/javascript/tree/master/react
[Webpack]: https://webpack.github.io/
[WA]: https://webpack.github.io/analyse/
[WV]: https://chrisbateman.github.io/webpack-visualizer/
[Node.js]: https://nodejs.org/en/
[Flow]: https://flow.org/
[Atom]: https://atom.io/
[Atom config]: https://medium.com/@fastphrase/integrating-flow-into-a-react-project-fbbc2f130eed
[Nuclide]: https://nuclide.io/docs/quick-start/getting-started/
[flow-typed]: https://github.com/flowtype/flow-typed
This diff is collapsed.
node_modules/* #ignored by default
flow-typed/*
bundle.js
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// React syntax style mostly according to https://github.com/airbnb/javascript/tree/master/react
{
"env": {
"browser": true,
"node": true,
"es6": true
},
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 6,
"ecmaFeatures": {
"jsx": true
}
},
"extends": [
"eslint:recommended",
"airbnb",
"plugin:flowtype/recommended",
"plugin:react/recommended"
],
"plugins": [
"flowtype",
"react"
],
"rules": {
"no-tabs": "off",
"indent": ["error", "tab"],
"react/jsx-indent": ["error", "tab"],
"react/jsx-indent-props": ["error", "tab"],
"react/prefer-stateless-function": "off",
"react/destructuring-assignment": ["error", "always", {"ignoreClassFields": true}],
"jsx-quotes": ["error", "prefer-single"],
"no-plusplus": "off",
"no-console": ["error", { "allow": ["error"] }],
// Specifies the maximum length of a line.
"max-len": ["warn", 120, 2, {
"ignoreUrls": true,
"ignoreComments": false,
"ignoreRegExpLiterals": true,
"ignoreStrings": true,
"ignoreTemplateLiterals": true
}],
// Enforces consistent spacing between keys and values in object literal properties.
"key-spacing": ["error", {"align": {
"beforeColon": false,
"afterColon": true,
"on": "value"
}}],
// Prohibits padding inside curly braces.
"object-curly-spacing": ["error", "never"],
"no-use-before-define": "off", // message types
"default-case": "off"
},
"settings": {
"import/resolver": {
"node": {
"paths": ["components"] // import './components/Component' -> import 'Component'
}
},
"flowtype": {
"onlyFilesWithFlowAnnotation": true
}
}
}
[ignore]
<PROJECT_ROOT>/node_modules/material-ui/.*\.js\.flow
[libs]
<PROJECT_ROOT>/flow-typed/
node_modules/jss/flow-typed
[options]
include_warnings=true
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=components
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import {faHome, faLink, faGlobeEurope, faTachometerAlt, faList} from '@fortawesome/free-solid-svg-icons';
import {faCreditCard} from '@fortawesome/free-regular-svg-icons';
type ProvidedMenuProp = {|title: string, icon: string|};
const menuSkeletons: Array<{|id: string, menu: ProvidedMenuProp|}> = [
{
id: 'home',
menu: {
title: 'Home',
icon: faHome,
},
}, {
id: 'chain',
menu: {
title: 'Chain',
icon: faLink,
},
}, {
id: 'txpool',
menu: {
title: 'TxPool',
icon: faCreditCard,
},
}, {
id: 'network',
menu: {
title: 'Network',
icon: faGlobeEurope,
},
}, {
id: 'system',
menu: {
title: 'System',
icon: faTachometerAlt,
},
}, {
id: 'logs',
menu: {
title: 'Logs',
icon: faList,
},
},
];
export type MenuProp = {|...ProvidedMenuProp, id: string|};
// The sidebar menu and the main content are rendered based on these elements.
// Using the id is circumstantial in some cases, so it is better to insert it also as a value.
// This way the mistyping is prevented.
export const MENU: Map<string, {...MenuProp}> = new Map(menuSkeletons.map(({id, menu}) => ([id, {id, ...menu}])));
export const DURATION = 200;
export const chartStrokeWidth = 0.2;
export const styles = {
light: {
color: 'rgba(255, 255, 255, 0.54)',
},
};
// unit contains the units for the bytePlotter.
export const unit = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'];
// simplifyBytes returns the simplified version of the given value followed by the unit.
export const simplifyBytes = (x: number) => {
let i = 0;
for (; x > 1024 && i < 8; i++) {
x /= 1024;
}
return x.toFixed(2).toString().concat(' ', unit[i], 'B');
};
// hues contains predefined colors for gradient stop colors.
export const hues = ['#00FF00', '#FFFF00', '#FF7F00', '#FF0000'];
export const hueScale = [0, 2048, 102400, 2097152];
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import SideBar from './SideBar';
import Main from './Main';
import type {Content} from '../types/content';
// styles contains the constant styles of the component.
const styles = {
body: {
display: 'flex',
width: '100%',
height: '92%',
},
};
export type Props = {
opened: boolean,
changeContent: string => void,
active: string,
content: Content,
shouldUpdate: Object,
send: string => void,
};
// Body renders the body of the dashboard.
class Body extends Component<Props> {
render() {
return (
<div style={styles.body}>
<SideBar
opened={this.props.opened}
changeContent={this.props.changeContent}
/>
<Main
active={this.props.active}
content={this.props.content}
shouldUpdate={this.props.shouldUpdate}
send={this.props.send}
/>
</div>
);
}
}
export default Body;
// @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;
// @flow
// Copyright 2018 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 {ChildrenArray} from 'react';
import Grid from '@material-ui/core/Grid';
// styles contains the constant styles of the component.
const styles = {
container: {
flexWrap: 'nowrap',
height: '100%',
maxWidth: '100%',
margin: 0,
},
item: {
flex: 1,
padding: 0,
},
};
export type Props = {
children: ChildrenArray<React$Element<any>>,
};
// ChartRow renders a row of equally sized responsive charts.
class ChartRow extends Component<Props> {
render() {
return (
<Grid container direction='row' style={styles.container} justify='space-between'>
{React.Children.map(this.props.children, child => (
<Grid item xs style={styles.item}>
{child}
</Grid>
))}
</Grid>
);
}
}
export default ChartRow;
// @flow
// Copyright 2018 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 Typography from '@material-ui/core/Typography';
import {styles, simplifyBytes} from '../common';
// multiplier multiplies a number by another.
export const multiplier = <T>(by: number = 1) => (x: number) => x * by;
// percentPlotter renders a tooltip, which displays the value of the payload followed by a percent sign.
export const percentPlotter = <T>(text: string, mapper: (T => T) = multiplier(1)) => (payload: T) => {
const p = mapper(payload);
if (typeof p !== 'number') {
return null;
}
return (
<Typography type='caption' color='inherit'>
<span style={styles.light}>{text}</span> {p.toFixed(2)} %
</Typography>
);
};
// bytePlotter renders a tooltip, which displays the payload as a byte value.
export const bytePlotter = <T>(text: string, mapper: (T => T) = multiplier(1)) => (payload: T) => {
const p = mapper(payload);
if (typeof p !== 'number') {
return null;
}
return (
<Typography type='caption' color='inherit'>
<span style={styles.light}>{text}</span> {simplifyBytes(p)}
</Typography>
);
};
// bytePlotter renders a tooltip, which displays the payload as a byte value followed by '/s'.
export const bytePerSecPlotter = <T>(text: string, mapper: (T => T) = multiplier(1)) => (payload: T) => {
const p = mapper(payload);
if (typeof p !== 'number') {
return null;
}
return (
<Typography type='caption' color='inherit'>
<span style={styles.light}>{text}</span>
{simplifyBytes(p)}/s
</Typography>
);
};
export type Props = {
active: boolean,
payload: Object,
tooltip: <T>(text: string, mapper?: T => T) => (payload: mixed) => null | React$Element<any>,
};
// CustomTooltip takes a tooltip function, and uses it to plot the active value of the chart.
class CustomTooltip extends Component<Props> {
render() {
const {active, payload, tooltip} = this.props;
if (!active || typeof tooltip !== 'function' || !Array.isArray(payload) || payload.length < 1) {
return null;
}
return tooltip(payload[0].value);
}
}
export default CustomTooltip;
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import {hot} from 'react-hot-loader';
import withStyles from '@material-ui/core/styles/withStyles';
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';
// deepUpdate updates an object corresponding to the given update data, which has
// the shape of the same structure as the original object. updater also has the same
// structure, except that it contains functions where the original data needs to be
// updated. These functions are used to handle the update.
//
// Since the messages have the same shape as the state content, this approach allows
// the generalization of the message handling. The only necessary thing is to set a
// handler function for every path of the state in order to maximize the flexibility
// of the update.
const deepUpdate = (updater: Object, update: Object, prev: Object): $Shape<Content> => {
if (typeof update === 'undefined') {
return prev;
}
if (typeof updater === 'function') {
return updater(update, prev);
}
const updated = {};
Object.keys(prev).forEach((key) => {
updated[key] = deepUpdate(updater[key], update[key], prev[key]);
});
return updated;
};
// shouldUpdate returns the structure of a message. It is used to prevent unnecessary render
// method triggerings. In the affected component's shouldComponentUpdate method it can be checked
// whether the involved data was changed or not by checking the message structure.
//
// We could return the message itself too, but it's safer not to give access to it.
const shouldUpdate = (updater: Object, msg: Object) => {
const su = {};
Object.keys(msg).forEach((key) => {
su[key] = typeof updater[key] !== 'function' ? shouldUpdate(updater[key], msg[key]) : true;
});
return su;
};
// replacer is a state updater function, which replaces the original data.
const replacer = <T>(update: T) => update;
// appender is a state updater function, which appends the update data to the
// existing data. limit defines the maximum allowed size of the created array,
// mapper maps the update data.
const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, prev: Array<T>) => [
...prev,
...update.map(sample => mapper(sample)),
].slice(-limit);
// defaultContent returns the initial value of the state content. Needs to be a function in order to
// instantiate the object again, because it is used by the state, and isn't automatically cleaned
// when a new connection is established. The state is mutated during the update in order to avoid
// the execution of unnecessary operations (e.g. copy of the log array).
const defaultContent: () => Content = () => ({
general: {
commit: null,
version: null,
genesis: '',
},
home: {},
chain: {
currentBlock: {
number: 0,
timestamp: 0,
},
},
txpool: {},
network: {
peers: {
bundles: {},
},
diff: [],
activePeerCount: 0,
},
system: {
activeMemory: [],
virtualMemory: [],
networkIngress: [],
networkEgress: [],
processCPU: [],
systemCPU: [],
diskRead: [],
diskWrite: [],
},
logs: {
chunks: [],
endTop: false,
endBottom: true,
topChanged: SAME,
bottomChanged: SAME,
},
});
// updaters contains the state updater functions for each path of the state.
//
// TODO (kurkomisi): Define a tricky type which embraces the content and the updaters.
const updaters = {
general: {
version: replacer,
commit: replacer,
genesis: replacer,
},
home: null,
chain: chainInserter(),
txpool: null,
network: peerInserter(200),
system: {
activeMemory: appender(200),
virtualMemory: appender(200),
networkIngress: appender(200),
networkEgress: appender(200),
processCPU: appender(200),
systemCPU: appender(200),
diskRead: appender(200),
diskWrite: appender(200),
},
logs: logInserter(5),
};
// styles contains the constant styles of the component.
const styles = {
dashboard: {
display: 'flex',
flexFlow: 'column',
width: '100%',
height: '100%',
zIndex: 1,
overflow: 'hidden',
},
};
// themeStyles returns the styles generated from the theme for the component.
const themeStyles: Object = (theme: Object) => ({
dashboard: {
background: theme.palette.background.default,
},
});
export type Props = {
classes: Object, // injected by withStyles()
};
type State = {
active: string, // active menu
sideBar: boolean, // true if the sidebar is opened
content: Content, // the visualized data
shouldUpdate: Object, // labels for the components, which need to re-render based on the incoming message
server: ?WebSocket,
};
// Dashboard is the main component, which renders the whole page, makes connection with the server and
// listens for messages. When there is an incoming message, updates the page's content correspondingly.
class Dashboard extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
active: MENU.get('home').id,
sideBar: true,
content: defaultContent(),
shouldUpdate: {},
server: null,
};
}
// componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
componentDidMount() {
this.reconnect();
}
// reconnect establishes a websocket connection with the server, listens for incoming messages
// and tries to reconnect on connection loss.
reconnect = () => {
const host = process.env.NODE_ENV === 'production' ? window.location.host : 'localhost:8080';
const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://')}${host}/api`);
server.onopen = () => {
this.setState({content: defaultContent(), shouldUpdate: {}, server});
};
server.onmessage = (event) => {
const msg: $Shape<Content> = JSON.parse(event.data);
if (!msg) {
console.error(`Incoming message is ${msg}`);
return;
}
this.update(msg);
};
server.onclose = () => {
this.setState({server: null});
setTimeout(this.reconnect, 3000);
};
};
// send sends a message to the server, which can be accessed only through this function for safety reasons.
send = (msg: string) => {
if (this.state.server != null) {
this.state.server.send(msg);
}
};
// update updates the content corresponding to the incoming message.
update = (msg: $Shape<Content>) => {
this.setState(prevState => ({
content: deepUpdate(updaters, msg, prevState.content),
shouldUpdate: shouldUpdate(updaters, msg),
}));
};
// changeContent sets the active label, which is used at the content rendering.
changeContent = (newActive: string) => {
this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {}));
};
// switchSideBar opens or closes the sidebar's state.
switchSideBar = () => {
this.setState(prevState => ({sideBar: !prevState.sideBar}));
};
render() {
return (
<div className={this.props.classes.dashboard} style={styles.dashboard}>
<Header
switchSideBar={this.switchSideBar}
content={this.state.content}
/>
<Body
opened={this.state.sideBar}
changeContent={this.changeContent}
active={this.state.active}
content={this.state.content}
shouldUpdate={this.state.shouldUpdate}
send={this.send}
/>
</div>
);
}
}
export default hot(module)(withStyles(themeStyles)(Dashboard));
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import ResponsiveContainer from 'recharts/es6/component/ResponsiveContainer';
import AreaChart from 'recharts/es6/chart/AreaChart';
import Area from 'recharts/es6/cartesian/Area';
import ReferenceLine from 'recharts/es6/cartesian/ReferenceLine';
import Label from 'recharts/es6/component/Label';
import Tooltip from 'recharts/es6/component/Tooltip';
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';
const CPU = 'cpu';
const MEMORY = 'memory';
const DISK = 'disk';
const TRAFFIC = 'traffic';
const TOP = 'Top';
const BOTTOM = 'Bottom';
const cpuLabelTop = 'Process load';
const cpuLabelBottom = 'System load';
const memoryLabelTop = 'Active memory';
const memoryLabelBottom = 'Virtual memory';
const diskLabelTop = 'Disk read';
const diskLabelBottom = 'Disk write';
const trafficLabelTop = 'Download';
const trafficLabelBottom = 'Upload';
// styles contains the constant styles of the component.
const styles = {
footer: {
maxWidth: '100%',
flexWrap: 'nowrap',
margin: 0,
},
chartRowWrapper: {
height: '100%',
padding: 0,
},
doubleChartWrapper: {
height: '100%',
width: '99%',
},
link: {
color: 'inherit',
textDecoration: 'none',
},
};
// themeStyles returns the styles generated from the theme for the component.
const themeStyles: Object = (theme: Object) => ({
footer: {
backgroundColor: theme.palette.grey[900],
color: theme.palette.getContrastText(theme.palette.grey[900]),
zIndex: theme.zIndex.appBar,
height: theme.spacing.unit * 10,
},
});
export type Props = {
classes: Object, // injected by withStyles()
theme: Object,
general: General,
system: System,
shouldUpdate: Object,
};
type State = {};
// Footer renders the footer of the dashboard.
class Footer extends Component<Props, State> {
shouldComponentUpdate(nextProps: Readonly<Props>, nextState: Readonly<State>, nextContext: any) {
return typeof nextProps.shouldUpdate.general !== 'undefined' || typeof nextProps.shouldUpdate.system !== 'undefined';
}
// halfHeightChart renders an area chart with half of the height of its parent.
halfHeightChart = (chartProps, tooltip, areaProps, label, position) => (
<ResponsiveContainer width='100%' height='50%'>
<AreaChart {...chartProps}>
{!tooltip || (<Tooltip cursor={false} content={<CustomTooltip tooltip={tooltip} />} />)}
<Area isAnimationActive={false} strokeWidth={chartStrokeWidth} type='monotone' {...areaProps} />
<ReferenceLine x={0} strokeWidth={0}>
<Label fill={areaProps.fill} value={label} position={position} />
</ReferenceLine>
</AreaChart>
</ResponsiveContainer>
);
// doubleChart renders a pair of charts separated by the baseline.
doubleChart = (syncId, chartKey, topChart, bottomChart) => {
if (!Array.isArray(topChart.data) || !Array.isArray(bottomChart.data)) {
return null;
}
const topDefault = topChart.default || 0;
const bottomDefault = bottomChart.default || 0;
const topKey = `${chartKey}${TOP}`;
const bottomKey = `${chartKey}${BOTTOM}`;
const topColor = '#8884d8';
const bottomColor = '#82ca9d';
return (
<div style={styles.doubleChartWrapper}>
{this.halfHeightChart(
{
syncId,
data: topChart.data.map(({value}) => ({[topKey]: value || topDefault})),
margin: {top: 5, right: 5, bottom: 0, left: 5},
},
topChart.tooltip,
{dataKey: topKey, stroke: topColor, fill: topColor},
topChart.label,
'insideBottomLeft',
)}
{this.halfHeightChart(
{
syncId,
data: bottomChart.data.map(({value}) => ({[bottomKey]: -value || -bottomDefault})),
margin: {top: 0, right: 5, bottom: 5, left: 5},
},
bottomChart.tooltip,
{dataKey: bottomKey, stroke: bottomColor, fill: bottomColor},
bottomChart.label,
'insideTopLeft',
)}
</div>
);
};
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}>
<Grid item xs style={styles.chartRowWrapper}>
<ChartRow>
{this.doubleChart(
FOOTER_SYNC_ID,
CPU,
{data: system.processCPU, tooltip: percentPlotter(cpuLabelTop), label: cpuLabelTop},
{data: system.systemCPU, tooltip: percentPlotter(cpuLabelBottom, multiplier(-1)), label: cpuLabelBottom},
)}
{this.doubleChart(
FOOTER_SYNC_ID,
MEMORY,
{data: system.activeMemory, tooltip: bytePlotter(memoryLabelTop), label: memoryLabelTop},
{data: system.virtualMemory, tooltip: bytePlotter(memoryLabelBottom, multiplier(-1)), label: memoryLabelBottom},
)}
{this.doubleChart(
FOOTER_SYNC_ID,
DISK,
{data: system.diskRead, tooltip: bytePerSecPlotter(diskLabelTop), label: diskLabelTop},
{data: system.diskWrite, tooltip: bytePerSecPlotter(diskLabelBottom, multiplier(-1)), label: diskLabelBottom},
)}
{this.doubleChart(
FOOTER_SYNC_ID,
TRAFFIC,
{data: system.networkIngress, tooltip: bytePerSecPlotter(trafficLabelTop), label: trafficLabelTop},
{data: system.networkEgress, tooltip: bytePerSecPlotter(trafficLabelBottom, multiplier(-1)), label: trafficLabelBottom},
)}
</ChartRow>
</Grid>
<Grid item>
<Typography type='caption' color='inherit'>
<span style={commonStyles.light}>Geth</span> {general.version}
</Typography>
{general.commit && (
<Typography type='caption' color='inherit'>
<span style={commonStyles.light}>{'Commit '}</span>
<a
href={`https://github.com/ethereum/go-ethereum/commit/${general.commit}`}
target='_blank'
rel='noopener noreferrer'
style={styles.link}
>
{general.commit.substring(0, 8)}
</a>
</Typography>
)}
<Typography style={styles.headerText}>
<span style={commonStyles.light}>Network</span> {network}
</Typography>
</Grid>
</Grid>
);
}
}
export default withStyles(themeStyles)(Footer);
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
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, 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%',
minHeight: 'unset',
},
};
// themeStyles returns the styles generated from the theme for the component.
const themeStyles = (theme: Object) => ({
header: {
backgroundColor: theme.palette.grey[900],
color: theme.palette.getContrastText(theme.palette.grey[900]),
zIndex: theme.zIndex.appBar,
},
toolbar: {
paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit,
},
title: {
paddingLeft: theme.spacing.unit,
fontSize: 3 * theme.spacing.unit,
flex: 1,
},
});
export type Props = {
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, 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;
return (
<AppBar position='static' className={classes.header} style={styles.header}>
<Toolbar className={classes.toolbar} style={styles.toolbar}>
<IconButton onClick={this.props.switchSideBar}>
<FontAwesomeIcon icon={faBars} />
</IconButton>
<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>
);
}
}
export default withStyles(themeStyles)(Header);
This diff is collapsed.
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Chain from 'Chain';
import Network from 'Network';
import Logs from 'Logs';
import Footer from 'Footer';
import {MENU} from '../common';
import type {Content} from '../types/content';
// styles contains the constant styles of the component.
const styles = {
wrapper: {
display: 'flex',
flexDirection: 'column',
width: '100%',
},
content: {
flex: 1,
overflow: 'auto',
},
};
// themeStyles returns the styles generated from the theme for the component.
const themeStyles = theme => ({
content: {
backgroundColor: theme.palette.background.default,
padding: theme.spacing.unit * 3,
},
});
export type Props = {
classes: Object,
active: string,
content: Content,
shouldUpdate: Object,
send: string => void,
};
type State = {};
// Main renders the chosen content.
class Main extends Component<Props, State> {
constructor(props) {
super(props);
this.container = React.createRef();
this.content = React.createRef();
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.content && typeof this.content.didUpdate === 'function') {
this.content.didUpdate(prevProps, prevState, snapshot);
}
}
onScroll = () => {
if (this.content && typeof this.content.onScroll === 'function') {
this.content.onScroll();
}
};
getSnapshotBeforeUpdate(prevProps: Readonly<P>, prevState: Readonly<S>) {
if (this.content && typeof this.content.beforeUpdate === 'function') {
return this.content.beforeUpdate();
}
return null;
}
render() {
const {
classes, active, content, shouldUpdate,
} = this.props;
let children = null;
switch (active) {
case MENU.get('home').id:
children = <div>Work in progress.</div>;
break;
case MENU.get('chain').id:
children = <Chain
content={this.props.content.chain}
/>;
break;
case MENU.get('txpool').id:
children = <div>Work in progress.</div>;
break;
case MENU.get('network').id:
children = <Network
content={this.props.content.network}
container={this.container}
/>;
break;
case MENU.get('system').id:
children = <div>Work in progress.</div>;
break;
case MENU.get('logs').id:
children = (
<Logs
ref={(ref) => { this.content = ref; }}
container={this.container}
send={this.props.send}
content={this.props.content}
shouldUpdate={shouldUpdate}
/>
);
}
return (
<div style={styles.wrapper}>
<div
className={classes.content}
style={styles.content}
ref={(ref) => { this.container = ref; }}
onScroll={this.onScroll}
>
{children}
</div>
<Footer
general={content.general}
system={content.system}
shouldUpdate={shouldUpdate}
/>
</div>
);
}
}
export default withStyles(themeStyles)(Main);
This diff is collapsed.
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Icon from '@material-ui/core/Icon';
import Transition from 'react-transition-group/Transition';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {MENU, DURATION} from '../common';
// styles contains the constant styles of the component.
const styles = {
menu: {
default: {
transition: `margin-left ${DURATION}ms`,
},
transition: {
entered: {marginLeft: -200},
},
},
};
// themeStyles returns the styles generated from the theme for the component.
const themeStyles = theme => ({
list: {
background: theme.palette.grey[900],
},
listItem: {
minWidth: theme.spacing(7),
color: theme.palette.common.white,
},
icon: {
fontSize: theme.spacing(3),
overflow: 'unset',
},
});
export type Props = {
classes: Object, // injected by withStyles()
opened: boolean,
changeContent: string => void,
};
type State = {}
// SideBar renders the sidebar of the dashboard.
class SideBar extends Component<Props, State> {
shouldComponentUpdate(nextProps: Readonly<Props>, nextState: Readonly<State>, nextContext: any) {
return nextProps.opened !== this.props.opened;
}
// clickOn returns a click event handler function for the given menu item.
clickOn = menu => (event) => {
event.preventDefault();
this.props.changeContent(menu);
};
// menuItems returns the menu items corresponding to the sidebar state.
menuItems = (transitionState) => {
const {classes} = this.props;
const children = [];
MENU.forEach((menu) => {
children.push((
<ListItem button key={menu.id} onClick={this.clickOn(menu.id)} className={classes.listItem}>
<ListItemIcon>
<Icon className={classes.icon}>
<FontAwesomeIcon icon={menu.icon} />
</Icon>
</ListItemIcon>
<ListItemText
primary={menu.title}
style={{
...styles.menu.default,
...styles.menu.transition[transitionState],
padding: 0,
}}
/>
</ListItem>
));
});
return children;
};
// menu renders the list of the menu items.
menu = (transitionState: Object) => (
<div className={this.props.classes.list}>
<List>
{this.menuItems(transitionState)}
</List>
</div>
);
render() {
return (
<Transition mountOnEnter in={this.props.opened} timeout={{enter: DURATION}}>
{this.menu}
</Transition>
);
}
}
export default withStyles(themeStyles)(SideBar);
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// fa-only-woff-loader removes the .eot, .ttf, .svg dependencies of the FontAwesome library,
// because they produce unused extra blobs.
module.exports = content => content
.replace(/src.*url(?!.*url.*(\.eot)).*(\.eot)[^;]*;/, '')
.replace(/url(?!.*url.*(\.eot)).*(\.eot)[^,]*,/, '')
.replace(/url(?!.*url.*(\.ttf)).*(\.ttf)[^,]*,/, '')
.replace(/,[^,]*url(?!.*url.*(\.svg)).*(\.svg)[^;]*;/, ';');
<!DOCTYPE html>
<html lang="en" style="height: 100%">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Go Ethereum Dashboard</title>
<link rel="shortcut icon" type="image/ico" href="https://ethereum.org/favicon.ico" />
<style>
::-webkit-scrollbar {
width: 16px;
}
::-webkit-scrollbar-thumb {
background: #212121;
}
::-webkit-scrollbar-corner {
background: transparent;
}
</style>
</head>
<body style="height: 100%; margin: 0">
<div id="dashboard" style="height: 100%"></div>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import {render} from 'react-dom';
import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
import Dashboard from './components/Dashboard';
const theme: Object = createMuiTheme({
// typography: {
// useNextVariants: true,
// },
palette: {
type: 'dark',
},
});
const dashboard = document.getElementById('dashboard');
if (dashboard) {
// Renders the whole dashboard.
render(
<MuiThemeProvider theme={theme}>
<Dashboard />
</MuiThemeProvider>,
dashboard,
);
}
{
"private": true,
"dependencies": {
"@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.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.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": "4.0.1",
"@material-ui/icons": "4.0.1",
"babel-eslint": "10.0.1",
"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.16.0",
"eslint-config-airbnb": "^17.0.0",
"eslint-loader": "2.1.2",
"eslint-plugin-flowtype": "3.9.1",
"eslint-plugin-import": "2.17.3",
"eslint-plugin-jsx-a11y": "6.2.1",
"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.98.1",
"flow-bin-loader": "^1.0.3",
"flow-typed": "2.5.2",
"js-beautify": "1.10.0",
"path": "^0.12.7",
"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.3.0",
"url": "^0.11.0",
"url-loader": "1.1.2",
"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": {
"build": "webpack --config webpack.config.prod.js",
"stats": "webpack --config webpack.config.prod.js --profile --json > stats.json",
"dev": "webpack-dev-server --open --config webpack.config.dev.js",
"dash": "webpack-dashboard -- yarn dev",
"install-flow": "flow-typed install",
"flow": "flow status --show-all-errors",
"eslint": "eslint **/*"
},
"sideEffects": false,
"license": "LGPL-3.0-or-later"
}
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
export type Content = {
general: General,
home: Home,
chain: Chain,
txpool: TxPool,
network: Network,
system: System,
logs: Logs,
};
export type ChartEntries = Array<ChartEntry>;
export type ChartEntry = {
value: number,
};
export type General = {
version: ?string,
commit: ?string,
genesis: ?string,
};
export type Home = {
/* TODO (kurkomisi) */
};
export type Chain = {
currentBlock: Block,
};
export type Block = {
number: number,
timestamp: number,
}
export type TxPool = {
/* TODO (kurkomisi) */
};
export type Network = {
peers: Peers,
diff: Array<PeerEvent>,
activePeerCount: number,
};
export type PeerEvent = {
name: string,
addr: string,
enode: string,
protocols: {[string]: Object},
remove: string,
location: GeoLocation,
connected: Date,
disconnected: Date,
ingress: ChartEntries,
egress: ChartEntries,
activity: string,
};
export type Peers = {
bundles: {[string]: PeerBundle},
};
export type PeerBundle = {
location: GeoLocation,
knownPeers: {[string]: KnownPeer},
attempts: number,
};
export type KnownPeer = {
connected: Array<Date>,
disconnected: Array<Date>,
ingress: Array<ChartEntries>,
egress: Array<ChartEntries>,
name: string,
enode: string,
protocols: {[string]: Object},
active: boolean,
};
export type GeoLocation = {
country: string,
city: string,
latitude: number,
longitude: number,
};
export type System = {
activeMemory: ChartEntries,
virtualMemory: ChartEntries,
networkIngress: ChartEntries,
networkEgress: ChartEntries,
processCPU: ChartEntries,
systemCPU: ChartEntries,
diskRead: ChartEntries,
diskWrite: ChartEntries,
};
export type Record = {
t: string,
lvl: Object,
msg: string,
ctx: Array<string>
};
export type Chunk = {
content: string,
name: string,
};
export type Logs = {
chunks: Array<Chunk>,
endTop: boolean,
endBottom: boolean,
topChanged: number,
bottomChanged: number,
};
export type LogsMessage = {
source: ?LogFile,
chunk: Array<Record>,
};
export type LogFile = {
name: string,
last: string,
};
// 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/>.
const path = require('path');
module.exports = {
target: 'web',
entry: {
bundle: './index',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, ''),
sourceMapFilename: '[file].map',
},
resolve: {
modules: [
'node_modules',
path.resolve(__dirname, 'components'), // import './components/Component' -> import 'Component'
],
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
test: /\.jsx$/, // regexp for JSX files
exclude: /node_modules/,
use: [ // order: from bottom to top
{
loader: 'babel-loader',
options: {
presets: [ // order: from bottom to top
'@babel/env',
'@babel/react',
],
plugins: [ // order: from top to bottom
'@babel/proposal-function-bind', // instead of stage 0
'@babel/proposal-class-properties', // static defaultProps
'@babel/transform-flow-strip-types',
'react-hot-loader/babel',
],
},
},
// 'eslint-loader', // show errors in the console
],
},
{
test: /\.css$/,
oneOf: [
{
test: /font-awesome/,
use: [
'style-loader',
'css-loader',
path.resolve(__dirname, './fa-only-woff-loader.js'),
],
},
{
use: [
'style-loader',
'css-loader',
],
},
],
},
{
test: /\.woff2?$/, // font-awesome icons
use: 'url-loader',
},
],
},
};
// 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/>.
const webpack = require('webpack');
const merge = require('webpack-merge');
const WebpackDashboard = require('webpack-dashboard/plugin');
const common = require('./webpack.config.common.js');
module.exports = merge(common, {
mode: 'development',
plugins: [
new WebpackDashboard(),
new webpack.HotModuleReplacementPlugin(),
],
// devtool: 'eval',
devtool: 'source-map',
devServer: {
port: 8081,
hot: true,
compress: true,
},
});
// 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/>.
const TerserPlugin = require('terser-webpack-plugin');
const merge = require('webpack-merge');
const common = require('./webpack.config.common.js');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
optimization: {
minimize: true,
namedModules: true, // Module names instead of numbers - resolves the large diff problem.
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true,
terserOptions: {
output: {
comments: false,
beautify: true,
},
},
}),
],
},
});
This diff is collapsed.
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
}
}
}
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dashboard
import "time"
// DefaultConfig contains default settings for the dashboard.
var DefaultConfig = Config{
Host: "localhost",
Port: 8080,
Refresh: 5 * time.Second,
}
// Config contains the configuration parameters of the dashboard.
type Config struct {
// Host is the host interface on which to start the dashboard server. If this
// field is empty, no dashboard will be started.
Host string `toml:",omitempty"`
// Port is the TCP port number on which to start the dashboard server. The
// default zero value is/ valid and will pick a port number randomly (useful
// for ephemeral nodes).
Port int `toml:",omitempty"`
// Refresh is the refresh rate of the data updates, the chartEntry will be collected this often.
Refresh time.Duration `toml:",omitempty"`
}
// Copyright 2018 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/>.
// +build !windows
package dashboard
import (
"syscall"
"github.com/ethereum/go-ethereum/log"
)
// getProcessCPUTime retrieves the process' CPU time since program startup.
func getProcessCPUTime() float64 {
var usage syscall.Rusage
if err := syscall.Getrusage(syscall.RUSAGE_SELF, &usage); err != nil {
log.Warn("Failed to retrieve CPU time", "err", err)
return 0
}
return float64(usage.Utime.Sec+usage.Stime.Sec) + float64(usage.Utime.Usec+usage.Stime.Usec)/1000000
}
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dashboard
// getProcessCPUTime returns 0 on Windows as there is no system call to resolve
// the actual process' CPU time.
func getProcessCPUTime() float64 {
return 0
}
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dashboard
//go:generate yarn --cwd ./assets install
//go:generate yarn --cwd ./assets build
//go:generate yarn --cwd ./assets js-beautify -f bundle.js.map -r -w 1
//go:generate go-bindata -nometadata -o assets.go -prefix assets -nocompress -pkg dashboard assets/index.html assets/bundle.js assets/bundle.js.map
//go:generate sh -c "sed 's#var _bundleJs#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go"
//go:generate sh -c "sed 's#var _bundleJsMap#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go"
//go:generate sh -c "sed 's#var _indexHtml#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go"
//go:generate gofmt -w -s assets.go
import (
"fmt"
"io"
"net"
"net/http"
"sync"
"sync/atomic"
"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"
"github.com/ethereum/go-ethereum/rpc"
"github.com/mohae/deepcopy"
"golang.org/x/net/websocket"
)
const (
sampleLimit = 200 // Maximum number of data samples
dataCollectorCount = 4
)
// Dashboard contains the dashboard internals.
type Dashboard struct {
config *Config // Configuration values for the dashboard
listener net.Listener // Network listener listening for dashboard clients
conns map[uint32]*client // Currently live websocket connections
nextConnID uint32 // Next connection id
history *Message // Stored historical 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.
type client struct {
conn *websocket.Conn // Particular live websocket connection
msg chan *Message // Message queue for the update messages
logger log.Logger // Logger for the particular live websocket connection
}
// New creates a new dashboard instance with the given configuration.
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,
quit: make(chan chan error),
history: &Message{
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(sampleLimit),
VirtualMemory: emptyChartEntries(sampleLimit),
NetworkIngress: emptyChartEntries(sampleLimit),
NetworkEgress: emptyChartEntries(sampleLimit),
ProcessCPU: emptyChartEntries(sampleLimit),
SystemCPU: emptyChartEntries(sampleLimit),
DiskRead: emptyChartEntries(sampleLimit),
DiskWrite: emptyChartEntries(sampleLimit),
},
},
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(limit int) ChartEntries {
ce := make(ChartEntries, limit)
for i := 0; i < limit; i++ {
ce[i] = new(ChartEntry)
}
return ce
}
// Protocols implements the node.Service interface.
func (db *Dashboard) Protocols() []p2p.Protocol { return nil }
// APIs implements the node.Service interface.
func (db *Dashboard) APIs() []rpc.API { return nil }
// Start starts the data collection thread and the listening server of the dashboard.
// Implements the node.Service interface.
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(dataCollectorCount)
go db.collectChainData()
go db.collectSystemData()
go db.streamLogs()
go db.collectPeerData()
http.HandleFunc("/", db.webHandler)
http.Handle("/api", websocket.Handler(db.apiHandler))
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", db.config.Host, db.config.Port))
if err != nil {
return err
}
db.listener = listener
go func() {
if err := http.Serve(listener, nil); err != http.ErrServerClosed {
log.Warn("Could not accept incoming HTTP connections", "err", err)
}
}()
return nil
}
// Stop stops the data collection thread and the connection listener of the dashboard.
// Implements the node.Service interface.
func (db *Dashboard) Stop() error {
// Close the connection listener.
var errs []error
if err := db.listener.Close(); err != nil {
errs = append(errs, err)
}
// Close the collectors.
errc := make(chan error, dataCollectorCount)
for i := 0; i < dataCollectorCount; i++ {
db.quit <- errc
if err := <-errc; err != nil {
errs = append(errs, err)
}
}
// Close the connections.
db.lock.Lock()
for _, c := range db.conns {
if err := c.conn.Close(); err != nil {
c.logger.Warn("Failed to close connection", "err", err)
}
}
db.lock.Unlock()
// Wait until every goroutine terminates.
db.wg.Wait()
log.Info("Dashboard stopped")
var err error
if len(errs) > 0 {
err = fmt.Errorf("%v", errs)
}
return err
}
// webHandler handles all non-api requests, simply flattening and returning the dashboard website.
func (db *Dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
log.Debug("Request", "URL", r.URL)
path := r.URL.String()
if path == "/" {
path = "/index.html"
}
blob, err := Asset(path[1:])
if err != nil {
log.Warn("Failed to load the asset", "path", path, "err", err)
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Write(blob)
}
// apiHandler handles requests for the dashboard.
func (db *Dashboard) apiHandler(conn *websocket.Conn) {
id := atomic.AddUint32(&db.nextConnID, 1)
client := &client{
conn: conn,
msg: make(chan *Message, 128),
logger: log.New("id", id),
}
done := make(chan struct{})
// Start listening for messages to send.
db.wg.Add(1)
go func() {
defer db.wg.Done()
for {
select {
case <-done:
return
case msg := <-client.msg:
if err := websocket.JSON.Send(client.conn, msg); err != nil {
client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
client.conn.Close()
return
}
}
}
}()
// 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()
// Start tracking the connection and drop at connection loss.
db.lock.Lock()
client.msg <- h
db.conns[id] = client
db.lock.Unlock()
defer func() {
db.lock.Lock()
delete(db.conns, id)
db.lock.Unlock()
}()
for {
r := new(Request)
if err := websocket.JSON.Receive(conn, r); err != nil {
if err != io.EOF {
client.logger.Warn("Failed to receive request", "err", err)
}
close(done)
return
}
if r.Logs != nil {
db.handleLogRequest(r.Logs, client)
}
}
}
// sendToAll sends the given message to the active dashboards.
func (db *Dashboard) sendToAll(msg *Message) {
db.lock.Lock()
for _, c := range db.conns {
select {
case c.msg <- msg:
default:
c.conn.Close()
}
}
db.lock.Unlock()
}
// 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/>.
package dashboard
import (
"net"
"time"
"github.com/apilayer/freegeoip"
)
// geoDBInfo contains all the geographical information we could extract based on an IP
// address.
type geoDBInfo struct {
Country struct {
Names struct {
English string `maxminddb:"en" json:"en,omitempty"`
} `maxminddb:"names" json:"names,omitempty"`
} `maxminddb:"country" json:"country,omitempty"`
City struct {
Names struct {
English string `maxminddb:"en" json:"en,omitempty"`
} `maxminddb:"names" json:"names,omitempty"`
} `maxminddb:"city" json:"city,omitempty"`
Location struct {
Latitude float64 `maxminddb:"latitude" json:"latitude,omitempty"`
Longitude float64 `maxminddb:"longitude" json:"longitude,omitempty"`
} `maxminddb:"location" json:"location,omitempty"`
}
// geoLocation contains geographical information.
type geoLocation struct {
Country string `json:"country,omitempty"`
City string `json:"city,omitempty"`
Latitude float64 `json:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty"`
}
// geoDB represents a geoip database that can be queried for IP to geographical
// information conversions.
type geoDB struct {
geodb *freegeoip.DB
}
// Open creates a new geoip database with an up-to-date database from the internet.
func openGeoDB() (*geoDB, error) {
// Initiate a geoip database to cross reference locations
db, err := freegeoip.OpenURL(freegeoip.MaxMindDB, 24*time.Hour, time.Hour)
if err != nil {
return nil, err
}
// Wait until the database is updated to the latest data
select {
case <-db.NotifyOpen():
case err := <-db.NotifyError():
return nil, err
}
// Assemble and return our custom wrapper
return &geoDB{geodb: db}, nil
}
// Close terminates the database background updater.
func (db *geoDB) close() error {
db.geodb.Close()
return nil
}
// Lookup converts an IP address to a geographical location.
func (db *geoDB) lookup(ip net.IP) *geoDBInfo {
result := new(geoDBInfo)
db.geodb.Lookup(ip, result)
return result
}
// Location retrieves the geographical location of the given IP address.
func (db *geoDB) location(ip string) *geoLocation {
location := db.lookup(net.ParseIP(ip))
return &geoLocation{
Country: location.Country.Names.English,
City: location.City.Names.English,
Latitude: location.Location.Latitude,
Longitude: location.Location.Longitude,
}
}
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dashboard
import (
"bytes"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/mohae/deepcopy"
"github.com/rjeczalik/notify"
)
var emptyChunk = json.RawMessage("[]")
// prepLogs creates a JSON array from the given log record buffer.
// Returns the prepared array and the position of the last '\n'
// character in the original buffer, or -1 if it doesn't contain any.
func prepLogs(buf []byte) (json.RawMessage, int) {
b := make(json.RawMessage, 1, len(buf)+1)
b[0] = '['
b = append(b, buf...)
last := -1
for i := 1; i < len(b); i++ {
if b[i] == '\n' {
b[i] = ','
last = i
}
}
if last < 0 {
return emptyChunk, -1
}
b[last] = ']'
return b[:last+1], last - 1
}
// handleLogRequest searches for the log file specified by the timestamp of the
// request, creates a JSON array out of it and sends it to the requesting client.
func (db *Dashboard) handleLogRequest(r *LogsRequest, c *client) {
files, err := ioutil.ReadDir(db.logdir)
if err != nil {
log.Warn("Failed to open logdir", "path", db.logdir, "err", err)
return
}
re := regexp.MustCompile(`\.log$`)
fileNames := make([]string, 0, len(files))
for _, f := range files {
if f.Mode().IsRegular() && re.MatchString(f.Name()) {
fileNames = append(fileNames, f.Name())
}
}
if len(fileNames) < 1 {
log.Warn("No log files in logdir", "path", db.logdir)
return
}
idx := sort.Search(len(fileNames), func(idx int) bool {
// Returns the smallest index such as fileNames[idx] >= r.Name,
// if there is no such index, returns n.
return fileNames[idx] >= r.Name
})
switch {
case idx < 0:
return
case idx == 0 && r.Past:
return
case idx >= len(fileNames):
return
case r.Past:
idx--
case idx == len(fileNames)-1 && fileNames[idx] == r.Name:
return
case idx == len(fileNames)-1 || (idx == len(fileNames)-2 && fileNames[idx] == r.Name):
// The last file is continuously updated, and its chunks are streamed,
// so in order to avoid log record duplication on the client side, it is
// handled differently. Its actual content is always saved in the history.
db.logLock.RLock()
if db.history.Logs != nil {
c.msg <- &Message{
Logs: deepcopy.Copy(db.history.Logs).(*LogsMessage),
}
}
db.logLock.RUnlock()
return
case fileNames[idx] == r.Name:
idx++
}
path := filepath.Join(db.logdir, fileNames[idx])
var buf []byte
if buf, err = ioutil.ReadFile(path); err != nil {
log.Warn("Failed to read file", "path", path, "err", err)
return
}
chunk, end := prepLogs(buf)
if end < 0 {
log.Warn("The file doesn't contain valid logs", "path", path)
return
}
c.msg <- &Message{
Logs: &LogsMessage{
Source: &LogFile{
Name: fileNames[idx],
Last: r.Past && idx == 0,
},
Chunk: chunk,
},
}
}
// streamLogs watches the file system, and when the logger writes
// the new log records into the files, picks them up, then makes
// JSON array out of them and sends them to the clients.
func (db *Dashboard) streamLogs() {
defer db.wg.Done()
var (
err error
errc chan error
)
defer func() {
if errc == nil {
errc = <-db.quit
}
errc <- err
}()
files, err := ioutil.ReadDir(db.logdir)
if err != nil {
log.Warn("Failed to open logdir", "path", db.logdir, "err", err)
return
}
var (
opened *os.File // File descriptor for the opened active log file.
buf []byte // Contains the recently written log chunks, which are not sent to the clients yet.
)
// The log records are always written into the last file in alphabetical order, because of the timestamp.
re := regexp.MustCompile(`\.log$`)
i := len(files) - 1
for i >= 0 && (!files[i].Mode().IsRegular() || !re.MatchString(files[i].Name())) {
i--
}
if i < 0 {
log.Warn("No log files in logdir", "path", db.logdir)
return
}
if opened, err = os.OpenFile(filepath.Join(db.logdir, files[i].Name()), os.O_RDONLY, 0600); err != nil {
log.Warn("Failed to open file", "name", files[i].Name(), "err", err)
return
}
defer opened.Close() // Close the lastly opened file.
fi, err := opened.Stat()
if err != nil {
log.Warn("Problem with file", "name", opened.Name(), "err", err)
return
}
db.logLock.Lock()
db.history.Logs = &LogsMessage{
Source: &LogFile{
Name: fi.Name(),
Last: true,
},
Chunk: emptyChunk,
}
db.logLock.Unlock()
watcher := make(chan notify.EventInfo, 10)
if err := notify.Watch(db.logdir, watcher, notify.Create); err != nil {
log.Warn("Failed to create file system watcher", "err", err)
return
}
defer notify.Stop(watcher)
ticker := time.NewTicker(db.config.Refresh)
defer ticker.Stop()
loop:
for err == nil || errc == nil {
select {
case event := <-watcher:
// Make sure that new log file was created.
if !re.Match([]byte(event.Path())) {
break
}
if opened == nil {
log.Warn("The last log file is not opened")
break loop
}
// The new log file's name is always greater,
// because it is created using the actual log record's time.
if opened.Name() >= event.Path() {
break
}
// Read the rest of the previously opened file.
chunk, err := ioutil.ReadAll(opened)
if err != nil {
log.Warn("Failed to read file", "name", opened.Name(), "err", err)
break loop
}
buf = append(buf, chunk...)
opened.Close()
if chunk, last := prepLogs(buf); last >= 0 {
// Send the rest of the previously opened file.
db.sendToAll(&Message{
Logs: &LogsMessage{
Chunk: chunk,
},
})
}
if opened, err = os.OpenFile(event.Path(), os.O_RDONLY, 0644); err != nil {
log.Warn("Failed to open file", "name", event.Path(), "err", err)
break loop
}
buf = buf[:0]
// Change the last file in the history.
fi, err := opened.Stat()
if err != nil {
log.Warn("Problem with file", "name", opened.Name(), "err", err)
break loop
}
db.logLock.Lock()
db.history.Logs.Source.Name = fi.Name()
db.history.Logs.Chunk = emptyChunk
db.logLock.Unlock()
case <-ticker.C: // Send log updates to the client.
if opened == nil {
log.Warn("The last log file is not opened")
break loop
}
// Read the new logs created since the last read.
chunk, err := ioutil.ReadAll(opened)
if err != nil {
log.Warn("Failed to read file", "name", opened.Name(), "err", err)
break loop
}
b := append(buf, chunk...)
chunk, last := prepLogs(b)
if last < 0 {
break
}
// Only keep the invalid part of the buffer, which can be valid after the next read.
buf = b[last+1:]
var l *LogsMessage
// Update the history.
db.logLock.Lock()
if bytes.Equal(db.history.Logs.Chunk, emptyChunk) {
db.history.Logs.Chunk = chunk
l = deepcopy.Copy(db.history.Logs).(*LogsMessage)
} else {
b = make([]byte, len(db.history.Logs.Chunk)+len(chunk)-1)
copy(b, db.history.Logs.Chunk)
b[len(db.history.Logs.Chunk)-1] = ','
copy(b[len(db.history.Logs.Chunk):], chunk[1:])
db.history.Logs.Chunk = b
l = &LogsMessage{Chunk: chunk}
}
db.logLock.Unlock()
db.sendToAll(&Message{Logs: l})
case errc = <-db.quit:
break loop
}
}
}
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dashboard
import (
"encoding/json"
"github.com/ethereum/go-ethereum/common"
)
type Message struct {
General *GeneralMessage `json:"general,omitempty"`
Home *HomeMessage `json:"home,omitempty"`
Chain *ChainMessage `json:"chain,omitempty"`
TxPool *TxPoolMessage `json:"txpool,omitempty"`
Network *NetworkMessage `json:"network,omitempty"`
System *SystemMessage `json:"system,omitempty"`
Logs *LogsMessage `json:"logs,omitempty"`
}
type ChartEntries []*ChartEntry
type ChartEntry struct {
Value float64 `json:"value"`
}
type GeneralMessage struct {
Version string `json:"version,omitempty"`
Commit string `json:"commit,omitempty"`
Genesis common.Hash `json:"genesis,omitempty"`
}
type HomeMessage struct {
/* TODO (kurkomisi) */
}
type ChainMessage struct {
CurrentBlock *block `json:"currentBlock,omitempty"`
}
type TxPoolMessage struct {
/* TODO (kurkomisi) */
}
// NetworkMessage contains information about the peers
// organized based on their IP address and node ID.
type NetworkMessage struct {
Peers *peerContainer `json:"peers,omitempty"` // Peer tree.
Diff []*peerEvent `json:"diff,omitempty"` // Events that change the peer tree.
}
// SystemMessage contains the metered system data samples.
type SystemMessage struct {
ActiveMemory ChartEntries `json:"activeMemory,omitempty"`
VirtualMemory ChartEntries `json:"virtualMemory,omitempty"`
NetworkIngress ChartEntries `json:"networkIngress,omitempty"`
NetworkEgress ChartEntries `json:"networkEgress,omitempty"`
ProcessCPU ChartEntries `json:"processCPU,omitempty"`
SystemCPU ChartEntries `json:"systemCPU,omitempty"`
DiskRead ChartEntries `json:"diskRead,omitempty"`
DiskWrite ChartEntries `json:"diskWrite,omitempty"`
}
// LogsMessage wraps up a log chunk. If 'Source' isn't present, the chunk is a stream chunk.
type LogsMessage struct {
Source *LogFile `json:"source,omitempty"` // Attributes of the log file.
Chunk json.RawMessage `json:"chunk"` // Contains log records.
}
// LogFile contains the attributes of a log file.
type LogFile struct {
Name string `json:"name"` // The name of the file.
Last bool `json:"last"` // Denotes if the actual log file is the last one in the directory.
}
// Request represents the client request.
type Request struct {
Logs *LogsRequest `json:"logs,omitempty"`
}
// LogsRequest contains the attributes of the log file the client wants to receive.
type LogsRequest struct {
Name string `json:"name"` // The request handler searches for log file based on this file name.
Past bool `json:"past"` // Denotes whether the client wants the previous or the next file.
}
This diff is collapsed.
// 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/>.
package dashboard
import (
"runtime"
"time"
"github.com/elastic/gosigar"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p"
)
// meterCollector returns a function, which retrieves the count of a specific meter.
func meterCollector(name string) func() int64 {
if meter := metrics.Get(name); meter != nil {
m := meter.(metrics.Meter)
return func() int64 {
return m.Count()
}
}
return func() int64 {
return 0
}
}
// collectSystemData gathers data about the system and sends it to the clients.
func (db *Dashboard) collectSystemData() {
defer db.wg.Done()
systemCPUUsage := gosigar.Cpu{}
systemCPUUsage.Get()
var (
mem runtime.MemStats
collectNetworkIngress = meterCollector(p2p.MetricsInboundTraffic)
collectNetworkEgress = meterCollector(p2p.MetricsOutboundTraffic)
collectDiskRead = meterCollector("eth/db/chaindata/disk/read")
collectDiskWrite = meterCollector("eth/db/chaindata/disk/write")
prevNetworkIngress = collectNetworkIngress()
prevNetworkEgress = collectNetworkEgress()
prevProcessCPUTime = getProcessCPUTime()
prevSystemCPUUsage = systemCPUUsage
prevDiskRead = collectDiskRead()
prevDiskWrite = collectDiskWrite()
frequency = float64(db.config.Refresh / time.Second)
numCPU = float64(runtime.NumCPU())
)
for {
select {
case errc := <-db.quit:
errc <- nil
return
case <-time.After(db.config.Refresh):
systemCPUUsage.Get()
var (
curNetworkIngress = collectNetworkIngress()
curNetworkEgress = collectNetworkEgress()
curProcessCPUTime = getProcessCPUTime()
curSystemCPUUsage = systemCPUUsage
curDiskRead = collectDiskRead()
curDiskWrite = collectDiskWrite()
deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress)
deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress)
deltaProcessCPUTime = curProcessCPUTime - prevProcessCPUTime
deltaSystemCPUUsage = curSystemCPUUsage.Delta(prevSystemCPUUsage)
deltaDiskRead = curDiskRead - prevDiskRead
deltaDiskWrite = curDiskWrite - prevDiskWrite
)
prevNetworkIngress = curNetworkIngress
prevNetworkEgress = curNetworkEgress
prevProcessCPUTime = curProcessCPUTime
prevSystemCPUUsage = curSystemCPUUsage
prevDiskRead = curDiskRead
prevDiskWrite = curDiskWrite
runtime.ReadMemStats(&mem)
activeMemory := &ChartEntry{
Value: float64(mem.Alloc) / frequency,
}
virtualMemory := &ChartEntry{
Value: float64(mem.Sys) / frequency,
}
networkIngress := &ChartEntry{
Value: deltaNetworkIngress / frequency,
}
networkEgress := &ChartEntry{
Value: deltaNetworkEgress / frequency,
}
processCPU := &ChartEntry{
Value: deltaProcessCPUTime / frequency / numCPU * 100,
}
systemCPU := &ChartEntry{
Value: float64(deltaSystemCPUUsage.Sys+deltaSystemCPUUsage.User) / frequency / numCPU,
}
diskRead := &ChartEntry{
Value: float64(deltaDiskRead) / frequency,
}
diskWrite := &ChartEntry{
Value: float64(deltaDiskWrite) / frequency,
}
db.sysLock.Lock()
sys := db.history.System
sys.ActiveMemory = append(sys.ActiveMemory[1:], activeMemory)
sys.VirtualMemory = append(sys.VirtualMemory[1:], virtualMemory)
sys.NetworkIngress = append(sys.NetworkIngress[1:], networkIngress)
sys.NetworkEgress = append(sys.NetworkEgress[1:], networkEgress)
sys.ProcessCPU = append(sys.ProcessCPU[1:], processCPU)
sys.SystemCPU = append(sys.SystemCPU[1:], systemCPU)
sys.DiskRead = append(sys.DiskRead[1:], diskRead)
sys.DiskWrite = append(sys.DiskWrite[1:], diskWrite)
db.sysLock.Unlock()
db.sendToAll(&Message{
System: &SystemMessage{
ActiveMemory: ChartEntries{activeMemory},
VirtualMemory: ChartEntries{virtualMemory},
NetworkIngress: ChartEntries{networkIngress},
NetworkEgress: ChartEntries{networkEgress},
ProcessCPU: ChartEntries{processCPU},
SystemCPU: ChartEntries{systemCPU},
DiskRead: ChartEntries{diskRead},
DiskWrite: ChartEntries{diskWrite},
},
})
}
}
}
......@@ -27,7 +27,7 @@ var Enabled = false
var EnabledExpensive = false
// enablerFlags is the CLI flag names to use to enable metrics collections.
var enablerFlags = []string{"metrics", "dashboard"}
var enablerFlags = []string{"metrics"}
// expensiveEnablerFlags is the CLI flag names to use to enable metrics collections.
var expensiveEnablerFlags = []string{"metrics.expensive"}
......
# This is the official list of freegeoip authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS file.
#
# Names should be added to this file as
# Name or Organization <email address>
#
# The email address is not required for organizations.
#
# Please keep the list sorted.
Alexandre Fiori <fiorix@gmail.com>
# This is the official list of freegeoip contributors for copyright purposes.
# This file is distinct from the AUTHORS file.
#
# Names should be added to this file as
# Name or Organization <email address>
#
# Please keep the list sorted.
#
# Use the following command to generate the list:
#
# git shortlog -se | awk '{print $2 " " $3 " " $4}'
#
# The email address is not required for organizations.
Alex Goretoy <alex@goretoy.com>
Gleicon Moraes <gleicon@gmail.com>
Leandro Pereira <leandro@hardinfo.org>
Lucas Fontes <lxfontes@gmail.com>
Matthias Nehlsen <matthias.nehlsen@gmail.com>
Melchi <melchi.si@gmail.com>
Nick Muerdter <stuff@nickm.org>
Vladimir Agafonkin <agafonkin@gmail.com>
FROM golang:1.9
COPY cmd/freegeoip/public /var/www
ADD . /go/src/github.com/apilayer/freegeoip
RUN \
cd /go/src/github.com/apilayer/freegeoip/cmd/freegeoip && \
go get -d && go install && \
apt-get update && apt-get install -y libcap2-bin && \
setcap cap_net_bind_service=+ep /go/bin/freegeoip && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
useradd -ms /bin/bash freegeoip
USER freegeoip
ENTRYPOINT ["/go/bin/freegeoip"]
EXPOSE 8080
# CMD instructions:
# Add "-use-x-forwarded-for" if your server is behind a reverse proxy
# Add "-public", "/var/www" to enable the web front-end
# Add "-internal-server", "8888" to enable the pprof+metrics server
#
# Example:
# CMD ["-use-x-forwarded-for", "-public", "/var/www", "-internal-server", "8888"]
This diff is collapsed.
Copyright (c) 2009 The freegeoip authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* The names of authors or contributors may NOT be used to endorse or
promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
web: freegeoip -http :${PORT} -use-x-forwarded-for -public /app/cmd/freegeoip/public -quota-backend map -quota-max 10000
This diff is collapsed.
{
"name": "freegeoip",
"description": "IP geolocation web server",
"website": "https://github.com/apilayer/freegeoip",
"success_url": "/",
"keywords": ["golang", "geoip", "api"]
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Contributing
## Moving Notice
There is a fork being actively developed with a new API in preparation for the Go Standard Library:
[github.com/go-fsnotify/fsnotify](https://github.com/go-fsnotify/fsnotify)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment