dashboard, vendor: first fully working version (without rebase)

This commit is contained in:
Kurkó Mihály 2018-05-25 12:35:27 +03:00
parent 9a1ee019d1
commit 5efc9b99d1
14 changed files with 2298 additions and 1850 deletions

File diff suppressed because one or more lines are too long

View file

@ -68,4 +68,4 @@ export const styles = {
light: { light: {
color: 'rgba(255, 255, 255, 0.54)', color: 'rgba(255, 255, 255, 0.54)',
}, },
} };

View file

@ -37,8 +37,7 @@ export type Props = {
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>
); );

View file

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

View file

@ -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&nbsp;';
color = '#4c8f0f';
break;
case 'warn':
lvl = 'WARN&nbsp;';
color = '#b79a22';
break;
case 'error':
case 'eror':
lvl = 'ERROR';
color = '#754b70';
break;
case 'crit':
lvl = 'CRIT&nbsp;';
color = '#ce3c23';
break;
default:
lvl = '';
}
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 += '&nbsp;'.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}${'&nbsp;'.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: {
@ -195,7 +97,11 @@ const defaultContent: Content = {
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.
@ -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>
); );

View file

@ -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&nbsp;';
color = '#4c8f0f';
break;
case 'warn':
lvl = 'WARN&nbsp;';
color = '#b79a22';
break;
case 'error':
case 'eror':
lvl = 'ERROR';
color = '#754b70';
break;
case 'crit':
lvl = 'CRIT&nbsp;';
color = '#ce3c23';
break;
default:
lvl = '';
}
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 += '&nbsp;'.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}${'&nbsp;'.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>
); );
} }

View file

@ -51,32 +51,23 @@ export type Props = {
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");
} }
if (this.container.scrollHeight - this.container.scrollTop === this.container.clientHeight) {
console.log("Bottom");
// this.container.scrollTop = 0;
} }
onScroll = () => {
if (this.content && typeof this.content.onScroll === 'function') {
this.content.onScroll();
} }
}; };
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>

View file

@ -66,7 +66,7 @@ export type System = {
}; };
export type Record = { export type Record = {
t: Object, t: string,
lvl: Object, lvl: Object,
msg: string, msg: string,
ctx: Array<string> ctx: Array<string>
@ -74,10 +74,21 @@ export type Record = {
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>,
}; };

View file

@ -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,71 +257,122 @@ 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 {
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{ c.msg <- &Message{
Logs: &LogsMessage{ Logs: &LogsMessage{
Stream: false,
Past: r.Past,
End: !ok,
Chunk: b, Chunk: b,
}, },
} }
db.lock.Unlock() 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
}
} }
// collectData collects the required data to plot on the dashboard. // collectData collects the required data to plot on the dashboard.
@ -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,15 +559,8 @@ 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()
} }
}
case err := <-watcher.Errors:
if err != nil {
log.Warn("Fs watcher error", "err", err)
}
return
// Send log updates to the client.
case <-time.After(db.config.Refresh):
last := -1 last := -1
for i := 0; i < len(buf); i++ { for i := 0; i < len(buf); i++ {
if buf[i] == '\n' { if buf[i] == '\n' {
@ -519,41 +569,90 @@ func (db *Dashboard) streamLogs() {
} }
} }
if last >= 0 { if last >= 0 {
b := make([]byte, last+2) msg := make([]byte, last+2)
b[0] = '[' msg[0] = '['
copy(b[1:], buf[:last]) copy(msg[1:], buf[:last])
b[last+1] = ']' msg[last+1] = ']'
db.sendToAll(&Message{ db.sendToAll(&Message{
Logs: &LogsMessage{ Logs: &LogsMessage{
Chunk: b, Stream: true,
End: false,
Chunk: msg,
}, },
}) })
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.lock.Lock()
db.history.Logs.Chunk = b[:n] db.history.Logs.Chunk = json.RawMessage("[]")
db.lock.Unlock() db.lock.Unlock()
// Clear the valid/sent part of the buffer.
buf = buf[last+1:]
} }
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: 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()
} }
} }
} }

View file

@ -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
View 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
View file

@ -0,0 +1,8 @@
deepCopy
========
[![GoDoc](https://godoc.org/github.com/mohae/deepcopy?status.svg)](https://godoc.org/github.com/mohae/deepcopy)[![Build Status](https://travis-ci.org/mohae/deepcopy.png)](https://travis-ci.org/mohae/deepcopy)
DeepCopy makes deep copies of things: unexported field values are not copied.
## Usage
cpy := deepcopy.Copy(orig)

125
vendor/github.com/mohae/deepcopy/deepcopy.go generated vendored Normal file
View 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
View file

@ -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",