mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 02:12:23 +00:00
dashboard, vendor: first fully working version (without rebase)
This commit is contained in:
parent
9a1ee019d1
commit
5efc9b99d1
14 changed files with 2298 additions and 1850 deletions
3097
dashboard/assets.go
3097
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,13 +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,
|
send: string => void,
|
||||||
logs: () => Object,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Body renders the body of the dashboard.
|
// Body renders the body of the dashboard.
|
||||||
|
|
@ -55,7 +54,6 @@ class Body extends Component<Props> {
|
||||||
content={this.props.content}
|
content={this.props.content}
|
||||||
shouldUpdate={this.props.shouldUpdate}
|
shouldUpdate={this.props.shouldUpdate}
|
||||||
send={this.props.send}
|
send={this.props.send}
|
||||||
logs={this.props.logs}
|
|
||||||
/>
|
/>
|
||||||
</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);
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
import List, {ListItem} from 'material-ui/List';
|
|
||||||
import withStyles from 'material-ui/styles/withStyles';
|
import withStyles from 'material-ui/styles/withStyles';
|
||||||
|
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Body from './Body';
|
import Body from './Body';
|
||||||
import {MENU} from '../common';
|
import {MENU} from '../common';
|
||||||
import type {Content, Record, Chunk} 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
|
||||||
|
|
@ -76,104 +76,6 @@ 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);
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
// createLogChunk creates an HTML formatted object, which displays the given array similarly to
|
|
||||||
// the server side terminal.
|
|
||||||
const createLogChunk = (arr: Array<Record>) => {
|
|
||||||
let content = '';
|
|
||||||
arr.forEach((record) => {
|
|
||||||
let {t, lvl, msg, ctx} = 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 = '';
|
|
||||||
}
|
|
||||||
if (lvl === '' || typeof t !== 'string' || t.length < 19 || typeof msg !== 'string' || !Array.isArray(ctx)) {
|
|
||||||
content += `<span style="color:${color}">Invalid log record</span><br />`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ctx.length > 0) {
|
|
||||||
msg += ' '.repeat(Math.max(40 - msg.length, 0));
|
|
||||||
}
|
|
||||||
// Time format: 2006-01-02T15:04:05-0700 -> 01-02|15:04:05
|
|
||||||
content += `<span style="color:${color}">${lvl}</span>[${t.substr(5, 5)}|${t.substr(11, 8)}] ${msg}`;
|
|
||||||
|
|
||||||
for (let i = 0; i < ctx.length; i += 2) {
|
|
||||||
const key = ctx[i];
|
|
||||||
const value = ctx[i + 1];
|
|
||||||
let padding = fieldPadding.get(key);
|
|
||||||
if (typeof padding === 'undefined' || padding < value.length) {
|
|
||||||
padding = value.length;
|
|
||||||
fieldPadding.set(key, padding);
|
|
||||||
}
|
|
||||||
content += ` <span style="color:${color}">${key}</span>=${value}${' '.repeat(padding - value.length)}`;
|
|
||||||
}
|
|
||||||
content += '<br />';
|
|
||||||
});
|
|
||||||
return content;
|
|
||||||
};
|
|
||||||
|
|
||||||
// logAppender is a state updater function, which appends the new log chunks to the existing ones.
|
|
||||||
// In case the prev chunk array's last element doesn't have limit number of log record elements,
|
|
||||||
// it will be extended.
|
|
||||||
const logAppender = (limit: number) => (update: Array<Record>, prev: Array<Chunk>) => {
|
|
||||||
const newChunks = [];
|
|
||||||
let first = 0;
|
|
||||||
let last = 0;
|
|
||||||
let extended = 0;
|
|
||||||
if (prev.length > 0 && prev[prev.length - 1].len < limit) {
|
|
||||||
extended = 1;
|
|
||||||
const l = Math.min(limit - prev[prev.length - 1].len, update.length);
|
|
||||||
newChunks.push({
|
|
||||||
content: prev[prev.length - 1].content + createLogChunk(update.slice(0, l)),
|
|
||||||
t: prev[prev.length - 1].t,
|
|
||||||
len: prev[prev.length - 1].len + l,
|
|
||||||
});
|
|
||||||
first = l;
|
|
||||||
last = l;
|
|
||||||
}
|
|
||||||
while (last < update.length) {
|
|
||||||
last = Math.min(update.length, last + limit);
|
|
||||||
newChunks.push({
|
|
||||||
content: createLogChunk(update.slice(first, last)),
|
|
||||||
t: update[first].t,
|
|
||||||
len: last - first,
|
|
||||||
});
|
|
||||||
first += limit;
|
|
||||||
}
|
|
||||||
return [...prev.slice(0, prev.length - extended), ...newChunks];
|
|
||||||
};
|
|
||||||
|
|
||||||
// defaultContent is the initial value of the state content.
|
// defaultContent is the initial value of the state content.
|
||||||
const defaultContent: Content = {
|
const defaultContent: Content = {
|
||||||
general: {
|
general: {
|
||||||
|
|
@ -194,8 +96,12 @@ const defaultContent: Content = {
|
||||||
diskRead: [],
|
diskRead: [],
|
||||||
diskWrite: [],
|
diskWrite: [],
|
||||||
},
|
},
|
||||||
logs: {
|
logs: {
|
||||||
chunk: [],
|
chunks: [],
|
||||||
|
endTop: false,
|
||||||
|
endBottom: true,
|
||||||
|
topChanged: 0,
|
||||||
|
bottomChanged: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -221,9 +127,7 @@ const updaters = {
|
||||||
diskRead: appender(200),
|
diskRead: appender(200),
|
||||||
diskWrite: appender(200),
|
diskWrite: appender(200),
|
||||||
},
|
},
|
||||||
logs: {
|
logs: logInserter(5),
|
||||||
chunk: logAppender(50),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// styles contains the constant styles of the component.
|
// styles contains the constant styles of the component.
|
||||||
|
|
@ -236,10 +140,6 @@ const styles = {
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
logChunk: {
|
|
||||||
color: 'white',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// themeStyles returns the styles generated from the theme for the component.
|
// themeStyles returns the styles generated from the theme for the component.
|
||||||
|
|
@ -254,11 +154,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,
|
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
|
||||||
|
|
@ -302,7 +202,7 @@ class Dashboard extends Component<Props, State> {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// server can be accessed only through this function for safety reasons.
|
// send sends a message to the server, which can be accessed only through this function for safety reasons.
|
||||||
send = (msg: string) => {
|
send = (msg: string) => {
|
||||||
if (this.state.server != null) {
|
if (this.state.server != null) {
|
||||||
this.state.server.send(msg);
|
this.state.server.send(msg);
|
||||||
|
|
@ -327,18 +227,6 @@ class Dashboard extends Component<Props, State> {
|
||||||
this.setState(prevState => ({sideBar: !prevState.sideBar}));
|
this.setState(prevState => ({sideBar: !prevState.sideBar}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// logsHTML visualizes the log chunks. It is more efficient to insert pure HTML into the component, than
|
|
||||||
// to create individual component for each log record and track them all. It also postpones the OOM issue.
|
|
||||||
logsHTML = () => (
|
|
||||||
<List>
|
|
||||||
{this.state.content.logs.chunk.map((c, index) => (
|
|
||||||
<ListItem key={index}>
|
|
||||||
<div style={styles.logChunk} dangerouslySetInnerHTML={{__html: c.content}} />
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={this.props.classes.dashboard} style={styles.dashboard}>
|
<div className={this.props.classes.dashboard} style={styles.dashboard}>
|
||||||
|
|
@ -352,7 +240,6 @@ class Dashboard extends Component<Props, State> {
|
||||||
content={this.state.content}
|
content={this.state.content}
|
||||||
shouldUpdate={this.state.shouldUpdate}
|
shouldUpdate={this.state.shouldUpdate}
|
||||||
send={this.send}
|
send={this.send}
|
||||||
logs={this.logsHTML}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,274 @@
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
|
import List, {ListItem} from 'material-ui/List';
|
||||||
|
import type {Record, Content, LogsMessage, Logs as LogsType} from '../types/content';
|
||||||
|
|
||||||
|
// if the scroll position is closer to the top/bottom than this value, the client sends a request for a new log chunk.
|
||||||
|
const requestLimit = 100;
|
||||||
|
|
||||||
|
// 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 = '';
|
||||||
|
}
|
||||||
|
if (lvl === '' || typeof t !== 'string' || t.length < 19 || typeof msg !== 'string' || !Array.isArray(ctx)) {
|
||||||
|
content += `<span style="color:${color}">Invalid log record</span><br />`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ctx.length > 0) {
|
||||||
|
msg += ' '.repeat(Math.max(40 - msg.length, 0));
|
||||||
|
}
|
||||||
|
// Time format: 2006-01-02T15:04:05-0700 -> 01-02|15:04:05
|
||||||
|
content += `<span style="color:${color}">${lvl}</span>[${t.substr(5, 5)}|${t.substr(11, 8)}] ${msg}`;
|
||||||
|
|
||||||
|
for (let i = 0; i < ctx.length; i += 2) {
|
||||||
|
const key = ctx[i];
|
||||||
|
const value = ctx[i + 1];
|
||||||
|
let padding = fieldPadding.get(key);
|
||||||
|
if (typeof padding === 'undefined' || padding < value.length) {
|
||||||
|
padding = value.length;
|
||||||
|
fieldPadding.set(key, padding);
|
||||||
|
}
|
||||||
|
content += ` <span style="color:${color}">${key}</span>=${value}${' '.repeat(padding - value.length)}`;
|
||||||
|
}
|
||||||
|
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 OOM in the browser.
|
||||||
|
export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) => {
|
||||||
|
prev.topChanged = 0;
|
||||||
|
prev.bottomChanged = 0;
|
||||||
|
if (!update.stream && update.end) {
|
||||||
|
if (update.past) {
|
||||||
|
prev.endTop = true;
|
||||||
|
} else {
|
||||||
|
prev.endBottom = true;
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
if (update.stream && !prev.endBottom) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(update.chunk) || update.chunk.length < 1) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const chunk = {
|
||||||
|
content: createChunk(update.chunk),
|
||||||
|
tFirst: update.chunk[0].t,
|
||||||
|
tLast: update.chunk[update.chunk.length - 1].t,
|
||||||
|
};
|
||||||
|
if (!Array.isArray(prev.chunks) || prev.chunks.length < 1) {
|
||||||
|
prev.chunks = [chunk];
|
||||||
|
prev.topChanged = 1;
|
||||||
|
prev.bottomChanged = 1;
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
if (update.stream) {
|
||||||
|
// The stream chunks are appended to the last chunk, because otherwise the small stream chunks would cause
|
||||||
|
// imbalance in the amount of the visualized logs. In order to protect a chunk from growing too large a new
|
||||||
|
// chunk is created when a new file is opened on the server side. In case of stream end indicates if a new file
|
||||||
|
// was opened.
|
||||||
|
if (update.end) {
|
||||||
|
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];
|
||||||
|
} else {
|
||||||
|
prev.chunks[prev.chunks.length - 1].content += chunk.content;
|
||||||
|
prev.chunks[prev.chunks.length - 1].tLast = chunk.tLast;
|
||||||
|
}
|
||||||
|
prev.bottomChanged = 1;
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
if (update.past) {
|
||||||
|
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 (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 = {
|
||||||
|
logs: {
|
||||||
|
overflowX: 'auto',
|
||||||
|
},
|
||||||
|
logListItem: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
logChunk: {
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
logs: () => Object,
|
container: Object,
|
||||||
|
content: Content,
|
||||||
|
shouldUpdate: Object,
|
||||||
|
send: string => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
requestAllowed: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Logs renders the log page.
|
// Logs renders the log page.
|
||||||
class Logs extends Component<Props> {
|
class Logs extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
requestAllowed: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = () => {
|
||||||
|
const {logs} = this.props.content;
|
||||||
|
if (typeof this.props.container === 'undefined' || logs.chunks.length < 1 || !this.state.requestAllowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.atTop() && !logs.endTop) {
|
||||||
|
this.props.send(JSON.stringify({
|
||||||
|
Logs: {
|
||||||
|
Time: logs.chunks[0].tFirst,
|
||||||
|
Past: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
this.setState({requestAllowed: false});
|
||||||
|
}
|
||||||
|
if (this.atBottom() && !logs.endBottom) {
|
||||||
|
this.props.send(JSON.stringify({
|
||||||
|
Logs: {
|
||||||
|
Time: logs.chunks[logs.chunks.length - 1].tLast,
|
||||||
|
Past: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
this.setState({requestAllowed: false});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// atTop checks if the scroll position it at the top of the container.
|
||||||
|
atTop = () => this.props.container.scrollTop <= requestLimit;
|
||||||
|
|
||||||
|
// atBottom checks if the scroll position it at the bottom of the container.
|
||||||
|
atBottom = () =>
|
||||||
|
this.props.container.scrollHeight - this.props.container.scrollTop <=
|
||||||
|
this.props.container.clientHeight + requestLimit;
|
||||||
|
|
||||||
|
// 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 = () => {
|
||||||
|
if (typeof this.props.shouldUpdate.logs === 'undefined' || typeof this.content === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {logs} = this.props.content;
|
||||||
|
const {container} = this.props;
|
||||||
|
if (typeof container === 'undefined' || logs.chunks.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({requestAllowed: true});
|
||||||
|
if (this.content.clientHeight < container.clientHeight) {
|
||||||
|
// Only enters here at the beginning, when there isn't enough log to fill the container.
|
||||||
|
//
|
||||||
|
// In case there isn't any log chunk in the array, a request with time (new Date()).toISOString()
|
||||||
|
// could be sent, but it would allow to duplicate the first few records from the stream, since the
|
||||||
|
// stream handler loads the last file. No log records will appear before the first stream chunk.
|
||||||
|
if (!logs.endTop) {
|
||||||
|
this.props.send(JSON.stringify({
|
||||||
|
Logs: {
|
||||||
|
Time: logs.chunks[0].tFirst,
|
||||||
|
Past: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
this.setState({requestAllowed: false});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chunks = this.content.children[0].children;
|
||||||
|
if (this.atTop()) {
|
||||||
|
if (logs.topChanged > 0) {
|
||||||
|
container.scrollTop = chunks[0].clientHeight;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.atBottom() && logs.bottomChanged > 0) {
|
||||||
|
if (logs.endBottom) {
|
||||||
|
container.scrollTop = container.scrollHeight - container.clientHeight;
|
||||||
|
} else {
|
||||||
|
container.scrollTop = container.scrollHeight - chunks[chunks.length - 1].clientHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div >
|
<div style={styles.logs} ref={(ref) => { this.content = ref; }}>
|
||||||
{this.props.logs()}
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,36 +47,27 @@ 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,
|
send: string => void,
|
||||||
logs: () => Object,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Main renders the chosen content.
|
// Main renders the chosen content.
|
||||||
class Main extends Component<Props> {
|
class Main extends Component<Props, State> {
|
||||||
handleScroll = () => {
|
componentDidUpdate() {
|
||||||
if (typeof this.container !== 'undefined') {
|
if (this.content && typeof this.content.didUpdate === 'function') {
|
||||||
// console.log(this.container.scrollTop, this.container.scrollHeight);
|
this.content.didUpdate();
|
||||||
if (this.container.scrollTop === 0) {
|
}
|
||||||
// this.props.send(JSON.stringify({Logs: {Time: '2018-04-11T12:48:18.181274193+03:00'}}));
|
}
|
||||||
console.log("Top");
|
|
||||||
}
|
onScroll = () => {
|
||||||
if (this.container.scrollHeight - this.container.scrollTop === this.container.clientHeight) {
|
if (this.content && typeof this.content.onScroll === 'function') {
|
||||||
console.log("Bottom");
|
this.content.onScroll();
|
||||||
// this.container.scrollTop = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
// if (typeof this.container !== 'undefined') {
|
|
||||||
// this.container.scrollTop = this.container.scrollHeight - this.container.clientHeight;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
classes, active, content, shouldUpdate,
|
classes, active, content, shouldUpdate,
|
||||||
|
|
@ -92,7 +83,15 @@ 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 = <Logs logs={this.props.logs} />;
|
children = (
|
||||||
|
<Logs
|
||||||
|
ref={(ref) => { this.content = ref; }}
|
||||||
|
container={this.container}
|
||||||
|
send={this.props.send}
|
||||||
|
content={this.props.content}
|
||||||
|
shouldUpdate={shouldUpdate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -101,7 +100,7 @@ class Main extends Component<Props> {
|
||||||
className={classes.content}
|
className={classes.content}
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
ref={(ref) => { this.container = ref; }}
|
ref={(ref) => { this.container = ref; }}
|
||||||
onScroll={this.handleScroll}
|
onScroll={this.onScroll}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ export type ChartEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type General = {
|
export type General = {
|
||||||
version: ?string,
|
version: ?string,
|
||||||
commit: ?string,
|
commit: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Home = {
|
export type Home = {
|
||||||
|
|
@ -55,29 +55,40 @@ 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 = {
|
export type Record = {
|
||||||
t: Object,
|
t: string,
|
||||||
lvl: Object,
|
lvl: Object,
|
||||||
msg: string,
|
msg: string,
|
||||||
ctx: Array<string>
|
ctx: Array<string>
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Chunk = {
|
export type Chunk = {
|
||||||
content: string,
|
content: string,
|
||||||
t: string,
|
tFirst: string,
|
||||||
len: int,
|
tLast: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Logs = {
|
export type Logs = {
|
||||||
chunk: Array<Chunk>,
|
chunks: Array<Chunk>,
|
||||||
|
endTop: boolean,
|
||||||
|
endBottom: boolean,
|
||||||
|
topChanged: number,
|
||||||
|
bottomChanged: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LogsMessage = {
|
||||||
|
stream: boolean,
|
||||||
|
past: boolean,
|
||||||
|
end: boolean,
|
||||||
|
chunk: Array<Record>,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import (
|
||||||
"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/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/mohae/deepcopy"
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -110,7 +111,11 @@ func New(config *Config, commit string, logdir string) (*Dashboard, error) {
|
||||||
DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh),
|
DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh),
|
||||||
DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh),
|
DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh),
|
||||||
},
|
},
|
||||||
Logs: &LogsMessage{Chunk: json.RawMessage("[]")},
|
Logs: &LogsMessage{
|
||||||
|
Stream: true,
|
||||||
|
End: false,
|
||||||
|
Chunk: json.RawMessage("[]"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
logdir: logdir,
|
logdir: logdir,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
@ -239,7 +244,7 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
|
|
||||||
db.lock.Lock()
|
db.lock.Lock()
|
||||||
// Send the past data.
|
// Send the past data.
|
||||||
client.msg <- db.history.DeepCopy()
|
client.msg <- deepcopy.Copy(db.history).(*Message)
|
||||||
// Start tracking the connection and drop at connection loss.
|
// Start tracking the connection and drop at connection loss.
|
||||||
db.conns[id] = client
|
db.conns[id] = client
|
||||||
db.lock.Unlock()
|
db.lock.Unlock()
|
||||||
|
|
@ -252,70 +257,121 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
var r Request
|
var r Request
|
||||||
err := websocket.JSON.Receive(conn, &r)
|
err := websocket.JSON.Receive(conn, &r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
client.logger.Warn("Failed to receive request", "err", err)
|
||||||
close(done)
|
close(done)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Logs != nil {
|
if r.Logs != nil {
|
||||||
db.handleLogs(r.Logs, client) // TODO (kurkomisi): concurrent function call?
|
db.handleLogRequest(r.Logs, client) // TODO (kurkomisi): concurrent function call?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleLogs searches for the log file specified by the timestamp of the request, creates a JSON array out of it
|
func validateLogFile(path string) ([]byte, bool) {
|
||||||
|
f, err := os.OpenFile(path, os.O_RDONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to open file", "path", path, "err", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
var buf []byte
|
||||||
|
if buf, err = ioutil.ReadAll(f); err != nil {
|
||||||
|
log.Warn("Failed to read file", "path", path, "err", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
end := -1
|
||||||
|
for j := 0; j < len(buf); j++ {
|
||||||
|
if buf[j] == '\n' {
|
||||||
|
buf[j] = ','
|
||||||
|
end = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if end < 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[:end], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// and sends it to the requesting client.
|
||||||
func (db *Dashboard) handleLogs(r *LogsRequest, c *client) {
|
func (db *Dashboard) handleLogRequest(r *LogsRequest, c *client) {
|
||||||
files, err := ioutil.ReadDir(db.logdir)
|
files, err := ioutil.ReadDir(db.logdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to open logdir", "logdir", db.logdir, "err", err)
|
log.Warn("Failed to open logdir", "logdir", db.logdir, "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
re := regexp.MustCompile(".log$")
|
re := regexp.MustCompile(".log$")
|
||||||
valid := make([]string, len(files))
|
fileNames := make([]string, len(files))
|
||||||
n := 0
|
n := 0
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if f.Mode().IsRegular() && re.Match([]byte(f.Name())) {
|
if f.Mode().IsRegular() && re.Match([]byte(f.Name())) {
|
||||||
valid[n] = f.Name()
|
fileNames[n] = f.Name()
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(valid) < 1 {
|
n-- // The last file is handled by the stream handler in order to avoid log duplication on the client side.
|
||||||
log.Warn("There isn't any log file in the logdir", "logdir", db.logdir)
|
if n < 1 {
|
||||||
|
log.Warn("There isn't any old log file in the logdir", "path", db.logdir)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
timestamp := fmt.Sprintf("%s.log", strings.Replace(r.Time.Format("060102150405.00"), ".", "", 1))
|
timestamp := fmt.Sprintf("%s.log", strings.Replace(r.Time.Format("060102150405.00"), ".", "", 1))
|
||||||
i := sort.Search(len(valid), func(i int) bool {
|
|
||||||
return valid[i] >= timestamp
|
|
||||||
})
|
|
||||||
if i >= len(valid) {
|
|
||||||
i = len(valid) - 1
|
|
||||||
}
|
|
||||||
f, err := os.OpenFile(filepath.Join(db.logdir, valid[i]), os.O_RDONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to open file", "name", valid[i], "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
buf, err := ioutil.ReadAll(f)
|
|
||||||
last := -1
|
|
||||||
for i := 0; i < len(buf); i++ {
|
|
||||||
if buf[i] == '\n' {
|
|
||||||
buf[i] = ','
|
|
||||||
last = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if last >= 0 {
|
|
||||||
b := make([]byte, last+2)
|
|
||||||
b[0] = '['
|
|
||||||
copy(b[1:], buf[:last])
|
|
||||||
b[last+1] = ']'
|
|
||||||
|
|
||||||
db.lock.Lock() // TODO (kurkomisi): Maybe create mutex for the client.
|
i := sort.Search(n, func(i int) bool {
|
||||||
c.msg <- &Message{
|
return fileNames[i] >= timestamp // Returns the smallest index such as fileNames[i] >= timestamp.
|
||||||
Logs: &LogsMessage{
|
})
|
||||||
Chunk: b,
|
ok := false
|
||||||
},
|
var buf json.RawMessage
|
||||||
|
if r.Past {
|
||||||
|
if i >= n {
|
||||||
|
i = n - 1
|
||||||
}
|
}
|
||||||
db.lock.Unlock()
|
for i >= 0 && fileNames[i] >= timestamp {
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
for i >= 0 && !ok {
|
||||||
|
buf, ok = validateLogFile(filepath.Join(db.logdir, fileNames[i]))
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i < n && fileNames[i] <= timestamp {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
for i < n && !ok {
|
||||||
|
buf, ok = validateLogFile(filepath.Join(db.logdir, fileNames[i]))
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if buf == nil {
|
||||||
|
buf = json.RawMessage{}
|
||||||
|
}
|
||||||
|
b := make(json.RawMessage, len(buf)+2)
|
||||||
|
b[0] = '['
|
||||||
|
copy(b[1:], buf)
|
||||||
|
b[len(buf)+1] = ']'
|
||||||
|
|
||||||
|
db.lock.Lock()
|
||||||
|
c.msg <- &Message{
|
||||||
|
Logs: &LogsMessage{
|
||||||
|
Stream: false,
|
||||||
|
Past: r.Past,
|
||||||
|
End: !ok,
|
||||||
|
Chunk: b,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
db.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// metricCollector returns a function, which retrieves a specific metric.
|
||||||
|
func metricCollector(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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,12 +384,17 @@ func (db *Dashboard) collectData() {
|
||||||
var (
|
var (
|
||||||
mem runtime.MemStats
|
mem runtime.MemStats
|
||||||
|
|
||||||
prevNetworkIngress = metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count()
|
collectNetworkIngress = metricCollector("p2p/InboundTraffic")
|
||||||
prevNetworkEgress = metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count()
|
collectNetworkEgress = metricCollector("p2p/OutboundTraffic")
|
||||||
|
collectDiskRead = metricCollector("eth/db/chaindata/disk/read")
|
||||||
|
collectDiskWrite = metricCollector("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())
|
||||||
|
|
@ -349,12 +410,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)
|
||||||
|
|
@ -436,13 +497,6 @@ func (db *Dashboard) collectData() {
|
||||||
func (db *Dashboard) streamLogs() {
|
func (db *Dashboard) streamLogs() {
|
||||||
defer db.wg.Done()
|
defer db.wg.Done()
|
||||||
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to create fs watcher", "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer watcher.Close()
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(db.logdir)
|
files, err := ioutil.ReadDir(db.logdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to open logdir", "logdir", db.logdir, "err", err)
|
log.Warn("Failed to open logdir", "logdir", db.logdir, "err", err)
|
||||||
|
|
@ -468,31 +522,34 @@ func (db *Dashboard) streamLogs() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to create fs watcher", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
err = watcher.Add(db.logdir)
|
err = watcher.Add(db.logdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to add logdir to fs watcher", "logdir", db.logdir, "err", err)
|
log.Warn("Failed to add logdir to fs watcher", "logdir", db.logdir, "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
change := fsnotify.Create | fsnotify.Remove | fsnotify.Rename
|
defer opened.Close() // Close the lastly opened file.
|
||||||
|
ticker := time.NewTicker(db.config.Refresh)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
newFile := false
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-watcher.Events:
|
case event := <-watcher.Events:
|
||||||
switch {
|
// If new log file was created.
|
||||||
// If new log file is opened.
|
if event.Op&fsnotify.Create != 0 && re.Match([]byte(event.Name)) {
|
||||||
case event.Op&change != 0:
|
|
||||||
if re.Match([]byte(event.Name)) && opened.Name() < event.Name {
|
|
||||||
if opened, err = os.OpenFile(event.Name, os.O_RDONLY, 0644); err != nil {
|
|
||||||
log.Warn("Failed to open file", "name", event.Name, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
db.lock.Lock()
|
|
||||||
db.history.Logs.Chunk = json.RawMessage("[]")
|
|
||||||
db.lock.Unlock()
|
|
||||||
}
|
|
||||||
// If new log records were written into the opened log file.
|
|
||||||
case event.Op&fsnotify.Write != 0:
|
|
||||||
if opened != nil {
|
if opened != nil {
|
||||||
|
// The new log file's timestamp is always greater, since it is created of the actual time.
|
||||||
|
if opened.Name() >= event.Name {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Read the rest of the previously opened file.
|
||||||
chunk, err := ioutil.ReadAll(opened)
|
chunk, err := ioutil.ReadAll(opened)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to read file", "name", opened.Name(), "err", err)
|
log.Warn("Failed to read file", "name", opened.Name(), "err", err)
|
||||||
|
|
@ -502,6 +559,37 @@ func (db *Dashboard) streamLogs() {
|
||||||
copy(b, buf)
|
copy(b, buf)
|
||||||
copy(b[len(buf):], chunk)
|
copy(b[len(buf):], chunk)
|
||||||
buf = b
|
buf = b
|
||||||
|
opened.Close()
|
||||||
|
}
|
||||||
|
last := -1
|
||||||
|
for i := 0; i < len(buf); i++ {
|
||||||
|
if buf[i] == '\n' {
|
||||||
|
buf[i] = ','
|
||||||
|
last = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last >= 0 {
|
||||||
|
msg := make([]byte, last+2)
|
||||||
|
msg[0] = '['
|
||||||
|
copy(msg[1:], buf[:last])
|
||||||
|
msg[last+1] = ']'
|
||||||
|
|
||||||
|
db.sendToAll(&Message{
|
||||||
|
Logs: &LogsMessage{
|
||||||
|
Stream: true,
|
||||||
|
End: false,
|
||||||
|
Chunk: msg,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
db.lock.Lock()
|
||||||
|
db.history.Logs.Chunk = json.RawMessage("[]")
|
||||||
|
db.lock.Unlock()
|
||||||
|
}
|
||||||
|
buf = buf[:0]
|
||||||
|
newFile = true
|
||||||
|
if opened, err = os.OpenFile(event.Name, os.O_RDONLY, 0644); err != nil {
|
||||||
|
log.Warn("Failed to open file", "name", event.Name, "err", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case err := <-watcher.Errors:
|
case err := <-watcher.Errors:
|
||||||
|
|
@ -509,51 +597,62 @@ func (db *Dashboard) streamLogs() {
|
||||||
log.Warn("Fs watcher error", "err", err)
|
log.Warn("Fs watcher error", "err", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
// Send log updates to the client.
|
|
||||||
case <-time.After(db.config.Refresh):
|
|
||||||
last := -1
|
|
||||||
for i := 0; i < len(buf); i++ {
|
|
||||||
if buf[i] == '\n' {
|
|
||||||
buf[i] = ','
|
|
||||||
last = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if last >= 0 {
|
|
||||||
b := make([]byte, last+2)
|
|
||||||
b[0] = '['
|
|
||||||
copy(b[1:], buf[:last])
|
|
||||||
b[last+1] = ']'
|
|
||||||
|
|
||||||
db.sendToAll(&Message{
|
|
||||||
Logs: &LogsMessage{
|
|
||||||
Chunk: b,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
b = make([]byte, len(db.history.Logs.Chunk)+last+1)
|
|
||||||
// Cut the ']' from the end in order to concatenate the two arrays.
|
|
||||||
n := len(db.history.Logs.Chunk) - 1
|
|
||||||
copy(b, db.history.Logs.Chunk[:n])
|
|
||||||
if len(db.history.Logs.Chunk) > 2 {
|
|
||||||
// In case the array already contained log records, put the comma separator.
|
|
||||||
b[n] = ','
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
copy(b[n:], buf[:last])
|
|
||||||
n += last
|
|
||||||
b[n] = ']'
|
|
||||||
n++
|
|
||||||
|
|
||||||
db.lock.Lock()
|
|
||||||
db.history.Logs.Chunk = b[:n]
|
|
||||||
db.lock.Unlock()
|
|
||||||
|
|
||||||
// Clear the valid/sent part of the buffer.
|
|
||||||
buf = buf[last+1:]
|
|
||||||
}
|
|
||||||
case errc := <-db.quit:
|
case errc := <-db.quit:
|
||||||
errc <- nil
|
errc <- nil
|
||||||
return
|
return
|
||||||
|
// Send log updates to the client.
|
||||||
|
case <-ticker.C:
|
||||||
|
if opened == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b := make([]byte, len(buf)+len(chunk))
|
||||||
|
copy(b, buf)
|
||||||
|
copy(b[len(buf):], chunk)
|
||||||
|
last := -1
|
||||||
|
for i := 0; i < len(b); i++ {
|
||||||
|
if b[i] == '\n' {
|
||||||
|
b[i] = ','
|
||||||
|
last = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Clear the valid/sent part of the buffer.
|
||||||
|
buf = b[last+1:]
|
||||||
|
|
||||||
|
msg := make([]byte, last+2)
|
||||||
|
msg[0] = '['
|
||||||
|
copy(msg[1:], b[:last])
|
||||||
|
msg[last+1] = ']'
|
||||||
|
|
||||||
|
db.sendToAll(&Message{
|
||||||
|
Logs: &LogsMessage{
|
||||||
|
Stream: true,
|
||||||
|
End: newFile,
|
||||||
|
Chunk: msg,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
newFile = false
|
||||||
|
|
||||||
|
db.lock.Lock()
|
||||||
|
if len(db.history.Logs.Chunk) == 2 {
|
||||||
|
db.history.Logs.Chunk = msg
|
||||||
|
} else {
|
||||||
|
b = make([]byte, len(db.history.Logs.Chunk)+len(msg)-1)
|
||||||
|
copy(b, db.history.Logs.Chunk)
|
||||||
|
b[len(db.history.Logs.Chunk)-1] = ','
|
||||||
|
copy(b[len(db.history.Logs.Chunk):], msg[1:])
|
||||||
|
db.history.Logs.Chunk = b
|
||||||
|
}
|
||||||
|
db.lock.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,46 +31,18 @@ type Message struct {
|
||||||
Logs *LogsMessage `json:"logs,omitempty"`
|
Logs *LogsMessage `json:"logs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) DeepCopy() *Message {
|
|
||||||
return &Message{
|
|
||||||
m.General.DeepCopy(),
|
|
||||||
m.Home,
|
|
||||||
m.Chain,
|
|
||||||
m.TxPool,
|
|
||||||
m.Network,
|
|
||||||
m.System.DeepCopy(),
|
|
||||||
m.Logs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartEntries []*ChartEntry
|
type ChartEntries []*ChartEntry
|
||||||
|
|
||||||
func (ce ChartEntries) DeepCopy() ChartEntries {
|
|
||||||
nce := make(ChartEntries, len(ce))
|
|
||||||
for i, v := range ce {
|
|
||||||
nce[i] = v.DeepCopy()
|
|
||||||
}
|
|
||||||
return nce
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartEntry struct {
|
type ChartEntry struct {
|
||||||
Time time.Time `json:"time,omitempty"`
|
Time time.Time `json:"time,omitempty"`
|
||||||
Value float64 `json:"value,omitempty"`
|
Value float64 `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ce *ChartEntry) DeepCopy() *ChartEntry {
|
|
||||||
return &ChartEntry{ce.Time, ce.Value}
|
|
||||||
}
|
|
||||||
|
|
||||||
type GeneralMessage struct {
|
type GeneralMessage struct {
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
Commit string `json:"commit,omitempty"`
|
Commit string `json:"commit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GeneralMessage) DeepCopy() *GeneralMessage {
|
|
||||||
return &GeneralMessage{m.Version, m.Commit}
|
|
||||||
}
|
|
||||||
|
|
||||||
type HomeMessage struct {
|
type HomeMessage struct {
|
||||||
/* TODO (kurkomisi) */
|
/* TODO (kurkomisi) */
|
||||||
}
|
}
|
||||||
|
|
@ -98,21 +70,11 @@ type SystemMessage struct {
|
||||||
DiskWrite ChartEntries `json:"diskWrite,omitempty"`
|
DiskWrite ChartEntries `json:"diskWrite,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SystemMessage) DeepCopy() *SystemMessage {
|
|
||||||
return &SystemMessage{
|
|
||||||
m.ActiveMemory.DeepCopy(),
|
|
||||||
m.VirtualMemory.DeepCopy(),
|
|
||||||
m.NetworkIngress.DeepCopy(),
|
|
||||||
m.NetworkEgress.DeepCopy(),
|
|
||||||
m.ProcessCPU.DeepCopy(),
|
|
||||||
m.SystemCPU.DeepCopy(),
|
|
||||||
m.DiskRead.DeepCopy(),
|
|
||||||
m.DiskWrite.DeepCopy(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogsMessage struct {
|
type LogsMessage struct {
|
||||||
Chunk json.RawMessage `json:"chunk,omitempty"`
|
Stream bool `json:"stream"` // Denotes if the chunk is part of the stream or if it contains the records from a file.
|
||||||
|
Past bool `json:"past"` // Denotes whether the logs in the chunk were issued before or after the time given in the request.
|
||||||
|
End bool `json:"end"` // In case of stream denotes if new file was opened, otherwise denotes if there isn't more file.
|
||||||
|
Chunk json.RawMessage `json:"chunk"` // Contains log records.
|
||||||
}
|
}
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
|
|
@ -120,5 +82,6 @@ type Request struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogsRequest struct {
|
type LogsRequest struct {
|
||||||
Time time.Time `json:"time,omitempty"`
|
Time time.Time `json:"time"` // The request handler searches for log file based on this timestamp.
|
||||||
|
Past bool `json:"past"` // Denotes whether the message should contain logs issued before or after the given time.
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
vendor/github.com/mohae/deepcopy/LICENSE
generated
vendored
Normal file
21
vendor/github.com/mohae/deepcopy/LICENSE
generated
vendored
Normal file
|
|
@ -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.
|
||||||
8
vendor/github.com/mohae/deepcopy/README.md
generated
vendored
Normal file
8
vendor/github.com/mohae/deepcopy/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
deepCopy
|
||||||
|
========
|
||||||
|
[](https://godoc.org/github.com/mohae/deepcopy)[](https://travis-ci.org/mohae/deepcopy)
|
||||||
|
|
||||||
|
DeepCopy makes deep copies of things: unexported field values are not copied.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
cpy := deepcopy.Copy(orig)
|
||||||
125
vendor/github.com/mohae/deepcopy/deepcopy.go
generated
vendored
Normal file
125
vendor/github.com/mohae/deepcopy/deepcopy.go
generated
vendored
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
|
|
@ -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": "2gmvVTDCks8cPhpmyDlvm0sbrXE=",
|
"checksumSHA1": "2gmvVTDCks8cPhpmyDlvm0sbrXE=",
|
||||||
"path": "github.com/naoina/toml",
|
"path": "github.com/naoina/toml",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue