mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 02:12:23 +00:00
cmd, dashboard: change logging behavior
This commit is contained in:
parent
b0fa18b08f
commit
3ecf815981
9 changed files with 10409 additions and 8404 deletions
|
|
@ -46,7 +46,6 @@ import (
|
|||
swarmmetrics "github.com/ethereum/go-ethereum/swarm/metrics"
|
||||
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const clientIdentifier = "swarm"
|
||||
|
|
|
|||
17305
dashboard/assets.go
17305
dashboard/assets.go
File diff suppressed because one or more lines are too long
|
|
@ -21,8 +21,8 @@ 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;
|
||||
// requestBand says how wide is the top/bottom zone, eg. 0.1 means 10% of the container height.
|
||||
const requestBand = 0.1;
|
||||
|
||||
// fieldPadding is a global map with maximum field value lengths seen until now
|
||||
// to allow padding log contexts in a bit smarter way.
|
||||
|
|
@ -93,55 +93,37 @@ const createChunk = (records: Array<Record>) => {
|
|||
};
|
||||
|
||||
// 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.
|
||||
// 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 (!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;
|
||||
}
|
||||
if (!Array.isArray(prev.chunks)) {
|
||||
prev.chunks = [];
|
||||
}
|
||||
const content = createChunk(update.chunk);
|
||||
if (!update.old) {
|
||||
// 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;
|
||||
return prev;
|
||||
}
|
||||
const chunk = {
|
||||
content: createChunk(update.chunk),
|
||||
tFirst: update.chunk[0].t,
|
||||
tLast: update.chunk[update.chunk.length - 1].t,
|
||||
content,
|
||||
name: update.old.name,
|
||||
};
|
||||
if (!Array.isArray(prev.chunks) || prev.chunks.length < 1) {
|
||||
prev.chunks = [chunk];
|
||||
prev.topChanged = 1;
|
||||
prev.bottomChanged = 1;
|
||||
return prev;
|
||||
if (update.old.past) {
|
||||
if (update.old.last) {
|
||||
prev.endTop = true;
|
||||
}
|
||||
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);
|
||||
|
|
@ -151,6 +133,9 @@ export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType)
|
|||
prev.topChanged = 1;
|
||||
return prev;
|
||||
}
|
||||
if (update.old.last) {
|
||||
prev.endBottom = true;
|
||||
}
|
||||
if (prev.chunks.length >= limit) {
|
||||
prev.endTop = false;
|
||||
prev.chunks.splice(0, prev.chunks.length - limit + 1);
|
||||
|
|
@ -190,51 +175,72 @@ type State = {
|
|||
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 = () => {
|
||||
const {logs} = this.props.content;
|
||||
if (typeof this.props.container === 'undefined' || logs.chunks.length < 1 || !this.state.requestAllowed) {
|
||||
if (!this.state.requestAllowed || typeof this.content === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (this.atTop() && !logs.endTop) {
|
||||
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: {
|
||||
Time: logs.chunks[0].tFirst,
|
||||
Name: logs.chunks[0].name,
|
||||
Past: true,
|
||||
},
|
||||
}));
|
||||
this.setState({requestAllowed: false});
|
||||
}
|
||||
if (this.atBottom() && !logs.endBottom) {
|
||||
} else if (this.atBottom()) {
|
||||
if (!logs.endBottom) {
|
||||
this.setState({requestAllowed: false});
|
||||
this.props.send(JSON.stringify({
|
||||
Logs: {
|
||||
Time: logs.chunks[logs.chunks.length - 1].tLast,
|
||||
Name: logs.chunks[logs.chunks.length - 1].name,
|
||||
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;
|
||||
atTop = () => this.props.container.scrollTop <= this.props.container.scrollHeight * requestBand;
|
||||
|
||||
// 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;
|
||||
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 = () => ({
|
||||
scrollTop: this.props.container.scrollTop,
|
||||
firstHeight: this.content.children[0].children[0].clientHeight,
|
||||
});
|
||||
|
||||
// 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') {
|
||||
didUpdate = (prevProps, prevState, snapshot) => {
|
||||
if (typeof this.props.shouldUpdate.logs === 'undefined' || typeof this.content === 'undefined' || snapshot === null) {
|
||||
return;
|
||||
}
|
||||
const {logs} = this.props.content;
|
||||
|
|
@ -242,38 +248,34 @@ class Logs extends Component<Props, State> {
|
|||
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.
|
||||
// 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: {
|
||||
Time: logs.chunks[0].tFirst,
|
||||
Name: logs.chunks[0].name,
|
||||
Past: true,
|
||||
},
|
||||
}));
|
||||
this.setState({requestAllowed: false});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const chunks = this.content.children[0].children;
|
||||
if (this.atTop()) {
|
||||
let {scrollTop} = snapshot;
|
||||
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;
|
||||
scrollTop += chunks[0].clientHeight;
|
||||
} else if (logs.bottomChanged > 0) {
|
||||
if (logs.topChanged < 0) {
|
||||
scrollTop -= snapshot.firstHeight;
|
||||
}
|
||||
}
|
||||
if (logs.endBottom && this.atBottom()) {
|
||||
scrollTop = container.scrollHeight - container.clientHeight;
|
||||
}
|
||||
container.scrollTop = scrollTop;
|
||||
this.setState({requestAllowed: true});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const styles = {
|
|||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
overflowX: 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -55,10 +55,23 @@ export type Props = {
|
|||
};
|
||||
|
||||
// Main renders the chosen content.
|
||||
class Main extends Component<Props, State> {
|
||||
componentDidUpdate() {
|
||||
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();
|
||||
this.content.didUpdate(prevProps, prevState, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,8 +74,7 @@ export type Record = {
|
|||
|
||||
export type Chunk = {
|
||||
content: string,
|
||||
tFirst: string,
|
||||
tLast: string,
|
||||
name: string,
|
||||
};
|
||||
|
||||
export type Logs = {
|
||||
|
|
@ -87,8 +86,12 @@ export type Logs = {
|
|||
};
|
||||
|
||||
export type LogsMessage = {
|
||||
stream: boolean,
|
||||
past: boolean,
|
||||
end: boolean,
|
||||
old: ?LogFile,
|
||||
chunk: Array<Record>,
|
||||
};
|
||||
|
||||
export type LogFile = {
|
||||
name: string,
|
||||
past: string,
|
||||
last: string,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,70 +2,77 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/code-frame@7.0.0-beta.40", "@babel/code-frame@^7.0.0-beta.40":
|
||||
version "7.0.0-beta.40"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.40.tgz#37e2b0cf7c56026b4b21d3927cadf81adec32ac6"
|
||||
"@babel/code-frame@7.0.0-beta.44":
|
||||
version "7.0.0-beta.44"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9"
|
||||
dependencies:
|
||||
"@babel/highlight" "7.0.0-beta.40"
|
||||
"@babel/highlight" "7.0.0-beta.44"
|
||||
|
||||
"@babel/generator@7.0.0-beta.40":
|
||||
version "7.0.0-beta.40"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.40.tgz#ab61f9556f4f71dbd1138949c795bb9a21e302ea"
|
||||
"@babel/generator@7.0.0-beta.44":
|
||||
version "7.0.0-beta.44"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42"
|
||||
dependencies:
|
||||
"@babel/types" "7.0.0-beta.40"
|
||||
"@babel/types" "7.0.0-beta.44"
|
||||
jsesc "^2.5.1"
|
||||
lodash "^4.2.0"
|
||||
source-map "^0.5.0"
|
||||
trim-right "^1.0.1"
|
||||
|
||||
"@babel/helper-function-name@7.0.0-beta.40":
|
||||
version "7.0.0-beta.40"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.40.tgz#9d033341ab16517f40d43a73f2d81fc431ccd7b6"
|
||||
"@babel/helper-function-name@7.0.0-beta.44":
|
||||
version "7.0.0-beta.44"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd"
|
||||
dependencies:
|
||||
"@babel/helper-get-function-arity" "7.0.0-beta.40"
|
||||
"@babel/template" "7.0.0-beta.40"
|
||||
"@babel/types" "7.0.0-beta.40"
|
||||
"@babel/helper-get-function-arity" "7.0.0-beta.44"
|
||||
"@babel/template" "7.0.0-beta.44"
|
||||
"@babel/types" "7.0.0-beta.44"
|
||||
|
||||
"@babel/helper-get-function-arity@7.0.0-beta.40":
|
||||
version "7.0.0-beta.40"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.40.tgz#ac0419cf067b0ec16453e1274f03878195791c6e"
|
||||
"@babel/helper-get-function-arity@7.0.0-beta.44":
|
||||
version "7.0.0-beta.44"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15"
|
||||
dependencies:
|
||||
"@babel/types" "7.0.0-beta.40"
|
||||
"@babel/types" "7.0.0-beta.44"
|
||||
|
||||
"@babel/highlight@7.0.0-beta.40":
|
||||
version "7.0.0-beta.40"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.40.tgz#b43d67d76bf46e1d10d227f68cddcd263786b255"
|
||||
"@babel/helper-split-export-declaration@7.0.0-beta.44":
|
||||
version "7.0.0-beta.44"
|
||||
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:
|
||||
chalk "^2.0.0"
|
||||
esutils "^2.0.2"
|
||||
js-tokens "^3.0.0"
|
||||
|
||||
"@babel/template@7.0.0-beta.40":
|
||||
version "7.0.0-beta.40"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.40.tgz#034988c6424eb5c3268fe6a608626de1f4410fc8"
|
||||
"@babel/template@7.0.0-beta.44":
|
||||
version "7.0.0-beta.44"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
|
||||
dependencies:
|
||||
"@babel/code-frame" "7.0.0-beta.40"
|
||||
"@babel/types" "7.0.0-beta.40"
|
||||
babylon "7.0.0-beta.40"
|
||||
"@babel/code-frame" "7.0.0-beta.44"
|
||||
"@babel/types" "7.0.0-beta.44"
|
||||
babylon "7.0.0-beta.44"
|
||||
lodash "^4.2.0"
|
||||
|
||||
"@babel/traverse@^7.0.0-beta.40":
|
||||
version "7.0.0-beta.40"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.40.tgz#d140e449b2e093ef9fe1a2eecc28421ffb4e521e"
|
||||
"@babel/traverse@7.0.0-beta.44":
|
||||
version "7.0.0-beta.44"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966"
|
||||
dependencies:
|
||||
"@babel/code-frame" "7.0.0-beta.40"
|
||||
"@babel/generator" "7.0.0-beta.40"
|
||||
"@babel/helper-function-name" "7.0.0-beta.40"
|
||||
"@babel/types" "7.0.0-beta.40"
|
||||
babylon "7.0.0-beta.40"
|
||||
debug "^3.0.1"
|
||||
"@babel/code-frame" "7.0.0-beta.44"
|
||||
"@babel/generator" "7.0.0-beta.44"
|
||||
"@babel/helper-function-name" "7.0.0-beta.44"
|
||||
"@babel/helper-split-export-declaration" "7.0.0-beta.44"
|
||||
"@babel/types" "7.0.0-beta.44"
|
||||
babylon "7.0.0-beta.44"
|
||||
debug "^3.1.0"
|
||||
globals "^11.1.0"
|
||||
invariant "^2.2.0"
|
||||
lodash "^4.2.0"
|
||||
|
||||
"@babel/types@7.0.0-beta.40", "@babel/types@^7.0.0-beta.40":
|
||||
version "7.0.0-beta.40"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.40.tgz#25c3d7aae14126abe05fcb098c65a66b6d6b8c14"
|
||||
"@babel/types@7.0.0-beta.44":
|
||||
version "7.0.0-beta.44"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757"
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
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"
|
||||
|
||||
babel-core@^6.26.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
|
||||
version "6.26.3"
|
||||
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
|
||||
dependencies:
|
||||
babel-code-frame "^6.26.0"
|
||||
babel-generator "^6.26.0"
|
||||
|
|
@ -389,24 +396,24 @@ babel-core@^6.26.0:
|
|||
babel-traverse "^6.26.0"
|
||||
babel-types "^6.26.0"
|
||||
babylon "^6.18.0"
|
||||
convert-source-map "^1.5.0"
|
||||
debug "^2.6.8"
|
||||
convert-source-map "^1.5.1"
|
||||
debug "^2.6.9"
|
||||
json5 "^0.5.1"
|
||||
lodash "^4.17.4"
|
||||
minimatch "^3.0.4"
|
||||
path-is-absolute "^1.0.1"
|
||||
private "^0.1.7"
|
||||
private "^0.1.8"
|
||||
slash "^1.0.0"
|
||||
source-map "^0.5.6"
|
||||
source-map "^0.5.7"
|
||||
|
||||
babel-eslint@^8.2.1:
|
||||
version "8.2.2"
|
||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.2.tgz#1102273354c6f0b29b4ea28a65f97d122296b68b"
|
||||
version "8.2.3"
|
||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf"
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0-beta.40"
|
||||
"@babel/traverse" "^7.0.0-beta.40"
|
||||
"@babel/types" "^7.0.0-beta.40"
|
||||
babylon "^7.0.0-beta.40"
|
||||
"@babel/code-frame" "7.0.0-beta.44"
|
||||
"@babel/traverse" "7.0.0-beta.44"
|
||||
"@babel/types" "7.0.0-beta.44"
|
||||
babylon "7.0.0-beta.44"
|
||||
eslint-scope "~3.7.1"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
|
||||
|
|
@ -550,8 +557,8 @@ babel-helpers@^6.24.1:
|
|||
babel-template "^6.24.1"
|
||||
|
||||
babel-loader@^7.1.2:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.3.tgz#ff5b440da716e9153abb946251a9ab7670037b16"
|
||||
version "7.1.4"
|
||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015"
|
||||
dependencies:
|
||||
find-cache-dir "^1.0.0"
|
||||
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"
|
||||
to-fast-properties "^1.0.3"
|
||||
|
||||
babylon@7.0.0-beta.40, babylon@^7.0.0-beta.40:
|
||||
version "7.0.0-beta.40"
|
||||
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.40.tgz#91fc8cd56d5eb98b28e6fde41045f2957779940a"
|
||||
babylon@7.0.0-beta.44:
|
||||
version "7.0.0-beta.44"
|
||||
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
|
||||
|
||||
babylon@^6.18.0:
|
||||
version "6.18.0"
|
||||
|
|
@ -1413,7 +1420,15 @@ chalk@^1.1.3:
|
|||
strip-ansi "^3.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"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
|
||||
dependencies:
|
||||
|
|
@ -1646,7 +1661,7 @@ content-type@~1.0.4:
|
|||
version "1.0.4"
|
||||
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"
|
||||
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"
|
||||
|
||||
core-js@^2.4.0, core-js@^2.5.0:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
|
||||
version "2.5.7"
|
||||
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:
|
||||
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:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^3.0.1, debug@^3.1.0:
|
||||
debug@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
dependencies:
|
||||
|
|
@ -2916,10 +2931,14 @@ global@~4.3.0:
|
|||
min-document "^2.19.0"
|
||||
process "~0.5.1"
|
||||
|
||||
globals@^11.0.1, globals@^11.1.0:
|
||||
globals@^11.0.1:
|
||||
version "11.3.0"
|
||||
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:
|
||||
version "9.18.0"
|
||||
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"
|
||||
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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
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"
|
||||
|
||||
invariant@^2.2.0, invariant@^2.2.2:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688"
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
|
|
@ -3863,10 +3888,14 @@ lodash.uniq@^4.5.0:
|
|||
version "4.5.0"
|
||||
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"
|
||||
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:
|
||||
version "1.6.1"
|
||||
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"
|
||||
|
||||
make-dir@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
|
||||
dependencies:
|
||||
pify "^3.0.0"
|
||||
|
||||
|
|
@ -4895,7 +4924,7 @@ preserve@^0.2.0:
|
|||
version "0.2.0"
|
||||
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"
|
||||
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"
|
||||
|
||||
react-dom@^16.2.0:
|
||||
version "16.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044"
|
||||
version "16.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e"
|
||||
dependencies:
|
||||
fbjs "^0.8.16"
|
||||
loose-envify "^1.1.0"
|
||||
|
|
@ -5138,8 +5167,8 @@ react-transition-group@^2.2.1:
|
|||
warning "^3.0.0"
|
||||
|
||||
react@^16.2.0:
|
||||
version "16.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
|
||||
version "16.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585"
|
||||
dependencies:
|
||||
fbjs "^0.8.16"
|
||||
loose-envify "^1.1.0"
|
||||
|
|
@ -5471,6 +5500,10 @@ safe-regex@^1.1.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
|
|
@ -5914,12 +5947,18 @@ supports-color@^4.2.1:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0"
|
||||
dependencies:
|
||||
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:
|
||||
version "0.7.2"
|
||||
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"
|
||||
|
||||
ua-parser-js@^0.7.9:
|
||||
version "0.7.17"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
|
||||
version "0.7.18"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
|
||||
|
||||
uglify-js@^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"
|
||||
|
||||
whatwg-fetch@>=0.10.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
|
||||
|
||||
whet.extend@~0.9.9:
|
||||
version "0.9.9"
|
||||
|
|
|
|||
|
|
@ -32,22 +32,14 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"encoding/json"
|
||||
"github.com/elastic/gosigar"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/mohae/deepcopy"
|
||||
"golang.org/x/net/websocket"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -111,11 +103,6 @@ func New(config *Config, commit string, logdir string) (*Dashboard, error) {
|
|||
DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh),
|
||||
DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh),
|
||||
},
|
||||
Logs: &LogsMessage{
|
||||
Stream: true,
|
||||
End: false,
|
||||
Chunk: json.RawMessage("[]"),
|
||||
},
|
||||
},
|
||||
logdir: logdir,
|
||||
}, nil
|
||||
|
|
@ -267,101 +254,6 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
|||
}
|
||||
}
|
||||
|
||||
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.
|
||||
func (db *Dashboard) handleLogRequest(r *LogsRequest, c *client) {
|
||||
files, err := ioutil.ReadDir(db.logdir)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open logdir", "logdir", db.logdir, "err", err)
|
||||
return
|
||||
}
|
||||
re := regexp.MustCompile(".log$")
|
||||
fileNames := make([]string, len(files))
|
||||
n := 0
|
||||
for _, f := range files {
|
||||
if f.Mode().IsRegular() && re.Match([]byte(f.Name())) {
|
||||
fileNames[n] = f.Name()
|
||||
n++
|
||||
}
|
||||
}
|
||||
n-- // The last file is handled by the stream handler in order to avoid log duplication on the client side.
|
||||
if n < 1 {
|
||||
log.Warn("There isn't any old log file in the logdir", "path", db.logdir)
|
||||
return
|
||||
}
|
||||
timestamp := fmt.Sprintf("%s.log", strings.Replace(r.Time.Format("060102150405.00"), ".", "", 1))
|
||||
|
||||
i := sort.Search(n, func(i int) bool {
|
||||
return fileNames[i] >= timestamp // Returns the smallest index such as fileNames[i] >= timestamp.
|
||||
})
|
||||
ok := false
|
||||
var buf json.RawMessage
|
||||
if r.Past {
|
||||
if i >= n {
|
||||
i = n - 1
|
||||
}
|
||||
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 {
|
||||
|
|
@ -491,172 +383,6 @@ func (db *Dashboard) collectData() {
|
|||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// This could be embedded into collectData, but they shouldn't depend on each other, and also cleaner this way.
|
||||
func (db *Dashboard) streamLogs() {
|
||||
defer db.wg.Done()
|
||||
|
||||
files, err := ioutil.ReadDir(db.logdir)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open logdir", "logdir", 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$")
|
||||
var i int
|
||||
for i = len(files) - 1; i >= 0 && (!files[i].Mode().IsRegular() || !re.Match([]byte(files[i].Name()))); i-- {
|
||||
}
|
||||
if i >= 0 {
|
||||
if opened, err = os.OpenFile(filepath.Join(db.logdir, files[i].Name()), os.O_RDONLY, 0644); err != nil {
|
||||
log.Warn("Failed to open file", "name", files[i].Name(), "err", err)
|
||||
return
|
||||
}
|
||||
if buf, err = ioutil.ReadAll(opened); err != nil {
|
||||
log.Warn("Failed to read file", "name", opened.Name(), "err", err)
|
||||
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)
|
||||
if err != nil {
|
||||
log.Warn("Failed to add logdir to fs watcher", "logdir", db.logdir, "err", err)
|
||||
return
|
||||
}
|
||||
defer opened.Close() // Close the lastly opened file.
|
||||
ticker := time.NewTicker(db.config.Refresh)
|
||||
defer ticker.Stop()
|
||||
|
||||
newFile := false
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
// If new log file was created.
|
||||
if event.Op&fsnotify.Create != 0 && re.Match([]byte(event.Name)) {
|
||||
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)
|
||||
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)
|
||||
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:
|
||||
if err != nil {
|
||||
log.Warn("Fs watcher error", "err", err)
|
||||
}
|
||||
return
|
||||
case errc := <-db.quit:
|
||||
errc <- nil
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendToAll sends the given message to the active dashboards.
|
||||
func (db *Dashboard) sendToAll(msg *Message) {
|
||||
db.lock.Lock()
|
||||
|
|
|
|||
310
dashboard/log.go
Normal file
310
dashboard/log.go
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
// 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 (
|
||||
"encoding/json"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/mohae/deepcopy"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// embrace inserts buf into brackets.
|
||||
func embrace(buf []byte) []byte {
|
||||
b := make([]byte, len(buf)+2)
|
||||
b[0] = '['
|
||||
copy(b[1:], buf)
|
||||
b[len(buf)+1] = ']'
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// replaceNewLinesWithCommas replaces the '\n' characters with ',' characters and returns the last replaced position.
|
||||
func replaceNewLinesWithCommas(buf []byte) int {
|
||||
last := -1
|
||||
for i := 0; i < len(buf); i++ {
|
||||
if buf[i] == '\n' {
|
||||
buf[i] = ','
|
||||
last = i
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
// 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, len(files))
|
||||
n := 0
|
||||
for _, f := range files {
|
||||
if f.Mode().IsRegular() && re.Match([]byte(f.Name())) {
|
||||
fileNames[n] = f.Name()
|
||||
n++
|
||||
}
|
||||
}
|
||||
if n < 1 {
|
||||
log.Warn("There isn't any log file in the logdir", "path", db.logdir)
|
||||
return
|
||||
}
|
||||
i := sort.Search(n, func(i int) bool {
|
||||
return fileNames[i] >= r.Name // Returns the smallest index such as fileNames[i] >= timestamp.
|
||||
})
|
||||
|
||||
if i >= n || fileNames[i] != r.Name {
|
||||
log.Warn("The requested file isn't in the logdir", "path", filepath.Join(db.logdir, r.Name))
|
||||
return
|
||||
}
|
||||
|
||||
last := false
|
||||
if r.Past {
|
||||
if i <= 0 {
|
||||
log.Warn("There isn't more log file in the logdir", "path", db.logdir)
|
||||
return
|
||||
}
|
||||
i--
|
||||
if i == 0 {
|
||||
last = true
|
||||
}
|
||||
} else {
|
||||
if i >= n-1 {
|
||||
log.Warn("There isn't more log file in the logdir", "path", db.logdir)
|
||||
return
|
||||
}
|
||||
if i == n-2 {
|
||||
// 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
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
path := filepath.Join(db.logdir, fileNames[i])
|
||||
f, err := os.OpenFile(path, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
log.Warn("Failed to open file", "path", path, "err", err)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
lastComma := replaceNewLinesWithCommas(buf)
|
||||
if lastComma < 0 {
|
||||
log.Warn("The file doesn't contain valid logs", "path", path)
|
||||
return
|
||||
}
|
||||
db.lock.Lock()
|
||||
c.msg <- &Message{
|
||||
Logs: &LogsMessage{
|
||||
Old: &LogFile{
|
||||
Name: fileNames[i],
|
||||
Past: r.Past,
|
||||
Last: last,
|
||||
},
|
||||
Chunk: embrace(buf[:lastComma]),
|
||||
},
|
||||
}
|
||||
db.lock.Unlock()
|
||||
}
|
||||
|
||||
// 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.
|
||||
// This could be embedded into collectData, but they shouldn't depend on each other, and also cleaner this way.
|
||||
func (db *Dashboard) streamLogs() {
|
||||
defer db.wg.Done()
|
||||
|
||||
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$")
|
||||
var i int
|
||||
for i = len(files) - 1; i >= 0 && (!files[i].Mode().IsRegular() || !re.Match([]byte(files[i].Name()))); i-- {
|
||||
}
|
||||
if i < 0 {
|
||||
log.Warn("There isn't any log file in the logdir", "path", db.logdir)
|
||||
return
|
||||
}
|
||||
if opened, err = os.OpenFile(filepath.Join(db.logdir, files[i].Name()), os.O_RDONLY, 0644); 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{
|
||||
Old: &LogFile{
|
||||
Name: fi.Name(),
|
||||
Past: false,
|
||||
Last: true,
|
||||
},
|
||||
Chunk: json.RawMessage("[]"),
|
||||
}
|
||||
db.lock.Unlock()
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
log.Warn("Failed to add logdir to fs watcher", "logdir", db.logdir, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(db.config.Refresh)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
// Make sure that new log file was created.
|
||||
if event.Op&fsnotify.Create == 0 || !re.Match([]byte(event.Name)) {
|
||||
break
|
||||
}
|
||||
if opened == nil {
|
||||
log.Warn("The last log file is not opened")
|
||||
return
|
||||
}
|
||||
// The new log file's name is always greater, because it is created using the actual log record's time.
|
||||
if opened.Name() >= event.Name {
|
||||
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)
|
||||
return
|
||||
}
|
||||
b := make([]byte, len(buf)+len(chunk))
|
||||
copy(b, buf)
|
||||
copy(b[len(buf):], chunk)
|
||||
buf = b
|
||||
opened.Close()
|
||||
|
||||
if last := replaceNewLinesWithCommas(buf); last >= 0 {
|
||||
// Send the rest of the previously opened file.
|
||||
db.sendToAll(&Message{
|
||||
Logs: &LogsMessage{
|
||||
Chunk: embrace(buf[:last]),
|
||||
},
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
db.lock.Lock()
|
||||
db.history.Logs.Old.Name = fi.Name()
|
||||
db.history.Logs.Chunk = json.RawMessage("[]")
|
||||
db.lock.Unlock()
|
||||
case err := <-watcher.Errors:
|
||||
if err != nil {
|
||||
log.Warn("Fs watcher error", "err", err)
|
||||
}
|
||||
return
|
||||
case errc := <-db.quit:
|
||||
errc <- nil
|
||||
return
|
||||
// Send log updates to the client.
|
||||
case <-ticker.C:
|
||||
if opened == nil {
|
||||
log.Warn("The last log file is not opened")
|
||||
return
|
||||
}
|
||||
|
||||
// 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 := replaceNewLinesWithCommas(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:]
|
||||
|
||||
msg := embrace(b[:last])
|
||||
var l *LogsMessage
|
||||
// Update the history.
|
||||
db.lock.Lock()
|
||||
if len(db.history.Logs.Chunk) == 2 {
|
||||
db.history.Logs.Chunk = msg
|
||||
l = deepcopy.Copy(db.history.Logs).(*LogsMessage)
|
||||
} 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
|
||||
l = &LogsMessage{Chunk: msg}
|
||||
}
|
||||
db.lock.Unlock()
|
||||
|
||||
db.sendToAll(&Message{Logs: l})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -71,17 +71,21 @@ type SystemMessage struct {
|
|||
}
|
||||
|
||||
type LogsMessage struct {
|
||||
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.
|
||||
Old *LogFile `json:"old,omitempty"` // Attributes of the log file.
|
||||
Chunk json.RawMessage `json:"chunk"` // Contains log records.
|
||||
}
|
||||
|
||||
type LogFile struct {
|
||||
Name string `json:"name"` // The name of the file.
|
||||
Past bool `json:"past"` // Denotes if the file is the previous or the next one.
|
||||
Last bool `json:"last"` // Denotes if there isn't more file.
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Logs *LogsRequest `json:"logs,omitempty"`
|
||||
}
|
||||
|
||||
type LogsRequest struct {
|
||||
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.
|
||||
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.
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue