From 83b2bff85024f28a6ea35ec8a3863cde5e807052 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Mon, 9 Nov 2015 23:16:44 -0800 Subject: [PATCH] Improved log performance and reliability Signed-off-by: Jeffrey Morgan --- src/actions/ContainerActions.js | 4 + src/actions/ContainerServerActions.js | 4 +- src/components/ContainerHomeLogs.react.js | 80 ++++++------------- src/components/ContainerListItem.react.js | 2 +- src/components/ContainerLogs.react.js | 61 --------------- src/stores/ContainerStore.js | 35 ++++++++- src/stores/LogStore.js | 85 -------------------- src/utils/DockerUtil.js | 94 ++++++++++++++++++++++- styles/container-home.less | 8 +- 9 files changed, 160 insertions(+), 213 deletions(-) delete mode 100644 src/components/ContainerLogs.react.js delete mode 100644 src/stores/LogStore.js diff --git a/src/actions/ContainerActions.js b/src/actions/ContainerActions.js index 2bcbdf88..a9c2f4a6 100644 --- a/src/actions/ContainerActions.js +++ b/src/actions/ContainerActions.js @@ -38,6 +38,10 @@ class ContainerActions { run (name, repo, tag) { dockerUtil.run(name, repo, tag); } + + active (name) { + dockerUtil.active(name); + } } export default alt.createActions(ContainerActions); diff --git a/src/actions/ContainerServerActions.js b/src/actions/ContainerServerActions.js index 7308349f..59acb37c 100644 --- a/src/actions/ContainerServerActions.js +++ b/src/actions/ContainerServerActions.js @@ -15,7 +15,9 @@ class ContainerServerActions { 'updated', 'waiting', 'kill', - 'stopped' + 'stopped', + 'log', + 'logs' ); } } diff --git a/src/components/ContainerHomeLogs.react.js b/src/components/ContainerHomeLogs.react.js index 0853f986..bffe4157 100644 --- a/src/components/ContainerHomeLogs.react.js +++ b/src/components/ContainerHomeLogs.react.js @@ -1,74 +1,44 @@ import $ from 'jquery'; import React from 'react/addons'; -import LogStore from '../stores/LogStore'; import Router from 'react-router'; -import metrics from '../utils/MetricsUtil'; +import containerActions from '../actions/ContainerActions'; +import Convert from 'ansi-to-html'; -var _prevBottom = 0; +let escape = function (html) { + var text = document.createTextNode(html); + var div = document.createElement('div'); + div.appendChild(text); + return div.innerHTML; +}; + +let convert = new Convert(); +let prevBottom = 0; module.exports = React.createClass({ - mixins: [Router.Navigation], - getInitialState: function () { - return { - logs: [] - }; - }, - componentDidMount: function() { - if (!this.props.container) { - return; - } - this.update(); - this.scrollToBottom(); - LogStore.on(LogStore.SERVER_LOGS_EVENT, this.update); - LogStore.fetch(this.props.container.Name); + + componentDidUpdate: function () { + var node = $('.logs').get()[0]; + node.scrollTop = node.scrollHeight; }, componentWillReceiveProps: function (nextProps) { if (this.props.container && nextProps.container && this.props.container.Name !== nextProps.container.Name) { - LogStore.detach(this.props.container.Name); - LogStore.fetch(nextProps.container.Name); + containerActions.active(nextProps.container.Name); } }, - componentWillUnmount: function() { - if (!this.props.container) { - return; - } + componentDidMount: function () { + containerActions.active(this.props.container.Name); + }, - LogStore.detach(this.props.container.Name); - LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.update); - }, - componentDidUpdate: function () { - this.scrollToBottom(); - }, - scrollToBottom: function () { - var parent = $('.logs'); - if (parent[0].scrollHeight - parent.height() >= _prevBottom - 50) { - parent.scrollTop(parent[0].scrollHeight - parent.height()); - } - _prevBottom = parent[0].scrollHeight - parent.height(); - }, - handleClickLogs: function () { - metrics.track('Viewed Logs', { - from: 'preview' - }); - this.context.router.transitionTo('containerLogs', {name: this.props.container.Name}); - }, - update: function () { - if (!this.props.container) { - return; - } - this.setState({ - logs: LogStore.logs(this.props.container.Name) - }); + componentWillUnmount: function () { + containerActions.active(null); }, + render: function () { - var logs = this.state.logs.map(function (l, i) { - return ; - }); - if (logs.length === 0) { - logs = "No logs for this container."; - } + let logs = this.props.container.Logs ? + this.props.container.Logs.map((l) =>
'))}}>
) : + ['0 No logs for this container.']; return (
diff --git a/src/components/ContainerListItem.react.js b/src/components/ContainerListItem.react.js index 589ff6ef..76c9d886 100644 --- a/src/components/ContainerListItem.react.js +++ b/src/components/ContainerListItem.react.js @@ -96,7 +96,7 @@ var ContainerListItem = React.createClass({ return ( -
  • +
  • {state}
    diff --git a/src/components/ContainerLogs.react.js b/src/components/ContainerLogs.react.js deleted file mode 100644 index 7da5e841..00000000 --- a/src/components/ContainerLogs.react.js +++ /dev/null @@ -1,61 +0,0 @@ -import $ from 'jquery'; -import React from 'react/addons'; -import LogStore from '../stores/LogStore'; - -var _prevBottom = 0; - -module.exports = React.createClass({ - getInitialState: function () { - return { - logs: [] - }; - }, - componentDidMount: function() { - if (!this.props.container) { - return; - } - this.update(); - this.scrollToBottom(); - LogStore.on(LogStore.SERVER_LOGS_EVENT, this.update); - LogStore.fetch(this.props.container.Name); - }, - componentWillUnmount: function() { - if (!this.props.container) { - return; - } - - LogStore.detach(this.props.container.Name); - LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.update); - }, - componentDidUpdate: function () { - this.scrollToBottom(); - }, - scrollToBottom: function () { - var parent = $('.details-logs'); - if (parent.scrollTop() >= _prevBottom - 50) { - parent.scrollTop(parent[0].scrollHeight - parent.height()); - } - _prevBottom = parent[0].scrollHeight - parent.height(); - }, - update: function () { - if (!this.props.container) { - return; - } - this.setState({ - logs: LogStore.logs(this.props.container.Name) - }); - }, - render: function () { - var logs = this.state.logs.map(function (l, i) { - return ; - }); - if (logs.length === 0) { - logs = "No logs for this container."; - } - return ( -
    - {logs} -
    - ); - } -}); diff --git a/src/stores/ContainerStore.js b/src/stores/ContainerStore.js index 28a6e306..46ddfce8 100644 --- a/src/stores/ContainerStore.js +++ b/src/stores/ContainerStore.js @@ -3,6 +3,8 @@ import alt from '../alt'; import containerServerActions from '../actions/ContainerServerActions'; import containerActions from '../actions/ContainerActions'; +let MAX_LOG_SIZE = 3000; + class ContainerStore { constructor () { this.bindActions(containerActions); @@ -102,10 +104,8 @@ class ContainerStore { if (containers[container.Name] && containers[container.Name].State.Updating) { return; } - // Trigger log update - // TODO: fix this loading multiple times - // LogStore.fetch(container.Name); + container.Logs = containers[container.Name].Logs; containers[container.Name] = container; this.setState({containers}); @@ -141,7 +141,7 @@ class ContainerStore { } } - waiting({name, waiting}) { + waiting ({name, waiting}) { let containers = this.containers; if (containers[name]) { containers[name].State.Waiting = waiting; @@ -158,6 +158,33 @@ class ContainerStore { this.setState({pending: null}); } + log ({name, entry}) { + let container = this.containers[name]; + if (!container) { + return; + } + + if (!container.Logs) { + container.Logs = []; + } + + container.Logs.push.apply(container.Logs, entry.split('\n').filter(e => e.length)); + container.Logs = container.Logs.slice(container.Logs.length - MAX_LOG_SIZE, MAX_LOG_SIZE); + this.emitChange(); + } + + logs ({name, logs}) { + let container = this.containers[name]; + + if (!container) { + return; + } + + container.Logs = logs.split('\n'); + container.Logs = container.Logs.slice(container.Logs.length - MAX_LOG_SIZE, MAX_LOG_SIZE); + this.emitChange(); + } + static generateName (repo) { const base = _.last(repo.split('/')); const names = _.keys(this.getState().containers); diff --git a/src/stores/LogStore.js b/src/stores/LogStore.js deleted file mode 100644 index 89ed09ce..00000000 --- a/src/stores/LogStore.js +++ /dev/null @@ -1,85 +0,0 @@ -import {EventEmitter} from 'events'; -import assign from 'object-assign'; -import Convert from 'ansi-to-html'; -import docker from '../utils/DockerUtil'; -import stream from 'stream'; - -var _convert = new Convert(); -var _logs = {}; -var _streams = {}; - -var MAX_LOG_SIZE = 3000; - -module.exports = assign(Object.create(EventEmitter.prototype), { - SERVER_LOGS_EVENT: 'server_logs_event', - _escape: function (html) { - var text = document.createTextNode(html); - var div = document.createElement('div'); - div.appendChild(text); - return div.innerHTML; - }, - fetch: function (name) { - if (!name || !docker.client) { - return; - } - docker.client.getContainer(name).logs({ - stdout: true, - stderr: true, - timestamps: false, - tail: MAX_LOG_SIZE, - follow: false - }, (err, logStream) => { - if (err) { - return; - } - var logs = []; - var outstream = new stream.PassThrough(); - docker.client.modem.demuxStream(logStream, outstream, outstream); - outstream.on('data', (chunk) => { - logs.push(_convert.toHtml(this._escape(chunk))); - }); - logStream.on('end', () => { - _logs[name] = logs; - this.emit(this.SERVER_LOGS_EVENT); - this.attach(name); - }); - }); - }, - attach: function (name) { - if (!name || !docker.client || _streams[name]) { - return; - } - docker.client.getContainer(name).attach({ - stdout: true, - stderr: true, - logs: false, - stream: true - }, (err, logStream) => { - if (err) { - return; - } - _streams[name] = logStream; - var outstream = new stream.PassThrough(); - docker.client.modem.demuxStream(logStream, outstream, outstream); - outstream.on('data', (chunk) => { - _logs[name].push(_convert.toHtml(this._escape(chunk))); - if (_logs[name].length > MAX_LOG_SIZE) { - _logs[name] = _logs[name].slice(_logs[name].length - MAX_LOG_SIZE, MAX_LOG_SIZE); - } - this.emit(this.SERVER_LOGS_EVENT); - }); - logStream.on('end', () => { - this.detach(name); - }); - }); - }, - detach: function (name) { - if (_streams[name]) { - _streams[name].destroy(); - delete _streams[name]; - } - }, - logs: function (name) { - return _logs[name] || []; - } -}); diff --git a/src/utils/DockerUtil.js b/src/utils/DockerUtil.js index 7091482f..c3f1c5f7 100644 --- a/src/utils/DockerUtil.js +++ b/src/utils/DockerUtil.js @@ -7,13 +7,15 @@ import util from './Util'; import hubUtil from './HubUtil'; import metrics from '../utils/MetricsUtil'; import containerServerActions from '../actions/ContainerServerActions'; -import Promise from 'bluebird'; import rimraf from 'rimraf'; +import stream from 'stream'; export default { host: null, client: null, placeholders: {}, + streams: {}, + activeContainerName: null, setup (ip, name) { if (!ip || !name) { @@ -343,6 +345,85 @@ export default { }); }, + active (name) { + this.detach(); + this.activeContainerName = name; + + if (name) { + this.logs(); + } + }, + + logs () { + if (!this.activeContainerName) { + return; + } + + this.client.getContainer(this.activeContainerName).logs({ + stdout: true, + stderr: true, + tail: 1000, + follow: false, + timestamps: 1 + }, (err, logStream) => { + if (err) { + return; + } + + let logs = ''; + logStream.setEncoding('utf8'); + logStream.on('data', chunk => logs += chunk); + logStream.on('end', () => { + containerServerActions.logs({name: this.activeContainerName, logs}); + this.attach(); + }); + }); + }, + + attach () { + if (!this.activeContainerName) { + return; + } + + this.client.getContainer(this.activeContainerName).logs({ + stdout: true, + stderr: true, + tail: 0, + follow: true, + timestamps: 1 + }, (err, logStream) => { + if (err) { + return; + } + + if (this.stream) { + this.detach(); + } + this.stream = logStream; + + let timeout = null; + let batch = ''; + logStream.setEncoding('utf8'); + logStream.on('data', (chunk) => { + batch += chunk; + if (!timeout) { + timeout = setTimeout(() => { + containerServerActions.log({name: this.activeContainerName, entry: batch}); + timeout = null; + batch = ''; + }, 16); + } + }); + }); + }, + + detach () { + if (this.stream) { + this.stream.destroy(); + this.stream = null; + } + }, + listen () { this.client.getEvents((error, stream) => { if (error || !stream) { @@ -354,16 +435,25 @@ export default { stream.on('data', json => { let data = JSON.parse(json); - if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') { + if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') { return; } if (data.status === 'destroy') { containerServerActions.destroyed({id: data.id}); + this.detach(data.id); } else if (data.status === 'kill') { containerServerActions.kill({id: data.id}); + this.detach(data.id); } else if (data.status === 'stop') { containerServerActions.stopped({id: data.id}); + this.detach(data.id); + } else if (data.status === 'create') { + this.logs(); + this.fetchContainer(data.id); + } else if (data.status === 'start') { + this.attach(); + this.fetchContainer(data.id); } else if (data.id) { this.fetchContainer(data.id); } diff --git a/styles/container-home.less b/styles/container-home.less index b8dbc3f2..91433584 100644 --- a/styles/container-home.less +++ b/styles/container-home.less @@ -7,14 +7,15 @@ flex-direction: row; padding: 1rem; .left { - width: 100%; + display: flex; + flex: 0.9 1 0; flex-direction: column; margin-right: 1rem; } .right { + display: flex; + flex: 0.1 0 300px; width: 40%; - min-width: 200px; - max-width: 600px; flex-direction: column; } .full { @@ -103,7 +104,6 @@ color: @gray-lightest; font-family: @font-code; font-size: 10px; - white-space: pre-wrap; -webkit-user-select: text; padding: 1.2rem 1.2rem 5rem 1.2rem; overflow: auto;