cmd, dashboard, log: log collection and exploration (#17097)
* cmd, dashboard, internal, log, node: logging feature * cmd, dashboard, internal, log: requested changes * dashboard, vendor: gofmt, govendor, use vendored file watcher * dashboard, log: gofmt -s -w, goimports * dashboard, log: gosimple
This commit is contained in:
parent
2eedbe799f
commit
a9835c1816
|
@ -199,7 +199,12 @@ func init() {
|
||||||
|
|
||||||
app.Before = func(ctx *cli.Context) error {
|
app.Before = func(ctx *cli.Context) error {
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
if err := debug.Setup(ctx); err != nil {
|
|
||||||
|
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 err
|
||||||
}
|
}
|
||||||
// Cap the cache allowance and tune the garbage collector
|
// Cap the cache allowance and tune the garbage collector
|
||||||
|
|
|
@ -432,7 +432,7 @@ pv(1) tool to get a progress bar:
|
||||||
app.Flags = append(app.Flags, swarmmetrics.Flags...)
|
app.Flags = append(app.Flags, swarmmetrics.Flags...)
|
||||||
app.Before = func(ctx *cli.Context) error {
|
app.Before = func(ctx *cli.Context) error {
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
if err := debug.Setup(ctx); err != nil {
|
if err := debug.Setup(ctx, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
swarmmetrics.Setup(ctx)
|
swarmmetrics.Setup(ctx)
|
||||||
|
|
|
@ -193,7 +193,7 @@ var (
|
||||||
}
|
}
|
||||||
// Dashboard settings
|
// Dashboard settings
|
||||||
DashboardEnabledFlag = cli.BoolFlag{
|
DashboardEnabledFlag = cli.BoolFlag{
|
||||||
Name: "dashboard",
|
Name: metrics.DashboardEnabledFlag,
|
||||||
Usage: "Enable the dashboard",
|
Usage: "Enable the dashboard",
|
||||||
}
|
}
|
||||||
DashboardAddrFlag = cli.StringFlag{
|
DashboardAddrFlag = cli.StringFlag{
|
||||||
|
@ -1185,7 +1185,7 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
|
||||||
// RegisterDashboardService adds a dashboard to the stack.
|
// RegisterDashboardService adds a dashboard to the stack.
|
||||||
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) {
|
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) {
|
||||||
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||||
return dashboard.New(cfg, commit)
|
return dashboard.New(cfg, commit, ctx.ResolvePath("logs")), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18110
dashboard/assets.go
18110
dashboard/assets.go
File diff suppressed because one or more lines are too long
|
@ -68,4 +68,4 @@ export const styles = {
|
||||||
light: {
|
light: {
|
||||||
color: 'rgba(255, 255, 255, 0.54)',
|
color: 'rgba(255, 255, 255, 0.54)',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -32,11 +32,12 @@ const styles = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
changeContent: string => void,
|
changeContent: string => void,
|
||||||
active: string,
|
active: string,
|
||||||
content: Content,
|
content: Content,
|
||||||
shouldUpdate: Object,
|
shouldUpdate: Object,
|
||||||
|
send: string => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Body renders the body of the dashboard.
|
// Body renders the body of the dashboard.
|
||||||
|
@ -52,6 +53,7 @@ class Body extends Component<Props> {
|
||||||
active={this.props.active}
|
active={this.props.active}
|
||||||
content={this.props.content}
|
content={this.props.content}
|
||||||
shouldUpdate={this.props.shouldUpdate}
|
shouldUpdate={this.props.shouldUpdate}
|
||||||
|
send={this.props.send}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -85,7 +85,7 @@ export type Props = {
|
||||||
class CustomTooltip extends Component<Props> {
|
class CustomTooltip extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const {active, payload, tooltip} = this.props;
|
const {active, payload, tooltip} = this.props;
|
||||||
if (!active || typeof tooltip !== 'function') {
|
if (!active || typeof tooltip !== 'function' || !Array.isArray(payload) || payload.length < 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return tooltip(payload[0].value);
|
return tooltip(payload[0].value);
|
||||||
|
|
|
@ -24,6 +24,7 @@ import Header from './Header';
|
||||||
import Body from './Body';
|
import Body from './Body';
|
||||||
import {MENU} from '../common';
|
import {MENU} from '../common';
|
||||||
import type {Content} from '../types/content';
|
import type {Content} from '../types/content';
|
||||||
|
import {inserter as logInserter} from './Logs';
|
||||||
|
|
||||||
// deepUpdate updates an object corresponding to the given update data, which has
|
// 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
|
// the shape of the same structure as the original object. updater also has the same
|
||||||
|
@ -75,8 +76,11 @@ const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, pre
|
||||||
...update.map(sample => mapper(sample)),
|
...update.map(sample => mapper(sample)),
|
||||||
].slice(-limit);
|
].slice(-limit);
|
||||||
|
|
||||||
// defaultContent is the initial value of the state content.
|
// defaultContent returns the initial value of the state content. Needs to be a function in order to
|
||||||
const defaultContent: Content = {
|
// 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: {
|
general: {
|
||||||
version: null,
|
version: null,
|
||||||
commit: null,
|
commit: null,
|
||||||
|
@ -95,10 +99,14 @@ const defaultContent: Content = {
|
||||||
diskRead: [],
|
diskRead: [],
|
||||||
diskWrite: [],
|
diskWrite: [],
|
||||||
},
|
},
|
||||||
logs: {
|
logs: {
|
||||||
log: [],
|
chunks: [],
|
||||||
|
endTop: false,
|
||||||
|
endBottom: true,
|
||||||
|
topChanged: 0,
|
||||||
|
bottomChanged: 0,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
// updaters contains the state updater functions for each path of the state.
|
// updaters contains the state updater functions for each path of the state.
|
||||||
//
|
//
|
||||||
|
@ -122,9 +130,7 @@ const updaters = {
|
||||||
diskRead: appender(200),
|
diskRead: appender(200),
|
||||||
diskWrite: appender(200),
|
diskWrite: appender(200),
|
||||||
},
|
},
|
||||||
logs: {
|
logs: logInserter(5),
|
||||||
log: appender(200),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// styles contains the constant styles of the component.
|
// styles contains the constant styles of the component.
|
||||||
|
@ -151,10 +157,11 @@ export type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
active: string, // active menu
|
active: string, // active menu
|
||||||
sideBar: boolean, // true if the sidebar is opened
|
sideBar: boolean, // true if the sidebar is opened
|
||||||
content: Content, // the visualized data
|
content: Content, // the visualized data
|
||||||
shouldUpdate: Object, // labels for the components, which need to re-render based on the incoming message
|
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
|
// Dashboard is the main component, which renders the whole page, makes connection with the server and
|
||||||
|
@ -165,8 +172,9 @@ class Dashboard extends Component<Props, State> {
|
||||||
this.state = {
|
this.state = {
|
||||||
active: MENU.get('home').id,
|
active: MENU.get('home').id,
|
||||||
sideBar: true,
|
sideBar: true,
|
||||||
content: defaultContent,
|
content: defaultContent(),
|
||||||
shouldUpdate: {},
|
shouldUpdate: {},
|
||||||
|
server: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +189,7 @@ class Dashboard extends Component<Props, State> {
|
||||||
// PROD is defined by webpack.
|
// PROD is defined by webpack.
|
||||||
const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://')}${PROD ? window.location.host : 'localhost:8080'}/api`);
|
const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://')}${PROD ? window.location.host : 'localhost:8080'}/api`);
|
||||||
server.onopen = () => {
|
server.onopen = () => {
|
||||||
this.setState({content: defaultContent, shouldUpdate: {}});
|
this.setState({content: defaultContent(), shouldUpdate: {}, server});
|
||||||
};
|
};
|
||||||
server.onmessage = (event) => {
|
server.onmessage = (event) => {
|
||||||
const msg: $Shape<Content> = JSON.parse(event.data);
|
const msg: $Shape<Content> = JSON.parse(event.data);
|
||||||
|
@ -192,10 +200,18 @@ class Dashboard extends Component<Props, State> {
|
||||||
this.update(msg);
|
this.update(msg);
|
||||||
};
|
};
|
||||||
server.onclose = () => {
|
server.onclose = () => {
|
||||||
|
this.setState({server: null});
|
||||||
setTimeout(this.reconnect, 3000);
|
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 updates the content corresponding to the incoming message.
|
||||||
update = (msg: $Shape<Content>) => {
|
update = (msg: $Shape<Content>) => {
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
|
@ -226,6 +242,7 @@ class Dashboard extends Component<Props, State> {
|
||||||
active={this.state.active}
|
active={this.state.active}
|
||||||
content={this.state.content}
|
content={this.state.content}
|
||||||
shouldUpdate={this.state.shouldUpdate}
|
shouldUpdate={this.state.shouldUpdate}
|
||||||
|
send={this.send}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,310 @@
|
||||||
|
// @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 List, {ListItem} from 'material-ui/List';
|
||||||
|
import type {Record, Content, LogsMessage, Logs as LogsType} from '../types/content';
|
||||||
|
|
||||||
|
// requestBand says how wide is the top/bottom zone, eg. 0.1 means 10% of the container height.
|
||||||
|
const requestBand = 0.05;
|
||||||
|
|
||||||
|
// fieldPadding is a global map with maximum field value lengths seen until now
|
||||||
|
// to allow padding log contexts in a bit smarter way.
|
||||||
|
const fieldPadding = new Map();
|
||||||
|
|
||||||
|
// createChunk creates an HTML formatted object, which displays the given array similarly to
|
||||||
|
// the server side terminal.
|
||||||
|
const createChunk = (records: Array<Record>) => {
|
||||||
|
let content = '';
|
||||||
|
records.forEach((record) => {
|
||||||
|
const {t, ctx} = record;
|
||||||
|
let {lvl, msg} = record;
|
||||||
|
let color = '#ce3c23';
|
||||||
|
switch (lvl) {
|
||||||
|
case 'trace':
|
||||||
|
case 'trce':
|
||||||
|
lvl = 'TRACE';
|
||||||
|
color = '#3465a4';
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
case 'dbug':
|
||||||
|
lvl = 'DEBUG';
|
||||||
|
color = '#3d989b';
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
lvl = 'INFO ';
|
||||||
|
color = '#4c8f0f';
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
lvl = 'WARN ';
|
||||||
|
color = '#b79a22';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
case 'eror':
|
||||||
|
lvl = 'ERROR';
|
||||||
|
color = '#754b70';
|
||||||
|
break;
|
||||||
|
case 'crit':
|
||||||
|
lvl = 'CRIT ';
|
||||||
|
color = '#ce3c23';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
lvl = '';
|
||||||
|
}
|
||||||
|
const time = new Date(t);
|
||||||
|
if (lvl === '' || !(time instanceof Date) || isNaN(time) || typeof msg !== 'string' || !Array.isArray(ctx)) {
|
||||||
|
content += '<span style="color:#ce3c23">Invalid log record</span><br />';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ctx.length > 0) {
|
||||||
|
msg += ' '.repeat(Math.max(40 - msg.length, 0));
|
||||||
|
}
|
||||||
|
const month = `0${time.getMonth() + 1}`.slice(-2);
|
||||||
|
const date = `0${time.getDate()}`.slice(-2);
|
||||||
|
const hours = `0${time.getHours()}`.slice(-2);
|
||||||
|
const minutes = `0${time.getMinutes()}`.slice(-2);
|
||||||
|
const seconds = `0${time.getSeconds()}`.slice(-2);
|
||||||
|
content += `<span style="color:${color}">${lvl}</span>[${month}-${date}|${hours}:${minutes}:${seconds}] ${msg}`;
|
||||||
|
|
||||||
|
for (let i = 0; i < ctx.length; i += 2) {
|
||||||
|
const key = ctx[i];
|
||||||
|
const val = ctx[i + 1];
|
||||||
|
let padding = fieldPadding.get(key);
|
||||||
|
if (typeof padding !== 'number' || padding < val.length) {
|
||||||
|
padding = val.length;
|
||||||
|
fieldPadding.set(key, padding);
|
||||||
|
}
|
||||||
|
let p = '';
|
||||||
|
if (i < ctx.length - 2) {
|
||||||
|
p = ' '.repeat(padding - val.length);
|
||||||
|
}
|
||||||
|
content += ` <span style="color:${color}">${key}</span>=${val}${p}`;
|
||||||
|
}
|
||||||
|
content += '<br />';
|
||||||
|
});
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
// inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array.
|
||||||
|
// limit is the maximum length of the chunk array, used in order to prevent the browser from OOM.
|
||||||
|
export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) => {
|
||||||
|
prev.topChanged = 0;
|
||||||
|
prev.bottomChanged = 0;
|
||||||
|
if (!Array.isArray(update.chunk) || update.chunk.length < 1) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(prev.chunks)) {
|
||||||
|
prev.chunks = [];
|
||||||
|
}
|
||||||
|
const content = createChunk(update.chunk);
|
||||||
|
if (!update.source) {
|
||||||
|
// In case of stream chunk.
|
||||||
|
if (!prev.endBottom) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
if (prev.chunks.length < 1) {
|
||||||
|
// This should never happen, because the first chunk is always a non-stream chunk.
|
||||||
|
return [{content, name: '00000000000000.log'}];
|
||||||
|
}
|
||||||
|
prev.chunks[prev.chunks.length - 1].content += content;
|
||||||
|
prev.bottomChanged = 1;
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const chunk = {
|
||||||
|
content,
|
||||||
|
name: update.source.name,
|
||||||
|
};
|
||||||
|
if (prev.chunks.length > 0 && update.source.name < prev.chunks[0].name) {
|
||||||
|
if (update.source.last) {
|
||||||
|
prev.endTop = true;
|
||||||
|
}
|
||||||
|
if (prev.chunks.length >= limit) {
|
||||||
|
prev.endBottom = false;
|
||||||
|
prev.chunks.splice(limit - 1, prev.chunks.length - limit + 1);
|
||||||
|
prev.bottomChanged = -1;
|
||||||
|
}
|
||||||
|
prev.chunks = [chunk, ...prev.chunks];
|
||||||
|
prev.topChanged = 1;
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
if (update.source.last) {
|
||||||
|
prev.endBottom = true;
|
||||||
|
}
|
||||||
|
if (prev.chunks.length >= limit) {
|
||||||
|
prev.endTop = false;
|
||||||
|
prev.chunks.splice(0, prev.chunks.length - limit + 1);
|
||||||
|
prev.topChanged = -1;
|
||||||
|
}
|
||||||
|
prev.chunks = [...prev.chunks, chunk];
|
||||||
|
prev.bottomChanged = 1;
|
||||||
|
return prev;
|
||||||
|
};
|
||||||
|
|
||||||
|
// styles contains the constant styles of the component.
|
||||||
|
const styles = {
|
||||||
|
logListItem: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
logChunk: {
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
container: Object,
|
||||||
|
content: Content,
|
||||||
|
shouldUpdate: Object,
|
||||||
|
send: string => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
requestAllowed: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logs renders the log page.
|
||||||
|
class Logs extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.content = React.createRef();
|
||||||
|
this.state = {
|
||||||
|
requestAllowed: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {container} = this.props;
|
||||||
|
container.scrollTop = container.scrollHeight - container.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// onScroll is triggered by the parent component's scroll event, and sends requests if the scroll position is
|
||||||
|
// at the top or at the bottom.
|
||||||
|
onScroll = () => {
|
||||||
|
if (!this.state.requestAllowed || typeof this.content === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {logs} = this.props.content;
|
||||||
|
if (logs.chunks.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.atTop()) {
|
||||||
|
if (!logs.endTop) {
|
||||||
|
this.setState({requestAllowed: false});
|
||||||
|
this.props.send(JSON.stringify({
|
||||||
|
Logs: {
|
||||||
|
Name: logs.chunks[0].name,
|
||||||
|
Past: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else if (this.atBottom()) {
|
||||||
|
if (!logs.endBottom) {
|
||||||
|
this.setState({requestAllowed: false});
|
||||||
|
this.props.send(JSON.stringify({
|
||||||
|
Logs: {
|
||||||
|
Name: logs.chunks[logs.chunks.length - 1].name,
|
||||||
|
Past: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// atTop checks if the scroll position it at the top of the container.
|
||||||
|
atTop = () => this.props.container.scrollTop <= this.props.container.scrollHeight * requestBand;
|
||||||
|
|
||||||
|
// atBottom checks if the scroll position it at the bottom of the container.
|
||||||
|
atBottom = () => {
|
||||||
|
const {container} = this.props;
|
||||||
|
return container.scrollHeight - container.scrollTop <=
|
||||||
|
container.clientHeight + container.scrollHeight * requestBand;
|
||||||
|
};
|
||||||
|
|
||||||
|
// beforeUpdate is called by the parent component, saves the previous scroll position
|
||||||
|
// and the height of the first log chunk, which can be deleted during the insertion.
|
||||||
|
beforeUpdate = () => {
|
||||||
|
let firstHeight = 0;
|
||||||
|
if (this.content && this.content.children[0] && this.content.children[0].children[0]) {
|
||||||
|
firstHeight = this.content.children[0].children[0].clientHeight;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
scrollTop: this.props.container.scrollTop,
|
||||||
|
firstHeight,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// didUpdate is called by the parent component, which provides the container. Sends the first request if the
|
||||||
|
// visible part of the container isn't full, and resets the scroll position in order to avoid jumping when new
|
||||||
|
// chunk is inserted.
|
||||||
|
didUpdate = (prevProps, prevState, snapshot) => {
|
||||||
|
if (typeof this.props.shouldUpdate.logs === 'undefined' || typeof this.content === 'undefined' || snapshot === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {logs} = this.props.content;
|
||||||
|
const {container} = this.props;
|
||||||
|
if (typeof container === 'undefined' || logs.chunks.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.content.clientHeight < container.clientHeight) {
|
||||||
|
// Only enters here at the beginning, when there isn't enough log to fill the container
|
||||||
|
// and the scroll bar doesn't appear.
|
||||||
|
if (!logs.endTop) {
|
||||||
|
this.setState({requestAllowed: false});
|
||||||
|
this.props.send(JSON.stringify({
|
||||||
|
Logs: {
|
||||||
|
Name: logs.chunks[0].name,
|
||||||
|
Past: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chunks = this.content.children[0].children;
|
||||||
|
let {scrollTop} = snapshot;
|
||||||
|
if (logs.topChanged > 0) {
|
||||||
|
scrollTop += chunks[0].clientHeight;
|
||||||
|
} else if (logs.bottomChanged > 0) {
|
||||||
|
if (logs.topChanged < 0) {
|
||||||
|
scrollTop -= snapshot.firstHeight;
|
||||||
|
} else if (logs.endBottom && this.atBottom()) {
|
||||||
|
scrollTop = container.scrollHeight - container.clientHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.scrollTop = scrollTop;
|
||||||
|
this.setState({requestAllowed: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div ref={(ref) => { this.content = ref; }}>
|
||||||
|
<List>
|
||||||
|
{this.props.content.logs.chunks.map((c, index) => (
|
||||||
|
<ListItem style={styles.logListItem} key={index}>
|
||||||
|
<div style={styles.logChunk} dangerouslySetInnerHTML={{__html: c.content}} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logs;
|
|
@ -21,6 +21,7 @@ import React, {Component} from 'react';
|
||||||
import withStyles from 'material-ui/styles/withStyles';
|
import withStyles from 'material-ui/styles/withStyles';
|
||||||
|
|
||||||
import {MENU} from '../common';
|
import {MENU} from '../common';
|
||||||
|
import Logs from './Logs';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import type {Content} from '../types/content';
|
import type {Content} from '../types/content';
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ const styles = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -46,14 +47,40 @@ const themeStyles = theme => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
active: string,
|
active: string,
|
||||||
content: Content,
|
content: Content,
|
||||||
shouldUpdate: Object,
|
shouldUpdate: Object,
|
||||||
|
send: string => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Main renders the chosen content.
|
// Main renders the chosen content.
|
||||||
class Main extends Component<Props> {
|
class Main extends Component<Props> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.container = React.createRef();
|
||||||
|
this.content = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshotBeforeUpdate() {
|
||||||
|
if (this.content && typeof this.content.beforeUpdate === 'function') {
|
||||||
|
return this.content.beforeUpdate();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
classes, active, content, shouldUpdate,
|
classes, active, content, shouldUpdate,
|
||||||
|
@ -69,12 +96,27 @@ class Main extends Component<Props> {
|
||||||
children = <div>Work in progress.</div>;
|
children = <div>Work in progress.</div>;
|
||||||
break;
|
break;
|
||||||
case MENU.get('logs').id:
|
case MENU.get('logs').id:
|
||||||
children = <div>{content.logs.log.map((log, index) => <div key={index}>{log}</div>)}</div>;
|
children = (
|
||||||
|
<Logs
|
||||||
|
ref={(ref) => { this.content = ref; }}
|
||||||
|
container={this.container}
|
||||||
|
send={this.props.send}
|
||||||
|
content={this.props.content}
|
||||||
|
shouldUpdate={shouldUpdate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.wrapper}>
|
<div style={styles.wrapper}>
|
||||||
<div className={classes.content} style={styles.content}>{children}</div>
|
<div
|
||||||
|
className={classes.content}
|
||||||
|
style={styles.content}
|
||||||
|
ref={(ref) => { this.container = ref; }}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
<Footer
|
<Footer
|
||||||
general={content.general}
|
general={content.general}
|
||||||
system={content.system}
|
system={content.system}
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #212121;
|
background: #212121;
|
||||||
}
|
}
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="height: 100%; margin: 0">
|
<body style="height: 100%; margin: 0">
|
||||||
|
|
|
@ -18,24 +18,24 @@
|
||||||
|
|
||||||
export type Content = {
|
export type Content = {
|
||||||
general: General,
|
general: General,
|
||||||
home: Home,
|
home: Home,
|
||||||
chain: Chain,
|
chain: Chain,
|
||||||
txpool: TxPool,
|
txpool: TxPool,
|
||||||
network: Network,
|
network: Network,
|
||||||
system: System,
|
system: System,
|
||||||
logs: Logs,
|
logs: Logs,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChartEntries = Array<ChartEntry>;
|
export type ChartEntries = Array<ChartEntry>;
|
||||||
|
|
||||||
export type ChartEntry = {
|
export type ChartEntry = {
|
||||||
time: Date,
|
time: Date,
|
||||||
value: number,
|
value: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type General = {
|
export type General = {
|
||||||
version: ?string,
|
version: ?string,
|
||||||
commit: ?string,
|
commit: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Home = {
|
export type Home = {
|
||||||
|
@ -55,16 +55,42 @@ export type Network = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type System = {
|
export type System = {
|
||||||
activeMemory: ChartEntries,
|
activeMemory: ChartEntries,
|
||||||
virtualMemory: ChartEntries,
|
virtualMemory: ChartEntries,
|
||||||
networkIngress: ChartEntries,
|
networkIngress: ChartEntries,
|
||||||
networkEgress: ChartEntries,
|
networkEgress: ChartEntries,
|
||||||
processCPU: ChartEntries,
|
processCPU: ChartEntries,
|
||||||
systemCPU: ChartEntries,
|
systemCPU: ChartEntries,
|
||||||
diskRead: ChartEntries,
|
diskRead: ChartEntries,
|
||||||
diskWrite: 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 = {
|
export type Logs = {
|
||||||
log: Array<string>,
|
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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,70 +2,77 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@babel/code-frame@7.0.0-beta.40", "@babel/code-frame@^7.0.0-beta.40":
|
"@babel/code-frame@7.0.0-beta.44":
|
||||||
version "7.0.0-beta.40"
|
version "7.0.0-beta.44"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.40.tgz#37e2b0cf7c56026b4b21d3927cadf81adec32ac6"
|
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/highlight" "7.0.0-beta.40"
|
"@babel/highlight" "7.0.0-beta.44"
|
||||||
|
|
||||||
"@babel/generator@7.0.0-beta.40":
|
"@babel/generator@7.0.0-beta.44":
|
||||||
version "7.0.0-beta.40"
|
version "7.0.0-beta.44"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.40.tgz#ab61f9556f4f71dbd1138949c795bb9a21e302ea"
|
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "7.0.0-beta.40"
|
"@babel/types" "7.0.0-beta.44"
|
||||||
jsesc "^2.5.1"
|
jsesc "^2.5.1"
|
||||||
lodash "^4.2.0"
|
lodash "^4.2.0"
|
||||||
source-map "^0.5.0"
|
source-map "^0.5.0"
|
||||||
trim-right "^1.0.1"
|
trim-right "^1.0.1"
|
||||||
|
|
||||||
"@babel/helper-function-name@7.0.0-beta.40":
|
"@babel/helper-function-name@7.0.0-beta.44":
|
||||||
version "7.0.0-beta.40"
|
version "7.0.0-beta.44"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.40.tgz#9d033341ab16517f40d43a73f2d81fc431ccd7b6"
|
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-get-function-arity" "7.0.0-beta.40"
|
"@babel/helper-get-function-arity" "7.0.0-beta.44"
|
||||||
"@babel/template" "7.0.0-beta.40"
|
"@babel/template" "7.0.0-beta.44"
|
||||||
"@babel/types" "7.0.0-beta.40"
|
"@babel/types" "7.0.0-beta.44"
|
||||||
|
|
||||||
"@babel/helper-get-function-arity@7.0.0-beta.40":
|
"@babel/helper-get-function-arity@7.0.0-beta.44":
|
||||||
version "7.0.0-beta.40"
|
version "7.0.0-beta.44"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.40.tgz#ac0419cf067b0ec16453e1274f03878195791c6e"
|
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "7.0.0-beta.40"
|
"@babel/types" "7.0.0-beta.44"
|
||||||
|
|
||||||
"@babel/highlight@7.0.0-beta.40":
|
"@babel/helper-split-export-declaration@7.0.0-beta.44":
|
||||||
version "7.0.0-beta.40"
|
version "7.0.0-beta.44"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.40.tgz#b43d67d76bf46e1d10d227f68cddcd263786b255"
|
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc"
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "7.0.0-beta.44"
|
||||||
|
|
||||||
|
"@babel/highlight@7.0.0-beta.44":
|
||||||
|
version "7.0.0-beta.44"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5"
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk "^2.0.0"
|
chalk "^2.0.0"
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
js-tokens "^3.0.0"
|
js-tokens "^3.0.0"
|
||||||
|
|
||||||
"@babel/template@7.0.0-beta.40":
|
"@babel/template@7.0.0-beta.44":
|
||||||
version "7.0.0-beta.40"
|
version "7.0.0-beta.44"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.40.tgz#034988c6424eb5c3268fe6a608626de1f4410fc8"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "7.0.0-beta.40"
|
"@babel/code-frame" "7.0.0-beta.44"
|
||||||
"@babel/types" "7.0.0-beta.40"
|
"@babel/types" "7.0.0-beta.44"
|
||||||
babylon "7.0.0-beta.40"
|
babylon "7.0.0-beta.44"
|
||||||
lodash "^4.2.0"
|
lodash "^4.2.0"
|
||||||
|
|
||||||
"@babel/traverse@^7.0.0-beta.40":
|
"@babel/traverse@7.0.0-beta.44":
|
||||||
version "7.0.0-beta.40"
|
version "7.0.0-beta.44"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.40.tgz#d140e449b2e093ef9fe1a2eecc28421ffb4e521e"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "7.0.0-beta.40"
|
"@babel/code-frame" "7.0.0-beta.44"
|
||||||
"@babel/generator" "7.0.0-beta.40"
|
"@babel/generator" "7.0.0-beta.44"
|
||||||
"@babel/helper-function-name" "7.0.0-beta.40"
|
"@babel/helper-function-name" "7.0.0-beta.44"
|
||||||
"@babel/types" "7.0.0-beta.40"
|
"@babel/helper-split-export-declaration" "7.0.0-beta.44"
|
||||||
babylon "7.0.0-beta.40"
|
"@babel/types" "7.0.0-beta.44"
|
||||||
debug "^3.0.1"
|
babylon "7.0.0-beta.44"
|
||||||
|
debug "^3.1.0"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
invariant "^2.2.0"
|
invariant "^2.2.0"
|
||||||
lodash "^4.2.0"
|
lodash "^4.2.0"
|
||||||
|
|
||||||
"@babel/types@7.0.0-beta.40", "@babel/types@^7.0.0-beta.40":
|
"@babel/types@7.0.0-beta.44":
|
||||||
version "7.0.0-beta.40"
|
version "7.0.0-beta.44"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.40.tgz#25c3d7aae14126abe05fcb098c65a66b6d6b8c14"
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757"
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
lodash "^4.2.0"
|
lodash "^4.2.0"
|
||||||
|
@ -376,8 +383,8 @@ babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
|
||||||
js-tokens "^3.0.2"
|
js-tokens "^3.0.2"
|
||||||
|
|
||||||
babel-core@^6.26.0:
|
babel-core@^6.26.0:
|
||||||
version "6.26.0"
|
version "6.26.3"
|
||||||
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
|
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-code-frame "^6.26.0"
|
babel-code-frame "^6.26.0"
|
||||||
babel-generator "^6.26.0"
|
babel-generator "^6.26.0"
|
||||||
|
@ -389,24 +396,24 @@ babel-core@^6.26.0:
|
||||||
babel-traverse "^6.26.0"
|
babel-traverse "^6.26.0"
|
||||||
babel-types "^6.26.0"
|
babel-types "^6.26.0"
|
||||||
babylon "^6.18.0"
|
babylon "^6.18.0"
|
||||||
convert-source-map "^1.5.0"
|
convert-source-map "^1.5.1"
|
||||||
debug "^2.6.8"
|
debug "^2.6.9"
|
||||||
json5 "^0.5.1"
|
json5 "^0.5.1"
|
||||||
lodash "^4.17.4"
|
lodash "^4.17.4"
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
path-is-absolute "^1.0.1"
|
path-is-absolute "^1.0.1"
|
||||||
private "^0.1.7"
|
private "^0.1.8"
|
||||||
slash "^1.0.0"
|
slash "^1.0.0"
|
||||||
source-map "^0.5.6"
|
source-map "^0.5.7"
|
||||||
|
|
||||||
babel-eslint@^8.2.1:
|
babel-eslint@^8.2.1:
|
||||||
version "8.2.2"
|
version "8.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.2.tgz#1102273354c6f0b29b4ea28a65f97d122296b68b"
|
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.0.0-beta.40"
|
"@babel/code-frame" "7.0.0-beta.44"
|
||||||
"@babel/traverse" "^7.0.0-beta.40"
|
"@babel/traverse" "7.0.0-beta.44"
|
||||||
"@babel/types" "^7.0.0-beta.40"
|
"@babel/types" "7.0.0-beta.44"
|
||||||
babylon "^7.0.0-beta.40"
|
babylon "7.0.0-beta.44"
|
||||||
eslint-scope "~3.7.1"
|
eslint-scope "~3.7.1"
|
||||||
eslint-visitor-keys "^1.0.0"
|
eslint-visitor-keys "^1.0.0"
|
||||||
|
|
||||||
|
@ -550,8 +557,8 @@ babel-helpers@^6.24.1:
|
||||||
babel-template "^6.24.1"
|
babel-template "^6.24.1"
|
||||||
|
|
||||||
babel-loader@^7.1.2:
|
babel-loader@^7.1.2:
|
||||||
version "7.1.3"
|
version "7.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.3.tgz#ff5b440da716e9153abb946251a9ab7670037b16"
|
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015"
|
||||||
dependencies:
|
dependencies:
|
||||||
find-cache-dir "^1.0.0"
|
find-cache-dir "^1.0.0"
|
||||||
loader-utils "^1.0.2"
|
loader-utils "^1.0.2"
|
||||||
|
@ -1081,9 +1088,9 @@ babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
|
||||||
lodash "^4.17.4"
|
lodash "^4.17.4"
|
||||||
to-fast-properties "^1.0.3"
|
to-fast-properties "^1.0.3"
|
||||||
|
|
||||||
babylon@7.0.0-beta.40, babylon@^7.0.0-beta.40:
|
babylon@7.0.0-beta.44:
|
||||||
version "7.0.0-beta.40"
|
version "7.0.0-beta.44"
|
||||||
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.40.tgz#91fc8cd56d5eb98b28e6fde41045f2957779940a"
|
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
|
||||||
|
|
||||||
babylon@^6.18.0:
|
babylon@^6.18.0:
|
||||||
version "6.18.0"
|
version "6.18.0"
|
||||||
|
@ -1413,7 +1420,15 @@ chalk@^1.1.3:
|
||||||
strip-ansi "^3.0.0"
|
strip-ansi "^3.0.0"
|
||||||
supports-color "^2.0.0"
|
supports-color "^2.0.0"
|
||||||
|
|
||||||
chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.1:
|
chalk@^2.0.0:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^3.2.1"
|
||||||
|
escape-string-regexp "^1.0.5"
|
||||||
|
supports-color "^5.3.0"
|
||||||
|
|
||||||
|
chalk@^2.1.0, chalk@^2.3.1:
|
||||||
version "2.3.2"
|
version "2.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1646,7 +1661,7 @@ content-type@~1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||||
|
|
||||||
convert-source-map@^1.5.0:
|
convert-source-map@^1.5.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
|
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
|
||||||
|
|
||||||
|
@ -1671,8 +1686,8 @@ core-js@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
||||||
|
|
||||||
core-js@^2.4.0, core-js@^2.5.0:
|
core-js@^2.4.0, core-js@^2.5.0:
|
||||||
version "2.5.3"
|
version "2.5.7"
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
|
||||||
|
|
||||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
@ -1914,7 +1929,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@^3.0.1, debug@^3.1.0:
|
debug@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2916,10 +2931,14 @@ global@~4.3.0:
|
||||||
min-document "^2.19.0"
|
min-document "^2.19.0"
|
||||||
process "~0.5.1"
|
process "~0.5.1"
|
||||||
|
|
||||||
globals@^11.0.1, globals@^11.1.0:
|
globals@^11.0.1:
|
||||||
version "11.3.0"
|
version "11.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0"
|
resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0"
|
||||||
|
|
||||||
|
globals@^11.1.0:
|
||||||
|
version "11.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/globals/-/globals-11.5.0.tgz#6bc840de6771173b191f13d3a9c94d441ee92642"
|
||||||
|
|
||||||
globals@^9.18.0:
|
globals@^9.18.0:
|
||||||
version "9.18.0"
|
version "9.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
|
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
|
||||||
|
@ -3176,10 +3195,16 @@ hyphenate-style-name@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b"
|
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b"
|
||||||
|
|
||||||
iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
|
iconv-lite@0.4.19, iconv-lite@^0.4.17:
|
||||||
version "0.4.19"
|
version "0.4.19"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
|
||||||
|
|
||||||
|
iconv-lite@~0.4.13:
|
||||||
|
version "0.4.23"
|
||||||
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
|
||||||
|
dependencies:
|
||||||
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
icss-replace-symbols@^1.1.0:
|
icss-replace-symbols@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
||||||
|
@ -3272,8 +3297,8 @@ interpret@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
|
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
|
||||||
|
|
||||||
invariant@^2.2.0, invariant@^2.2.2:
|
invariant@^2.2.0, invariant@^2.2.2:
|
||||||
version "2.2.3"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688"
|
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.0.0"
|
loose-envify "^1.0.0"
|
||||||
|
|
||||||
|
@ -3863,10 +3888,14 @@ lodash.uniq@^4.5.0:
|
||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
|
|
||||||
lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.4:
|
lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.3.0, lodash@~4.17.4:
|
||||||
version "4.17.5"
|
version "4.17.5"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
|
||||||
|
|
||||||
|
lodash@^4.17.4, lodash@^4.2.0:
|
||||||
|
version "4.17.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
|
||||||
|
|
||||||
loglevel@^1.4.1:
|
loglevel@^1.4.1:
|
||||||
version "1.6.1"
|
version "1.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
|
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
|
||||||
|
@ -3904,8 +3933,8 @@ macaddress@^0.2.8:
|
||||||
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
|
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
|
||||||
|
|
||||||
make-dir@^1.0.0:
|
make-dir@^1.0.0:
|
||||||
version "1.2.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
|
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
|
||||||
dependencies:
|
dependencies:
|
||||||
pify "^3.0.0"
|
pify "^3.0.0"
|
||||||
|
|
||||||
|
@ -4895,7 +4924,7 @@ preserve@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
||||||
|
|
||||||
private@^0.1.6, private@^0.1.7:
|
private@^0.1.6, private@^0.1.8:
|
||||||
version "0.1.8"
|
version "0.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
|
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
|
||||||
|
|
||||||
|
@ -5061,8 +5090,8 @@ rc@^1.1.7:
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
react-dom@^16.2.0:
|
react-dom@^16.2.0:
|
||||||
version "16.2.0"
|
version "16.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e"
|
||||||
dependencies:
|
dependencies:
|
||||||
fbjs "^0.8.16"
|
fbjs "^0.8.16"
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
|
@ -5138,8 +5167,8 @@ react-transition-group@^2.2.1:
|
||||||
warning "^3.0.0"
|
warning "^3.0.0"
|
||||||
|
|
||||||
react@^16.2.0:
|
react@^16.2.0:
|
||||||
version "16.2.0"
|
version "16.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585"
|
||||||
dependencies:
|
dependencies:
|
||||||
fbjs "^0.8.16"
|
fbjs "^0.8.16"
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
|
@ -5471,6 +5500,10 @@ safe-regex@^1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ret "~0.1.10"
|
ret "~0.1.10"
|
||||||
|
|
||||||
|
"safer-buffer@>= 2.1.2 < 3":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
|
|
||||||
sax@~1.2.1:
|
sax@~1.2.1:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
|
@ -5914,12 +5947,18 @@ supports-color@^4.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag "^2.0.0"
|
has-flag "^2.0.0"
|
||||||
|
|
||||||
supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0:
|
supports-color@^5.1.0, supports-color@^5.2.0:
|
||||||
version "5.3.0"
|
version "5.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0"
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag "^3.0.0"
|
has-flag "^3.0.0"
|
||||||
|
|
||||||
|
supports-color@^5.3.0:
|
||||||
|
version "5.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
|
||||||
|
dependencies:
|
||||||
|
has-flag "^3.0.0"
|
||||||
|
|
||||||
svgo@^0.7.0:
|
svgo@^0.7.0:
|
||||||
version "0.7.2"
|
version "0.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
|
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
|
||||||
|
@ -6108,8 +6147,8 @@ typedarray@^0.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
|
|
||||||
ua-parser-js@^0.7.9:
|
ua-parser-js@^0.7.9:
|
||||||
version "0.7.17"
|
version "0.7.18"
|
||||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
|
||||||
|
|
||||||
uglify-js@^2.8.29:
|
uglify-js@^2.8.29:
|
||||||
version "2.8.29"
|
version "2.8.29"
|
||||||
|
@ -6395,8 +6434,8 @@ websocket-extensions@>=0.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
|
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
|
||||||
|
|
||||||
whatwg-fetch@>=0.10.0:
|
whatwg-fetch@>=0.10.0:
|
||||||
version "2.0.3"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
|
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
|
||||||
|
|
||||||
whet.extend@~0.9.9:
|
whet.extend@~0.9.9:
|
||||||
version "0.9.9"
|
version "0.9.9"
|
||||||
|
|
|
@ -32,12 +32,15 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"io"
|
||||||
|
|
||||||
"github.com/elastic/gosigar"
|
"github.com/elastic/gosigar"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
"github.com/mohae/deepcopy"
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -60,10 +63,11 @@ type Dashboard struct {
|
||||||
|
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
conns map[uint32]*client // Currently live websocket connections
|
conns map[uint32]*client // Currently live websocket connections
|
||||||
charts *SystemMessage
|
history *Message
|
||||||
commit string
|
|
||||||
lock sync.RWMutex // Lock protecting the dashboard's internals
|
lock sync.RWMutex // Lock protecting the dashboard's internals
|
||||||
|
|
||||||
|
logdir string
|
||||||
|
|
||||||
quit chan chan error // Channel used for graceful exit
|
quit chan chan error // Channel used for graceful exit
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
@ -71,30 +75,39 @@ type Dashboard struct {
|
||||||
// client represents active websocket connection with a remote browser.
|
// client represents active websocket connection with a remote browser.
|
||||||
type client struct {
|
type client struct {
|
||||||
conn *websocket.Conn // Particular live websocket connection
|
conn *websocket.Conn // Particular live websocket connection
|
||||||
msg chan Message // Message queue for the update messages
|
msg chan *Message // Message queue for the update messages
|
||||||
logger log.Logger // Logger for the particular live websocket connection
|
logger log.Logger // Logger for the particular live websocket connection
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new dashboard instance with the given configuration.
|
// New creates a new dashboard instance with the given configuration.
|
||||||
func New(config *Config, commit string) (*Dashboard, error) {
|
func New(config *Config, commit string, logdir string) *Dashboard {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
db := &Dashboard{
|
versionMeta := ""
|
||||||
|
if len(params.VersionMeta) > 0 {
|
||||||
|
versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta)
|
||||||
|
}
|
||||||
|
return &Dashboard{
|
||||||
conns: make(map[uint32]*client),
|
conns: make(map[uint32]*client),
|
||||||
config: config,
|
config: config,
|
||||||
quit: make(chan chan error),
|
quit: make(chan chan error),
|
||||||
charts: &SystemMessage{
|
history: &Message{
|
||||||
ActiveMemory: emptyChartEntries(now, activeMemorySampleLimit, config.Refresh),
|
General: &GeneralMessage{
|
||||||
VirtualMemory: emptyChartEntries(now, virtualMemorySampleLimit, config.Refresh),
|
Commit: commit,
|
||||||
NetworkIngress: emptyChartEntries(now, networkIngressSampleLimit, config.Refresh),
|
Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
|
||||||
NetworkEgress: emptyChartEntries(now, networkEgressSampleLimit, config.Refresh),
|
},
|
||||||
ProcessCPU: emptyChartEntries(now, processCPUSampleLimit, config.Refresh),
|
System: &SystemMessage{
|
||||||
SystemCPU: emptyChartEntries(now, systemCPUSampleLimit, config.Refresh),
|
ActiveMemory: emptyChartEntries(now, activeMemorySampleLimit, config.Refresh),
|
||||||
DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh),
|
VirtualMemory: emptyChartEntries(now, virtualMemorySampleLimit, config.Refresh),
|
||||||
DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh),
|
NetworkIngress: emptyChartEntries(now, networkIngressSampleLimit, config.Refresh),
|
||||||
|
NetworkEgress: emptyChartEntries(now, networkEgressSampleLimit, config.Refresh),
|
||||||
|
ProcessCPU: emptyChartEntries(now, processCPUSampleLimit, config.Refresh),
|
||||||
|
SystemCPU: emptyChartEntries(now, systemCPUSampleLimit, config.Refresh),
|
||||||
|
DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh),
|
||||||
|
DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
commit: commit,
|
logdir: logdir,
|
||||||
}
|
}
|
||||||
return db, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// emptyChartEntries returns a ChartEntry array containing limit number of empty samples.
|
// emptyChartEntries returns a ChartEntry array containing limit number of empty samples.
|
||||||
|
@ -108,19 +121,20 @@ func emptyChartEntries(t time.Time, limit int, refresh time.Duration) ChartEntri
|
||||||
return ce
|
return ce
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protocols is a meaningless implementation of node.Service.
|
// Protocols implements the node.Service interface.
|
||||||
func (db *Dashboard) Protocols() []p2p.Protocol { return nil }
|
func (db *Dashboard) Protocols() []p2p.Protocol { return nil }
|
||||||
|
|
||||||
// APIs is a meaningless implementation of node.Service.
|
// APIs implements the node.Service interface.
|
||||||
func (db *Dashboard) APIs() []rpc.API { return nil }
|
func (db *Dashboard) APIs() []rpc.API { return nil }
|
||||||
|
|
||||||
// Start implements node.Service, starting the data collection thread and the listening server of the dashboard.
|
// 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 {
|
func (db *Dashboard) Start(server *p2p.Server) error {
|
||||||
log.Info("Starting dashboard")
|
log.Info("Starting dashboard")
|
||||||
|
|
||||||
db.wg.Add(2)
|
db.wg.Add(2)
|
||||||
go db.collectData()
|
go db.collectData()
|
||||||
go db.collectLogs() // In case of removing this line change 2 back to 1 in wg.Add.
|
go db.streamLogs()
|
||||||
|
|
||||||
http.HandleFunc("/", db.webHandler)
|
http.HandleFunc("/", db.webHandler)
|
||||||
http.Handle("/api", websocket.Handler(db.apiHandler))
|
http.Handle("/api", websocket.Handler(db.apiHandler))
|
||||||
|
@ -136,7 +150,8 @@ func (db *Dashboard) Start(server *p2p.Server) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop implements node.Service, stopping the data collection thread and the connection listener of the dashboard.
|
// Stop stops the data collection thread and the connection listener of the dashboard.
|
||||||
|
// Implements the node.Service interface.
|
||||||
func (db *Dashboard) Stop() error {
|
func (db *Dashboard) Stop() error {
|
||||||
// Close the connection listener.
|
// Close the connection listener.
|
||||||
var errs []error
|
var errs []error
|
||||||
|
@ -194,7 +209,7 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
id := atomic.AddUint32(&nextID, 1)
|
id := atomic.AddUint32(&nextID, 1)
|
||||||
client := &client{
|
client := &client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
msg: make(chan Message, 128),
|
msg: make(chan *Message, 128),
|
||||||
logger: log.New("id", id),
|
logger: log.New("id", id),
|
||||||
}
|
}
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
@ -218,29 +233,10 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
versionMeta := ""
|
|
||||||
if len(params.VersionMeta) > 0 {
|
|
||||||
versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta)
|
|
||||||
}
|
|
||||||
// Send the past data.
|
|
||||||
client.msg <- Message{
|
|
||||||
General: &GeneralMessage{
|
|
||||||
Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
|
|
||||||
Commit: db.commit,
|
|
||||||
},
|
|
||||||
System: &SystemMessage{
|
|
||||||
ActiveMemory: db.charts.ActiveMemory,
|
|
||||||
VirtualMemory: db.charts.VirtualMemory,
|
|
||||||
NetworkIngress: db.charts.NetworkIngress,
|
|
||||||
NetworkEgress: db.charts.NetworkEgress,
|
|
||||||
ProcessCPU: db.charts.ProcessCPU,
|
|
||||||
SystemCPU: db.charts.SystemCPU,
|
|
||||||
DiskRead: db.charts.DiskRead,
|
|
||||||
DiskWrite: db.charts.DiskWrite,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Start tracking the connection and drop at connection loss.
|
|
||||||
db.lock.Lock()
|
db.lock.Lock()
|
||||||
|
// Send the past data.
|
||||||
|
client.msg <- deepcopy.Copy(db.history).(*Message)
|
||||||
|
// Start tracking the connection and drop at connection loss.
|
||||||
db.conns[id] = client
|
db.conns[id] = client
|
||||||
db.lock.Unlock()
|
db.lock.Unlock()
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -249,29 +245,53 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
db.lock.Unlock()
|
db.lock.Unlock()
|
||||||
}()
|
}()
|
||||||
for {
|
for {
|
||||||
fail := []byte{}
|
r := new(Request)
|
||||||
if _, err := conn.Read(fail); err != nil {
|
if err := websocket.JSON.Receive(conn, r); err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
client.logger.Warn("Failed to receive request", "err", err)
|
||||||
|
}
|
||||||
close(done)
|
close(done)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Ignore all messages
|
if r.Logs != nil {
|
||||||
|
db.handleLogRequest(r.Logs, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// meterCollector returns a function, which retrieves a specific meter.
|
||||||
|
func meterCollector(name string) func() int64 {
|
||||||
|
if metric := metrics.DefaultRegistry.Get(name); metric != nil {
|
||||||
|
m := metric.(metrics.Meter)
|
||||||
|
return func() int64 {
|
||||||
|
return m.Count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return func() int64 {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectData collects the required data to plot on the dashboard.
|
// collectData collects the required data to plot on the dashboard.
|
||||||
func (db *Dashboard) collectData() {
|
func (db *Dashboard) collectData() {
|
||||||
defer db.wg.Done()
|
defer db.wg.Done()
|
||||||
|
|
||||||
systemCPUUsage := gosigar.Cpu{}
|
systemCPUUsage := gosigar.Cpu{}
|
||||||
systemCPUUsage.Get()
|
systemCPUUsage.Get()
|
||||||
var (
|
var (
|
||||||
mem runtime.MemStats
|
mem runtime.MemStats
|
||||||
|
|
||||||
prevNetworkIngress = metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count()
|
collectNetworkIngress = meterCollector("p2p/InboundTraffic")
|
||||||
prevNetworkEgress = metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count()
|
collectNetworkEgress = meterCollector("p2p/OutboundTraffic")
|
||||||
|
collectDiskRead = meterCollector("eth/db/chaindata/disk/read")
|
||||||
|
collectDiskWrite = meterCollector("eth/db/chaindata/disk/write")
|
||||||
|
|
||||||
|
prevNetworkIngress = collectNetworkIngress()
|
||||||
|
prevNetworkEgress = collectNetworkEgress()
|
||||||
prevProcessCPUTime = getProcessCPUTime()
|
prevProcessCPUTime = getProcessCPUTime()
|
||||||
prevSystemCPUUsage = systemCPUUsage
|
prevSystemCPUUsage = systemCPUUsage
|
||||||
prevDiskRead = metrics.DefaultRegistry.Get("eth/db/chaindata/disk/read").(metrics.Meter).Count()
|
prevDiskRead = collectDiskRead()
|
||||||
prevDiskWrite = metrics.DefaultRegistry.Get("eth/db/chaindata/disk/write").(metrics.Meter).Count()
|
prevDiskWrite = collectDiskWrite()
|
||||||
|
|
||||||
frequency = float64(db.config.Refresh / time.Second)
|
frequency = float64(db.config.Refresh / time.Second)
|
||||||
numCPU = float64(runtime.NumCPU())
|
numCPU = float64(runtime.NumCPU())
|
||||||
|
@ -285,12 +305,12 @@ func (db *Dashboard) collectData() {
|
||||||
case <-time.After(db.config.Refresh):
|
case <-time.After(db.config.Refresh):
|
||||||
systemCPUUsage.Get()
|
systemCPUUsage.Get()
|
||||||
var (
|
var (
|
||||||
curNetworkIngress = metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count()
|
curNetworkIngress = collectNetworkIngress()
|
||||||
curNetworkEgress = metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count()
|
curNetworkEgress = collectNetworkEgress()
|
||||||
curProcessCPUTime = getProcessCPUTime()
|
curProcessCPUTime = getProcessCPUTime()
|
||||||
curSystemCPUUsage = systemCPUUsage
|
curSystemCPUUsage = systemCPUUsage
|
||||||
curDiskRead = metrics.DefaultRegistry.Get("eth/db/chaindata/disk/read").(metrics.Meter).Count()
|
curDiskRead = collectDiskRead()
|
||||||
curDiskWrite = metrics.DefaultRegistry.Get("eth/db/chaindata/disk/write").(metrics.Meter).Count()
|
curDiskWrite = collectDiskWrite()
|
||||||
|
|
||||||
deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress)
|
deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress)
|
||||||
deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress)
|
deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress)
|
||||||
|
@ -341,14 +361,17 @@ func (db *Dashboard) collectData() {
|
||||||
Time: now,
|
Time: now,
|
||||||
Value: float64(deltaDiskWrite) / frequency,
|
Value: float64(deltaDiskWrite) / frequency,
|
||||||
}
|
}
|
||||||
db.charts.ActiveMemory = append(db.charts.ActiveMemory[1:], activeMemory)
|
sys := db.history.System
|
||||||
db.charts.VirtualMemory = append(db.charts.VirtualMemory[1:], virtualMemory)
|
db.lock.Lock()
|
||||||
db.charts.NetworkIngress = append(db.charts.NetworkIngress[1:], networkIngress)
|
sys.ActiveMemory = append(sys.ActiveMemory[1:], activeMemory)
|
||||||
db.charts.NetworkEgress = append(db.charts.NetworkEgress[1:], networkEgress)
|
sys.VirtualMemory = append(sys.VirtualMemory[1:], virtualMemory)
|
||||||
db.charts.ProcessCPU = append(db.charts.ProcessCPU[1:], processCPU)
|
sys.NetworkIngress = append(sys.NetworkIngress[1:], networkIngress)
|
||||||
db.charts.SystemCPU = append(db.charts.SystemCPU[1:], systemCPU)
|
sys.NetworkEgress = append(sys.NetworkEgress[1:], networkEgress)
|
||||||
db.charts.DiskRead = append(db.charts.DiskRead[1:], diskRead)
|
sys.ProcessCPU = append(sys.ProcessCPU[1:], processCPU)
|
||||||
db.charts.DiskWrite = append(db.charts.DiskRead[1:], diskWrite)
|
sys.SystemCPU = append(sys.SystemCPU[1:], systemCPU)
|
||||||
|
sys.DiskRead = append(sys.DiskRead[1:], diskRead)
|
||||||
|
sys.DiskWrite = append(sys.DiskRead[1:], diskWrite)
|
||||||
|
db.lock.Unlock()
|
||||||
|
|
||||||
db.sendToAll(&Message{
|
db.sendToAll(&Message{
|
||||||
System: &SystemMessage{
|
System: &SystemMessage{
|
||||||
|
@ -366,34 +389,12 @@ func (db *Dashboard) collectData() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectLogs collects and sends the logs to the active dashboards.
|
|
||||||
func (db *Dashboard) collectLogs() {
|
|
||||||
defer db.wg.Done()
|
|
||||||
|
|
||||||
id := 1
|
|
||||||
// TODO (kurkomisi): log collection comes here.
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case errc := <-db.quit:
|
|
||||||
errc <- nil
|
|
||||||
return
|
|
||||||
case <-time.After(db.config.Refresh / 2):
|
|
||||||
db.sendToAll(&Message{
|
|
||||||
Logs: &LogsMessage{
|
|
||||||
Log: []string{fmt.Sprintf("%-4d: This is a fake log.", id)},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
id++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendToAll sends the given message to the active dashboards.
|
// sendToAll sends the given message to the active dashboards.
|
||||||
func (db *Dashboard) sendToAll(msg *Message) {
|
func (db *Dashboard) sendToAll(msg *Message) {
|
||||||
db.lock.Lock()
|
db.lock.Lock()
|
||||||
for _, c := range db.conns {
|
for _, c := range db.conns {
|
||||||
select {
|
select {
|
||||||
case c.msg <- *msg:
|
case c.msg <- msg:
|
||||||
default:
|
default:
|
||||||
c.conn.Close()
|
c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,288 @@
|
||||||
|
// 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.lock.Lock()
|
||||||
|
if db.history.Logs != nil {
|
||||||
|
c.msg <- &Message{
|
||||||
|
Logs: db.history.Logs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.lock.Unlock()
|
||||||
|
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.lock.Lock()
|
||||||
|
db.history.Logs = &LogsMessage{
|
||||||
|
Source: &LogFile{
|
||||||
|
Name: fi.Name(),
|
||||||
|
Last: true,
|
||||||
|
},
|
||||||
|
Chunk: emptyChunk,
|
||||||
|
}
|
||||||
|
db.lock.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.lock.Lock()
|
||||||
|
db.history.Logs.Source.Name = fi.Name()
|
||||||
|
db.history.Logs.Chunk = emptyChunk
|
||||||
|
db.lock.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.lock.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.lock.Unlock()
|
||||||
|
|
||||||
|
db.sendToAll(&Message{Logs: l})
|
||||||
|
case errc = <-db.quit:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,10 @@
|
||||||
|
|
||||||
package dashboard
|
package dashboard
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
General *GeneralMessage `json:"general,omitempty"`
|
General *GeneralMessage `json:"general,omitempty"`
|
||||||
|
@ -67,6 +70,24 @@ type SystemMessage struct {
|
||||||
DiskWrite ChartEntries `json:"diskWrite,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 {
|
type LogsMessage struct {
|
||||||
Log []string `json:"log,omitempty"`
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,10 @@ var Flags = []cli.Flag{
|
||||||
memprofilerateFlag, blockprofilerateFlag, cpuprofileFlag, traceFlag,
|
memprofilerateFlag, blockprofilerateFlag, cpuprofileFlag, traceFlag,
|
||||||
}
|
}
|
||||||
|
|
||||||
var glogger *log.GlogHandler
|
var (
|
||||||
|
ostream log.Handler
|
||||||
|
glogger *log.GlogHandler
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
usecolor := term.IsTty(os.Stderr.Fd()) && os.Getenv("TERM") != "dumb"
|
usecolor := term.IsTty(os.Stderr.Fd()) && os.Getenv("TERM") != "dumb"
|
||||||
|
@ -103,14 +106,26 @@ func init() {
|
||||||
if usecolor {
|
if usecolor {
|
||||||
output = colorable.NewColorableStderr()
|
output = colorable.NewColorableStderr()
|
||||||
}
|
}
|
||||||
glogger = log.NewGlogHandler(log.StreamHandler(output, log.TerminalFormat(usecolor)))
|
ostream = log.StreamHandler(output, log.TerminalFormat(usecolor))
|
||||||
|
glogger = log.NewGlogHandler(ostream)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup initializes profiling and logging based on the CLI flags.
|
// Setup initializes profiling and logging based on the CLI flags.
|
||||||
// It should be called as early as possible in the program.
|
// It should be called as early as possible in the program.
|
||||||
func Setup(ctx *cli.Context) error {
|
func Setup(ctx *cli.Context, logdir string) error {
|
||||||
// logging
|
// logging
|
||||||
log.PrintOrigins(ctx.GlobalBool(debugFlag.Name))
|
log.PrintOrigins(ctx.GlobalBool(debugFlag.Name))
|
||||||
|
if logdir != "" {
|
||||||
|
rfh, err := log.RotatingFileHandler(
|
||||||
|
logdir,
|
||||||
|
262144,
|
||||||
|
log.JSONFormatOrderedEx(false, true),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
glogger.SetHandler(log.MultiHandler(ostream, rfh))
|
||||||
|
}
|
||||||
glogger.Verbosity(log.Lvl(ctx.GlobalInt(verbosityFlag.Name)))
|
glogger.Verbosity(log.Lvl(ctx.GlobalInt(verbosityFlag.Name)))
|
||||||
glogger.Vmodule(ctx.GlobalString(vmoduleFlag.Name))
|
glogger.Vmodule(ctx.GlobalString(vmoduleFlag.Name))
|
||||||
glogger.BacktraceAt(ctx.GlobalString(backtraceAtFlag.Name))
|
glogger.BacktraceAt(ctx.GlobalString(backtraceAtFlag.Name))
|
||||||
|
|
|
@ -77,11 +77,11 @@ type TerminalStringer interface {
|
||||||
// a terminal with color-coded level output and terser human friendly timestamp.
|
// a terminal with color-coded level output and terser human friendly timestamp.
|
||||||
// This format should only be used for interactive programs or while developing.
|
// This format should only be used for interactive programs or while developing.
|
||||||
//
|
//
|
||||||
// [TIME] [LEVEL] MESAGE key=value key=value ...
|
// [LEVEL] [TIME] MESAGE key=value key=value ...
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// [May 16 20:58:45] [DBUG] remove route ns=haproxy addr=127.0.0.1:50002
|
// [DBUG] [May 16 20:58:45] remove route ns=haproxy addr=127.0.0.1:50002
|
||||||
//
|
//
|
||||||
func TerminalFormat(usecolor bool) Format {
|
func TerminalFormat(usecolor bool) Format {
|
||||||
return FormatFunc(func(r *Record) []byte {
|
return FormatFunc(func(r *Record) []byte {
|
||||||
|
@ -202,6 +202,48 @@ func JSONFormat() Format {
|
||||||
return JSONFormatEx(false, true)
|
return JSONFormatEx(false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSONFormatOrderedEx formats log records as JSON arrays. If pretty is true,
|
||||||
|
// records will be pretty-printed. If lineSeparated is true, records
|
||||||
|
// will be logged with a new line between each record.
|
||||||
|
func JSONFormatOrderedEx(pretty, lineSeparated bool) Format {
|
||||||
|
jsonMarshal := json.Marshal
|
||||||
|
if pretty {
|
||||||
|
jsonMarshal = func(v interface{}) ([]byte, error) {
|
||||||
|
return json.MarshalIndent(v, "", " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FormatFunc(func(r *Record) []byte {
|
||||||
|
props := make(map[string]interface{})
|
||||||
|
|
||||||
|
props[r.KeyNames.Time] = r.Time
|
||||||
|
props[r.KeyNames.Lvl] = r.Lvl.String()
|
||||||
|
props[r.KeyNames.Msg] = r.Msg
|
||||||
|
|
||||||
|
ctx := make([]string, len(r.Ctx))
|
||||||
|
for i := 0; i < len(r.Ctx); i += 2 {
|
||||||
|
k, ok := r.Ctx[i].(string)
|
||||||
|
if !ok {
|
||||||
|
props[errorKey] = fmt.Sprintf("%+v is not a string key,", r.Ctx[i])
|
||||||
|
}
|
||||||
|
ctx[i] = k
|
||||||
|
ctx[i+1] = formatLogfmtValue(r.Ctx[i+1], true)
|
||||||
|
}
|
||||||
|
props[r.KeyNames.Ctx] = ctx
|
||||||
|
|
||||||
|
b, err := jsonMarshal(props)
|
||||||
|
if err != nil {
|
||||||
|
b, _ = jsonMarshal(map[string]string{
|
||||||
|
errorKey: err.Error(),
|
||||||
|
})
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
if lineSeparated {
|
||||||
|
b = append(b, '\n')
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// JSONFormatEx formats log records as JSON objects. If pretty is true,
|
// JSONFormatEx formats log records as JSON objects. If pretty is true,
|
||||||
// records will be pretty-printed. If lineSeparated is true, records
|
// records will be pretty-printed. If lineSeparated is true, records
|
||||||
// will be logged with a new line between each record.
|
// will be logged with a new line between each record.
|
||||||
|
|
110
log/handler.go
110
log/handler.go
|
@ -8,6 +8,11 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-stack/stack"
|
"github.com/go-stack/stack"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,6 +75,111 @@ func FileHandler(path string, fmtr Format) (Handler, error) {
|
||||||
return closingHandler{f, StreamHandler(f, fmtr)}, nil
|
return closingHandler{f, StreamHandler(f, fmtr)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// countingWriter wraps a WriteCloser object in order to count the written bytes.
|
||||||
|
type countingWriter struct {
|
||||||
|
w io.WriteCloser // the wrapped object
|
||||||
|
count uint // number of bytes written
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write increments the byte counter by the number of bytes written.
|
||||||
|
// Implements the WriteCloser interface.
|
||||||
|
func (w *countingWriter) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = w.w.Write(p)
|
||||||
|
w.count += uint(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the WriteCloser interface.
|
||||||
|
func (w *countingWriter) Close() error {
|
||||||
|
return w.w.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepFile opens the log file at the given path, and cuts off the invalid part
|
||||||
|
// from the end, because the previous execution could have been finished by interruption.
|
||||||
|
// Assumes that every line ended by '\n' contains a valid log record.
|
||||||
|
func prepFile(path string) (*countingWriter, error) {
|
||||||
|
f, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = f.Seek(-1, io.SeekEnd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
var cut int64
|
||||||
|
for {
|
||||||
|
if _, err := f.Read(buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if buf[0] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, err = f.Seek(-2, io.SeekCurrent); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cut++
|
||||||
|
}
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ns := fi.Size() - cut
|
||||||
|
if err = f.Truncate(ns); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &countingWriter{w: f, count: uint(ns)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotatingFileHandler returns a handler which writes log records to file chunks
|
||||||
|
// at the given path. When a file's size reaches the limit, the handler creates
|
||||||
|
// a new file named after the timestamp of the first log record it will contain.
|
||||||
|
func RotatingFileHandler(path string, limit uint, formatter Format) (Handler, error) {
|
||||||
|
if err := os.MkdirAll(path, 0700); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
re := regexp.MustCompile(`\.log$`)
|
||||||
|
last := len(files) - 1
|
||||||
|
for last >= 0 && (!files[last].Mode().IsRegular() || !re.MatchString(files[last].Name())) {
|
||||||
|
last--
|
||||||
|
}
|
||||||
|
var counter *countingWriter
|
||||||
|
if last >= 0 && files[last].Size() < int64(limit) {
|
||||||
|
// Open the last file, and continue to write into it until it's size reaches the limit.
|
||||||
|
if counter, err = prepFile(filepath.Join(path, files[last].Name())); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if counter == nil {
|
||||||
|
counter = new(countingWriter)
|
||||||
|
}
|
||||||
|
h := StreamHandler(counter, formatter)
|
||||||
|
|
||||||
|
return FuncHandler(func(r *Record) error {
|
||||||
|
if counter.count > limit {
|
||||||
|
counter.Close()
|
||||||
|
counter.w = nil
|
||||||
|
}
|
||||||
|
if counter.w == nil {
|
||||||
|
f, err := os.OpenFile(
|
||||||
|
filepath.Join(path, fmt.Sprintf("%s.log", strings.Replace(r.Time.Format("060102150405.00"), ".", "", 1))),
|
||||||
|
os.O_CREATE|os.O_APPEND|os.O_WRONLY,
|
||||||
|
0600,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
counter.w = f
|
||||||
|
counter.count = 0
|
||||||
|
}
|
||||||
|
return h.Log(r)
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
// NetHandler opens a socket to the given address and writes records
|
// NetHandler opens a socket to the given address and writes records
|
||||||
// over the connection.
|
// over the connection.
|
||||||
func NetHandler(network, addr string, fmtr Format) (Handler, error) {
|
func NetHandler(network, addr string, fmtr Format) (Handler, error) {
|
||||||
|
|
|
@ -57,6 +57,11 @@ func NewGlogHandler(h Handler) *GlogHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetHandler updates the handler to write records to the specified sub-handler.
|
||||||
|
func (h *GlogHandler) SetHandler(nh Handler) {
|
||||||
|
h.origin = nh
|
||||||
|
}
|
||||||
|
|
||||||
// pattern contains a filter for the Vmodule option, holding a verbosity level
|
// pattern contains a filter for the Vmodule option, holding a verbosity level
|
||||||
// and a file pattern to match.
|
// and a file pattern to match.
|
||||||
type pattern struct {
|
type pattern struct {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
const timeKey = "t"
|
const timeKey = "t"
|
||||||
const lvlKey = "lvl"
|
const lvlKey = "lvl"
|
||||||
const msgKey = "msg"
|
const msgKey = "msg"
|
||||||
|
const ctxKey = "ctx"
|
||||||
const errorKey = "LOG15_ERROR"
|
const errorKey = "LOG15_ERROR"
|
||||||
const skipLevel = 2
|
const skipLevel = 2
|
||||||
|
|
||||||
|
@ -101,6 +102,7 @@ type RecordKeyNames struct {
|
||||||
Time string
|
Time string
|
||||||
Msg string
|
Msg string
|
||||||
Lvl string
|
Lvl string
|
||||||
|
Ctx string
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Logger writes key/value pairs to a Handler
|
// A Logger writes key/value pairs to a Handler
|
||||||
|
@ -139,6 +141,7 @@ func (l *logger) write(msg string, lvl Lvl, ctx []interface{}, skip int) {
|
||||||
Time: timeKey,
|
Time: timeKey,
|
||||||
Msg: msgKey,
|
Msg: msgKey,
|
||||||
Lvl: lvlKey,
|
Lvl: lvlKey,
|
||||||
|
Ctx: ctxKey,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,7 +179,7 @@ func (c *Config) NodeDB() string {
|
||||||
if c.DataDir == "" {
|
if c.DataDir == "" {
|
||||||
return "" // ephemeral
|
return "" // ephemeral
|
||||||
}
|
}
|
||||||
return c.resolvePath(datadirNodeDatabase)
|
return c.ResolvePath(datadirNodeDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultIPCEndpoint returns the IPC path used by default.
|
// DefaultIPCEndpoint returns the IPC path used by default.
|
||||||
|
@ -262,8 +262,8 @@ var isOldGethResource = map[string]bool{
|
||||||
"trusted-nodes.json": true,
|
"trusted-nodes.json": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvePath resolves path in the instance directory.
|
// ResolvePath resolves path in the instance directory.
|
||||||
func (c *Config) resolvePath(path string) string {
|
func (c *Config) ResolvePath(path string) string {
|
||||||
if filepath.IsAbs(path) {
|
if filepath.IsAbs(path) {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
@ -309,7 +309,7 @@ func (c *Config) NodeKey() *ecdsa.PrivateKey {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
keyfile := c.resolvePath(datadirPrivateKey)
|
keyfile := c.ResolvePath(datadirPrivateKey)
|
||||||
if key, err := crypto.LoadECDSA(keyfile); err == nil {
|
if key, err := crypto.LoadECDSA(keyfile); err == nil {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
@ -332,12 +332,12 @@ func (c *Config) NodeKey() *ecdsa.PrivateKey {
|
||||||
|
|
||||||
// StaticNodes returns a list of node enode URLs configured as static nodes.
|
// StaticNodes returns a list of node enode URLs configured as static nodes.
|
||||||
func (c *Config) StaticNodes() []*discover.Node {
|
func (c *Config) StaticNodes() []*discover.Node {
|
||||||
return c.parsePersistentNodes(c.resolvePath(datadirStaticNodes))
|
return c.parsePersistentNodes(c.ResolvePath(datadirStaticNodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrustedNodes returns a list of node enode URLs configured as trusted nodes.
|
// TrustedNodes returns a list of node enode URLs configured as trusted nodes.
|
||||||
func (c *Config) TrustedNodes() []*discover.Node {
|
func (c *Config) TrustedNodes() []*discover.Node {
|
||||||
return c.parsePersistentNodes(c.resolvePath(datadirTrustedNodes))
|
return c.parsePersistentNodes(c.ResolvePath(datadirTrustedNodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsePersistentNodes parses a list of discovery node URLs loaded from a .json
|
// parsePersistentNodes parses a list of discovery node URLs loaded from a .json
|
||||||
|
|
|
@ -570,12 +570,12 @@ func (n *Node) OpenDatabase(name string, cache, handles int) (ethdb.Database, er
|
||||||
if n.config.DataDir == "" {
|
if n.config.DataDir == "" {
|
||||||
return ethdb.NewMemDatabase(), nil
|
return ethdb.NewMemDatabase(), nil
|
||||||
}
|
}
|
||||||
return ethdb.NewLDBDatabase(n.config.resolvePath(name), cache, handles)
|
return ethdb.NewLDBDatabase(n.config.ResolvePath(name), cache, handles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvePath returns the absolute path of a resource in the instance directory.
|
// ResolvePath returns the absolute path of a resource in the instance directory.
|
||||||
func (n *Node) ResolvePath(x string) string {
|
func (n *Node) ResolvePath(x string) string {
|
||||||
return n.config.resolvePath(x)
|
return n.config.ResolvePath(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// apis returns the collection of RPC descriptors this node offers.
|
// apis returns the collection of RPC descriptors this node offers.
|
||||||
|
|
|
@ -43,7 +43,7 @@ func (ctx *ServiceContext) OpenDatabase(name string, cache int, handles int) (et
|
||||||
if ctx.config.DataDir == "" {
|
if ctx.config.DataDir == "" {
|
||||||
return ethdb.NewMemDatabase(), nil
|
return ethdb.NewMemDatabase(), nil
|
||||||
}
|
}
|
||||||
db, err := ethdb.NewLDBDatabase(ctx.config.resolvePath(name), cache, handles)
|
db, err := ethdb.NewLDBDatabase(ctx.config.ResolvePath(name), cache, handles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ func (ctx *ServiceContext) OpenDatabase(name string, cache int, handles int) (et
|
||||||
// and if the user actually uses persistent storage. It will return an empty string
|
// and if the user actually uses persistent storage. It will return an empty string
|
||||||
// for emphemeral storage and the user's own input for absolute paths.
|
// for emphemeral storage and the user's own input for absolute paths.
|
||||||
func (ctx *ServiceContext) ResolvePath(path string) string {
|
func (ctx *ServiceContext) ResolvePath(path string) string {
|
||||||
return ctx.config.resolvePath(path)
|
return ctx.config.ResolvePath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service retrieves a currently running service registered of a specific type.
|
// Service retrieves a currently running service registered of a specific type.
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Joel
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,8 @@
|
||||||
|
deepCopy
|
||||||
|
========
|
||||||
|
[![GoDoc](https://godoc.org/github.com/mohae/deepcopy?status.svg)](https://godoc.org/github.com/mohae/deepcopy)[![Build Status](https://travis-ci.org/mohae/deepcopy.png)](https://travis-ci.org/mohae/deepcopy)
|
||||||
|
|
||||||
|
DeepCopy makes deep copies of things: unexported field values are not copied.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
cpy := deepcopy.Copy(orig)
|
|
@ -0,0 +1,125 @@
|
||||||
|
// deepcopy makes deep copies of things. A standard copy will copy the
|
||||||
|
// pointers: deep copy copies the values pointed to. Unexported field
|
||||||
|
// values are not copied.
|
||||||
|
//
|
||||||
|
// Copyright (c)2014-2016, Joel Scoble (github.com/mohae), all rights reserved.
|
||||||
|
// License: MIT, for more details check the included LICENSE file.
|
||||||
|
package deepcopy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface for delegating copy process to type
|
||||||
|
type Interface interface {
|
||||||
|
DeepCopy() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iface is an alias to Copy; this exists for backwards compatibility reasons.
|
||||||
|
func Iface(iface interface{}) interface{} {
|
||||||
|
return Copy(iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy creates a deep copy of whatever is passed to it and returns the copy
|
||||||
|
// in an interface{}. The returned value will need to be asserted to the
|
||||||
|
// correct type.
|
||||||
|
func Copy(src interface{}) interface{} {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the interface a reflect.Value
|
||||||
|
original := reflect.ValueOf(src)
|
||||||
|
|
||||||
|
// Make a copy of the same type as the original.
|
||||||
|
cpy := reflect.New(original.Type()).Elem()
|
||||||
|
|
||||||
|
// Recursively copy the original.
|
||||||
|
copyRecursive(original, cpy)
|
||||||
|
|
||||||
|
// Return the copy as an interface.
|
||||||
|
return cpy.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyRecursive does the actual copying of the interface. It currently has
|
||||||
|
// limited support for what it can handle. Add as needed.
|
||||||
|
func copyRecursive(original, cpy reflect.Value) {
|
||||||
|
// check for implement deepcopy.Interface
|
||||||
|
if original.CanInterface() {
|
||||||
|
if copier, ok := original.Interface().(Interface); ok {
|
||||||
|
cpy.Set(reflect.ValueOf(copier.DeepCopy()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle according to original's Kind
|
||||||
|
switch original.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
// Get the actual value being pointed to.
|
||||||
|
originalValue := original.Elem()
|
||||||
|
|
||||||
|
// if it isn't valid, return.
|
||||||
|
if !originalValue.IsValid() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cpy.Set(reflect.New(originalValue.Type()))
|
||||||
|
copyRecursive(originalValue, cpy.Elem())
|
||||||
|
|
||||||
|
case reflect.Interface:
|
||||||
|
// If this is a nil, don't do anything
|
||||||
|
if original.IsNil() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Get the value for the interface, not the pointer.
|
||||||
|
originalValue := original.Elem()
|
||||||
|
|
||||||
|
// Get the value by calling Elem().
|
||||||
|
copyValue := reflect.New(originalValue.Type()).Elem()
|
||||||
|
copyRecursive(originalValue, copyValue)
|
||||||
|
cpy.Set(copyValue)
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
t, ok := original.Interface().(time.Time)
|
||||||
|
if ok {
|
||||||
|
cpy.Set(reflect.ValueOf(t))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Go through each field of the struct and copy it.
|
||||||
|
for i := 0; i < original.NumField(); i++ {
|
||||||
|
// The Type's StructField for a given field is checked to see if StructField.PkgPath
|
||||||
|
// is set to determine if the field is exported or not because CanSet() returns false
|
||||||
|
// for settable fields. I'm not sure why. -mohae
|
||||||
|
if original.Type().Field(i).PkgPath != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
copyRecursive(original.Field(i), cpy.Field(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if original.IsNil() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Make a new slice and copy each element.
|
||||||
|
cpy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
|
||||||
|
for i := 0; i < original.Len(); i++ {
|
||||||
|
copyRecursive(original.Index(i), cpy.Index(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
if original.IsNil() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cpy.Set(reflect.MakeMap(original.Type()))
|
||||||
|
for _, key := range original.MapKeys() {
|
||||||
|
originalValue := original.MapIndex(key)
|
||||||
|
copyValue := reflect.New(originalValue.Type()).Elem()
|
||||||
|
copyRecursive(originalValue, copyValue)
|
||||||
|
copyKey := Copy(key.Interface())
|
||||||
|
cpy.SetMapIndex(reflect.ValueOf(copyKey), copyValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
cpy.Set(original)
|
||||||
|
}
|
||||||
|
}
|
|
@ -291,6 +291,12 @@
|
||||||
"revision": "ad45545899c7b13c020ea92b2072220eefad42b8",
|
"revision": "ad45545899c7b13c020ea92b2072220eefad42b8",
|
||||||
"revisionTime": "2015-03-14T17:03:34Z"
|
"revisionTime": "2015-03-14T17:03:34Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "2jsbDTvwxafPp7FJjJ8IIFlTLjs=",
|
||||||
|
"path": "github.com/mohae/deepcopy",
|
||||||
|
"revision": "c48cc78d482608239f6c4c92a4abd87eb8761c90",
|
||||||
|
"revisionTime": "2017-09-29T03:49:55Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "FYM/8R2CqS6PSNAoKl6X5gNJ20A=",
|
"checksumSHA1": "FYM/8R2CqS6PSNAoKl6X5gNJ20A=",
|
||||||
"path": "github.com/naoina/toml",
|
"path": "github.com/naoina/toml",
|
||||||
|
|
Loading…
Reference in New Issue