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
pull/16734/head
Kurkó Mihály 6 years ago committed by Péter Szilágyi
parent 2eedbe799f
commit a9835c1816
  1. 7
      cmd/geth/main.go
  2. 2
      cmd/swarm/main.go
  3. 4
      cmd/utils/flags.go
  4. 17652
      dashboard/assets.go
  5. 2
      dashboard/assets/common.jsx
  6. 10
      dashboard/assets/components/Body.jsx
  7. 2
      dashboard/assets/components/CustomTooltip.jsx
  8. 45
      dashboard/assets/components/Dashboard.jsx
  9. 310
      dashboard/assets/components/Logs.jsx
  10. 54
      dashboard/assets/components/Main.jsx
  11. 3
      dashboard/assets/index.html
  12. 60
      dashboard/assets/types/content.jsx
  13. 195
      dashboard/assets/yarn.lock
  14. 171
      dashboard/dashboard.go
  15. 288
      dashboard/log.go
  16. 25
      dashboard/message.go
  17. 21
      internal/debug/flags.go
  18. 46
      log/format.go
  19. 110
      log/handler.go
  20. 5
      log/handler_glog.go
  21. 3
      log/logger.go
  22. 12
      node/config.go
  23. 4
      node/node.go
  24. 4
      node/service.go
  25. 21
      vendor/github.com/mohae/deepcopy/LICENSE
  26. 8
      vendor/github.com/mohae/deepcopy/README.md
  27. 125
      vendor/github.com/mohae/deepcopy/deepcopy.go
  28. 6
      vendor/vendor.json

@ -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
}) })
} }

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&nbsp;';
color = '#4c8f0f';
break;
case 'warn':
lvl = 'WARN&nbsp;';
color = '#b79a22';
break;
case 'error':
case 'eror':
lvl = 'ERROR';
color = '#754b70';
break;
case 'crit':
lvl = 'CRIT&nbsp;';
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 += '&nbsp;'.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 = '&nbsp;'.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 := "" db.lock.Lock()
if len(params.VersionMeta) > 0 {
versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta)
}
// Send the past data. // Send the past data.
client.msg <- Message{ client.msg <- deepcopy.Copy(db.history).(*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. // Start tracking the connection and drop at connection loss.
db.lock.Lock()
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.

@ -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…
Cancel
Save