From ea3eb80c07e5650dff93bfdd33a2e9fee67a9fc0 Mon Sep 17 00:00:00 2001 From: Kyrylo Gliebov Date: Thu, 9 Aug 2018 18:26:16 +0200 Subject: [PATCH] Add correlated Logs view --- src/player.jsx | 3 + src/recordings.css | 16 ++++ src/recordings.jsx | 198 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 215 insertions(+), 2 deletions(-) diff --git a/src/player.jsx b/src/player.jsx index 929ab5d..b8a77f8 100644 --- a/src/player.jsx +++ b/src/player.jsx @@ -765,6 +765,9 @@ } } + /* Send packet ts to the top */ + this.props.onTsChange(this.pkt.pos); + /* Output the packet */ if (this.pkt.is_io && !this.pkt.is_output) { this.sendInput(this.pkt); diff --git a/src/recordings.css b/src/recordings.css index e9293cf..2d27ab4 100644 --- a/src/recordings.css +++ b/src/recordings.css @@ -366,3 +366,19 @@ table.listing-ct > thead th:last-child, tr.listing-ct-item td:last-child { -ms-user-select: none; user-select: none; } + +.highlighted { + background-color: #ededed !important; +} + +#logs-view { + height: 300px; + overflow-y: scroll; + margin-bottom: 0; +} + +.logs-view-log-time { + display: inline-block; + width: 150px; + vertical-align: middle; +} diff --git a/src/recordings.jsx b/src/recordings.jsx index 3153ee4..25c7e79 100644 --- a/src/recordings.jsx +++ b/src/recordings.jsx @@ -243,6 +243,184 @@ } } + function LogElement(props) { + const entry = props.entry; + const start = props.start; + const end = props.end; + const entry_timestamp = entry.__REALTIME_TIMESTAMP / 1000; + let className = 'cockpit-logline'; + if (start < entry_timestamp && end > entry_timestamp) { + className = 'cockpit-logline highlighted'; + } + return ( +
+
+ +
+
{formatDateTime(parseInt(entry.__REALTIME_TIMESTAMP / 1000))}
+ {entry.MESSAGE} +
+ ); + } + + function LogsView(props) { + const entries = props.entries; + const start = props.start; + const end = props.end; + const rows = entries.map((entry) => + + ); + return ( +
+ {rows} +
+ ); + } + + let Logs = class extends React.Component { + constructor(props) { + super(props); + this.journalctlError = this.journalctlError.bind(this); + this.journalctlIngest = this.journalctlIngest.bind(this); + this.journalctlPrepend = this.journalctlPrepend.bind(this); + this.getLogs = this.getLogs.bind(this); + this.loadLater = this.loadLater.bind(this); + this.loadEarlier = this.loadEarlier.bind(this); + this.loadForTs = this.loadForTs.bind(this); + this.journalCtl = null; + this.entries = []; + this.start = null; + this.end = null; + this.earlier_than = null; + this.load_earlier = false; + this.state = { + cursor: null, + after: null, + entries: [], + }; + } + + scrollToTop() { + const logs_view = document.getElementById("logs-view"); + logs_view.scrollTop = 0; + } + + scrollToBottom() { + const logs_view = document.getElementById("logs-view"); + logs_view.scrollTop = logs_view.scrollHeight; + } + + journalctlError(error) { + console.warn(cockpit.message(error)); + } + + journalctlIngest(entryList) { + if (this.load_earlier === true) { + entryList.push(...this.entries); + this.entries = entryList; + this.setState({entries: this.entries}); + this.load_earlier = false; + this.scrollToTop(); + } else { + if (entryList.length > 0) { + this.entries.push(...entryList); + const after = this.entries[this.entries.length - 1].__CURSOR; + this.setState({entries: this.entries, after: after}); + this.scrollToBottom(); + } + } + } + + journalctlPrepend(entryList) { + entryList.push(...this.entries); + this.setState({entries: this.entries}); + } + + getLogs() { + if (this.start != null && this.end != null) { + if (this.journalCtl != null) { + this.journalCtl.stop(); + this.journalCtl = null; + } + + let matches = []; + + let options = { + since: formatDateTime(this.start), + until: formatDateTime(this.end), + follow: false, + count: "all", + }; + + if (this.load_earlier === true) { + options["until"] = formatDateTime(this.earlier_than); + } else if (this.state.after != null) { + options["after"] = this.state.after; + delete options.since; + } + + const self = this; + this.journalCtl = Journal.journalctl(matches, options) + .fail(this.journalctlError) + .done(function(data) { + self.journalctlIngest(data); + }); + } + } + + loadEarlier() { + this.load_earlier = true; + this.start = this.start - 3600; + this.getLogs(); + } + + loadLater() { + this.start = this.end; + this.end = this.end + 3600; + this.getLogs(); + } + + loadForTs(ts) { + this.end = this.start + ts; + this.getLogs(); + } + + componentDidUpdate() { + if (this.props.recording) { + if (this.start === null && this.end === null) { + this.end = this.props.recording.start + 3600; + this.start = this.props.recording.start; + this.earlier_than = this.props.recording.start; + } + this.getLogs(); + } + if (this.props.curTs) { + const ts = this.props.curTs; + this.loadForTs(ts); + } + } + + render() { + if (this.props.recording) { + return ( +
+
+ Logs + +
+ +
+ +
+
+ ); + } else { + return (
Loading...
); + } + } + } + /* * A component representing a single recording view. * Properties: @@ -303,7 +481,8 @@ let player = (); + matchList={this.props.recording.matchList} + onTsChange={this.props.onTsChange} />); return (
@@ -548,6 +727,7 @@ this.handleDateUntilChange = this.handleDateUntilChange.bind(this); this.handleUsernameChange = this.handleUsernameChange.bind(this); this.handleHostnameChange = this.handleHostnameChange.bind(this); + this.handleTsChange = this.handleTsChange.bind(this); /* Journalctl instance */ this.journalctl = null; /* Recording ID journalctl instance is invoked with */ @@ -570,6 +750,7 @@ hostname: cockpit.location.options.hostname || null, error_tlog_uid: false, diff_hosts: false, + curTs: null, } } @@ -766,6 +947,10 @@ cockpit.location.go([], $.extend(cockpit.location.options, { hostname: hostname })); } + handleTsChange(ts) { + this.setState({curTs: ts}); + } + componentDidMount() { let proc = cockpit.spawn(["getent", "passwd", "tlog"]); @@ -842,7 +1027,16 @@ ); } else { return ( - +
+ +
+
+
+ +
+
+
+
); } }