| Index: web/inc/logdog-stream-view/logdog-stream-view.html
|
| diff --git a/web/inc/logdog-stream-view/logdog-stream-view.html b/web/inc/logdog-stream-view/logdog-stream-view.html
|
| index f90fdd7f98abe5597a2d7d7cfbec182031026a39..563bc0a1adef491f937e6df58803d8a43cbc1c1e 100644
|
| --- a/web/inc/logdog-stream-view/logdog-stream-view.html
|
| +++ b/web/inc/logdog-stream-view/logdog-stream-view.html
|
| @@ -6,12 +6,13 @@
|
|
|
| <link rel="import" href="../bower_components/polymer/polymer.html">
|
| <link rel="import" href="../bower_components/google-signin/google-signin-aware.html">
|
| -<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
|
| -<link rel="import" href="../bower_components/iron-icons/av-icons.html">
|
| -<link rel="import" href="../bower_components/iron-icons/editor-icons.html">
|
| -<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
| -<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
|
| -<link rel="import" href="">
|
| +<link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html">
|
| +
|
| +<link rel="import" href="../logdog-stream/logdog-stream.html">
|
| +<link rel="import" href="../logdog-stream/logdog-error.html">
|
| +<link rel="import" href="../luci-sleep-promise/luci-sleep-promise.html">
|
| +<link rel="import" href="logdog-stream-fetcher.html">
|
| +<link rel="import" href="logdog-stream-query.html">
|
|
|
| <!--
|
| An element for rendering muxed LogDog log streams.
|
| @@ -19,32 +20,12 @@
|
| <dom-module id="logdog-stream-view">
|
|
|
| <template>
|
| - <style is="custom-style">
|
| - #mainView {
|
| - position: relative;
|
| - }
|
| -
|
| - #buttons {
|
| - position: fixed;
|
| - height: auto;
|
| - padding: 5px;
|
| - background-color: rgba(0, 0, 0, 0.1);
|
| - z-index: 100;
|
| - }
|
| - #buttons > paper-button {
|
| + <style>
|
| + .buttons {
|
| background-color: white;
|
| }
|
|
|
| - .paper-button-highlight[toggles][active] {
|
| - background-color: #cd6a51;
|
| - }
|
| -
|
| - .paper-icon-button-highlight[toggles][active] {
|
| - background-color: #cd6a51;
|
| - border-radius: 80%;
|
| - }
|
| -
|
| - #streamStatus {
|
| + #stream-status {
|
| position: fixed;
|
| right: 16px;
|
| background-color: #EEEEEE;
|
| @@ -52,7 +33,7 @@
|
| }
|
|
|
| #logContent {
|
| - padding-top: 54px; /* Pad around buttons */
|
| + padding-top: 20px;
|
| background-color: white;
|
| }
|
|
|
| @@ -62,89 +43,53 @@
|
| }
|
|
|
| .log-entry-meta {
|
| - background-color: lightgray;
|
| - padding: 5px;
|
| - border-width: 2px 0px 0px 0px;
|
| - border-color: darkgray;
|
| - border-style: dotted;
|
| - user-select: none;
|
| -
|
| + vertical-align: top;
|
| + padding: 0 8px 0 0;
|
| + margin: 0 0 0 0;
|
| + float: left;
|
| font-style: italic;
|
| font-family: Courier New, Courier, monospace;
|
| font-size: 10px;
|
|
|
| - /* Can be toggled to "flex" by applying .showMeta class to #logs. */
|
| + /* Fixed width, break word if necessary. */
|
| + width: 150px;
|
| + word-break: break-word;
|
| +
|
| + /* Can be toggled by applying .showMeta class to #logs. */
|
| display: none;
|
| }
|
| - .log-entry-meta-line {
|
| - padding: 5px;
|
| - border-width: 1px;
|
| - border-style: solid;
|
| - border-color: gray;
|
| - border-radius: 10px;
|
| - margin-right: 10px;
|
| - text-align: center;
|
| - }
|
| .showMeta .log-entry-meta {
|
| - display: flex;
|
| - }
|
| -
|
| - /* .log-entry-content { */
|
| - .log-entry-chunk {
|
| + display: block;
|
| + }
|
| +
|
| + .log-entry-content {
|
| padding: 0 0 0 0;
|
| margin: 0 0 0 0;
|
| float: none;
|
| font-family: Courier New, Courier, monospace;
|
| font-size: 16px;
|
| list-style: none;
|
| -
|
| - border-bottom: 1px solid #CCCCCC;
|
| + }
|
| +
|
| + .log-entry-line {
|
| + padding-left: 0;
|
|
|
| /* Can be toggled by applying .wrapLines class to #logs. */
|
| white-space: pre;
|
| }
|
| -
|
| - /*.wrapLines .log-entry-content { */
|
| - .wrapLines .log-entry-chunk {
|
| + .wrapLines .log-entry-line {
|
| white-space: pre-wrap;
|
| word-break: break-word;
|
| }
|
|
|
| - .logFetchButtonContainer {
|
| - height: auto;
|
| - display: none;
|
| - flex-direction: row;
|
| - background-color: rgba(0, 0, 0, 0.2);
|
| - padding: 2px;
|
| - }
|
| -
|
| - .logFetchButtonVisible {
|
| - display: flex !important;
|
| - }
|
| -
|
| - .logFetchButton {
|
| - width: 100%;
|
| - height: 18px;
|
| - }
|
| -
|
| - .logSplitUpButton {
|
| - background: yellow;
|
| - }
|
| - .logSplitDownButton {
|
| - background: green;
|
| - }
|
| -
|
| - .logBottomButton {
|
| + .log-entry-line:nth-last-child(2) {
|
| + border-bottom: 1px solid #CCCCCC;
|
| + }
|
| +
|
| + #bottom {
|
| background-color: lightcoral;
|
| - }
|
| -
|
| - #logEnd {
|
| - margin-bottom: 30px;
|
| - background-color: gray;
|
| - }
|
| -
|
| - .clickable-log-anchor {
|
| - height: 24px;
|
| + height: 2px;
|
| + margin-bottom: 10px;
|
| }
|
|
|
| #status-bar {
|
| @@ -162,74 +107,32 @@
|
| }
|
| </style>
|
|
|
| + <google-signin-aware
|
| + id="aware"
|
| + on-google-signin-aware-success="_onSignin"></google-signin-aware>
|
| +
|
| <rpc-client
|
| id="client"
|
| auto-token
|
| host="[[host]]"></rpc-client>
|
|
|
| - <!--
|
| - This must be after "rpc-client" so we get the signin event after it
|
| - does.
|
| - -->
|
| - <google-signin-aware
|
| - id="aware"
|
| - on-google-signin-aware-success="_onSignin"></google-signin-aware>
|
| -
|
| <!-- Stream view options. -->
|
| - <div id="mainView">
|
| - <div id="buttons">
|
| - <!-- If we have exactly one stream, we will enable users to split. -->
|
| - <template is="dom-if" if="{{showStreamingControls}}">
|
| - <template is="dom-if" if="{{canSplit}}">
|
| - <paper-icon-button title="Split, load logs from end"
|
| - icon="editor:vertical-align-bottom"
|
| - on-tap="_splitClicked">
|
| - </paper-icon-button>
|
| - </template>
|
| -
|
| - <template is="dom-if" if="{{isSplit}}">
|
| - <paper-icon-button title="Scroll to split"
|
| - icon="editor:vertical-align-center" on-tap="_scrollToSplit">
|
| - </paper-icon-button>
|
| - </template>
|
| -
|
| - <paper-icon-button class="paper-icon-button-highlight" toggles
|
| - title="Automatically scroll to new logs." icon="icons:update"
|
| - active="{{follow}}">
|
| - </paper-icon-button>
|
| -
|
| - <paper-icon-button class="paper-icon-button-highlight" toggles
|
| - title="Automatically load logs." icon="{{playingIconName}}"
|
| - active="{{playing}}">
|
| - </paper-icon-button>
|
| -
|
| - <template is="dom-if" if="{{isSplit}}">
|
| - <paper-icon-button toggles
|
| - title="Load new logs, or backfill from top."
|
| - icon="{{backfillIconName}}"
|
| - active="{{backfill}}">
|
| - </paper-icon-button>
|
| - </template>
|
| - </template>
|
| -
|
| - <template is="dom-if" if="{{_not(playing)}}">
|
| - <paper-button class="paper-button-highlight" toggles raised
|
| - active="{{wrapLines}}">
|
| - Wrap
|
| - </paper-button>
|
| -
|
| - <template is="dom-if" if="{{metadata}}">
|
| - <paper-button class="paper-button-highlight" toggles raised
|
| - active="{{showMetadata}}">
|
| - Metadata
|
| - </paper-button>
|
| - </template>
|
| - </template>
|
| + <div class="main-view">
|
| + <div class="buttons">
|
| + <paper-checkbox checked="{{showMetadata}}">
|
| + Show Metadata
|
| + </paper-checkbox>
|
| + <paper-checkbox checked="{{wrapLines}}">
|
| + Wrap Lines
|
| + </paper-checkbox>
|
| + <paper-checkbox checked="{{follow}}">
|
| + Follow
|
| + </paper-checkbox>
|
| </div>
|
|
|
| <!-- Display current fetching status, if stream data is still loading. -->
|
| - <div id="streamStatus">
|
| - <template is="dom-if" if="{{streamStatus}}">
|
| + <template is="dom-if" if="{{streamStatus}}">
|
| + <div id="stream-status">
|
| <table>
|
| <template is="dom-repeat" items="{{streamStatus}}">
|
| <tr>
|
| @@ -238,12 +141,11 @@
|
| </tr>
|
| </template>
|
| </table>
|
| - </template>
|
| - </div>
|
| + </div>
|
| + </template>
|
|
|
| <!-- Muxed log content. -->
|
| - <div id="logContent"
|
| - on-mousewheel="_handleMouseWheel">
|
| + <div id="logContent" on-mousewheel="_handleMouseWheel">
|
| <div id="logs">
|
| <!-- Content will be populated with JavaScript as logs are loaded.
|
|
|
| @@ -254,49 +156,18 @@
|
| <div class="log-entry-meta-line">(Meta N)</div>
|
| </div>
|
| <div class="log-entry-content">
|
| - LINE #0
|
| + <div class="log-entry-line">LINE #0</div>
|
| ...
|
| - LINE #N
|
| + <div class="log-entry-line">LINE #N</div>
|
| </div>
|
| </div>
|
| ...
|
|
|
| -
|
| - Note that we can't use templating to show/hide the log dividers,
|
| - since our positional log insertion requires them to be present and
|
| - move along with insertions as points of reference.
|
| -->
|
| -
|
| - <div id="logSplit" class="logFetchButtonContainer">
|
| - <!-- Insert point (prepend for head, append for tail). -->
|
| - <paper-button id="logSplitUp"
|
| - class="logFetchButton logSplitUpButton giant"
|
| - disabled="[[streamAnchorsNotClickable]]"
|
| - on-click="_handleUpClick">
|
| - <iron-icon icon="file-upload"></iron-icon>
|
| - </paper-button>
|
| - <paper-button id="logSplitDown"
|
| - class="logFetchButton logSplitDownButton giant"
|
| - disabled="[[streamAnchorsNotClickable]]"
|
| - on-click="_handleDownClick">
|
| - <iron-icon icon="file-download"></iron-icon>
|
| - </paper-button>
|
| - </div>
|
| -
|
| - <div id="logBottom" class="logFetchButtonContainer">
|
| - <!--
|
| - Bottom of the log stream (red bottom line). When tail is complete,
|
| - all future logs get prepended to this.
|
| - -->
|
| - <paper-button id="logBottomButton"
|
| - class="logFetchButton logBottomButton giant"
|
| - disabled="[[streamAnchorsNotClickable]]"
|
| - on-click="_handleBottomClick">
|
| - <iron-icon icon="arrow-drop-down"></iron-icon>
|
| - </paper-button>
|
| - </div>
|
| - <div id="logEnd"></div>
|
| </div>
|
| +
|
| + <!-- Current red bottom line. -->
|
| + <div id="bottom"></div>
|
| </div>
|
|
|
| </div>
|
| @@ -335,16 +206,6 @@
|
| observer: "_streamsChanged",
|
| },
|
|
|
| - toolbarAnchor: {
|
| - type: Object,
|
| - value: null,
|
| - },
|
| -
|
| - mobile: {
|
| - type: Boolean,
|
| - value: false,
|
| - },
|
| -
|
| /**
|
| * The number of logs to load before forcing a page refresh.
|
| *
|
| @@ -358,18 +219,6 @@
|
| notify: true,
|
| },
|
|
|
| - /**
|
| - * If true, render metadata blocks alongside their log entries.
|
| - *
|
| - * This will cause significantly more HTML elements during rendering (so
|
| - * that each metadata element can show up next to its row) and greatly
|
| - * slow the viewer down.
|
| - */
|
| - metadata: {
|
| - type: Boolean,
|
| - value: false,
|
| - },
|
| -
|
| /** If true, show log metadata column. */
|
| showMetadata: {
|
| type: Boolean,
|
| @@ -392,52 +241,6 @@
|
| type: Boolean,
|
| value: false,
|
| observer: "_followChanged",
|
| - },
|
| -
|
| - canSplit: {
|
| - type: Boolean,
|
| - value: false,
|
| - readOnly: true,
|
| - },
|
| -
|
| - showStreamingControls: {
|
| - type: Boolean,
|
| - value: true,
|
| - readOnly: true,
|
| - },
|
| -
|
| - isSplit: {
|
| - type: Boolean,
|
| - value: false,
|
| - readOnly: true,
|
| - },
|
| -
|
| - streamAnchorsNotClickable: {
|
| - type: Boolean,
|
| - computed:
|
| - '_computeAnchorsNotClickable(playing, showStreamingControls)',
|
| - },
|
| -
|
| - playing: {
|
| - type: Boolean,
|
| - value: false,
|
| - observer: "_playingChanged",
|
| - },
|
| -
|
| - playingIconName: {
|
| - type: String,
|
| - computed: '_computePlayingIconName(playing)',
|
| - },
|
| -
|
| - backfill: {
|
| - type: Boolean,
|
| - value: false,
|
| - observer: "_backfillChanged",
|
| - },
|
| -
|
| - backfillIconName: {
|
| - type: String,
|
| - computed: '_computeBackfillIconName(backfill)',
|
| },
|
|
|
| /**
|
| @@ -462,358 +265,304 @@
|
| },
|
| },
|
|
|
| - created: function() {
|
| - this._scrollTimeoutId = null;
|
| -
|
| - // Create "onScrollHandler", which just invokes "_onScroll" while bound
|
| - // to "this". We create it here so we can unregister it later, since
|
| - // "bind" returns a modified value.
|
| - this._onScrollHandler = function(e) { this._onScroll(e); }.bind(this);
|
| - },
|
| -
|
| - attached: function() {
|
| - require(["inc/rpc/client", "inc/logdog-stream-view/viewer"],
|
| - function(client, viewer) {
|
| - // Instantiate our view, and install callbacks.
|
| - this._v = viewer;
|
| - this._model = new viewer.Model({
|
| - client: new client.luci_rpc.Client(this.$.client),
|
| - mobile: this.mobile,
|
| -
|
| - pushLogEntries: this._pushLogEntries.bind(this),
|
| - clearLogEntries: this._clearLogEntries.bind(this),
|
| - updateControls: this._updateControls.bind(this),
|
| - locationIsVisible: this._locationIsVisible.bind(this),
|
| - });
|
| - this._streamsChanged();
|
| - }.bind(this));
|
| -
|
| - window.addEventListener('scroll', this._onScrollHandler);
|
| + ready: function() {
|
| + this._scheduledWrite = null;
|
| + this._buffer = null;
|
| + this._currentLogBuffer = null;
|
| + this._authCallback = null;
|
| },
|
|
|
| detached: function() {
|
| - // Unregsiter event handlers.
|
| - window.removeEventListener('scroll', this._onScrollHandler);
|
| -
|
| - // Reset state.
|
| - this.reset();
|
| + this.stop();
|
| },
|
|
|
| stop: function() {
|
| - this.reset();
|
| + this._cancelFetch(true);
|
| },
|
|
|
| /** Clears state and begins fetching log data. */
|
| reset: function() {
|
| - if ( this._model ) {
|
| - this._model.reset();
|
| - }
|
| - this._resetScroll();
|
| - this._model = null;
|
| - this._renderedLogs = false;
|
| + this._resetLogState();
|
| +
|
| + this._resolveStreams().then(function(streams) {
|
| + this._resetToStreams(streams);
|
| + }.bind(this)).catch(function(error) {
|
| + this._loadStatusBar("Failed to resolve streams:" + error);
|
| + throw error;
|
| + }.bind(this));
|
| + },
|
| +
|
| + /** Clears all current logs. */
|
| + _resetLogState: function() {
|
| + this._cancelFetch(true);
|
| +
|
| + // Remove all current log elements. */
|
| + while (this.$.logs.hasChildNodes()) {
|
| + this.$.logs.removeChild(this.$.logs.lastChild);
|
| + }
|
| +
|
| + // Clear our buffer and streamer state.
|
| + this._buffer = null;
|
| + this._currentLogBuffer = null;
|
| + if (this._streamer) {
|
| + this._streamer.shutdown();
|
| + }
|
| + this._streamer = null;
|
| + },
|
| +
|
| + _resolveStreams: function() {
|
| + // Separate our configured streams into full stream paths and queries.
|
| + var parts = {
|
| + queries: [],
|
| + streams: [],
|
| + };
|
| + var query = new LogDogQuery(this.project);
|
| + this.streams.map(LogDogStream.splitProject).forEach(function(v) {
|
| + if (LogDogQuery.isQuery(v.path)) {
|
| + parts.queries.push(v);
|
| + } else {
|
| + parts.streams.push(v);
|
| + }
|
| + });
|
| +
|
| + // Resolve any outstanding queries into full stream paths.
|
| + //
|
| + // If we get an authentication error, register to have our query
|
| + // resolution callback invoked on signin changes until it works (or
|
| + // indefinitely).
|
| + var queries = parts.queries.map(function(v) {
|
| + var params = new LogDogQueryParams(v.project).
|
| + path(v.path).
|
| + streamType("text");
|
| + return new LogDogQuery(this.$.client, params);
|
| + }.bind(this));
|
| +
|
| + var issueQuery = function() {
|
| + this._loadStatusBar("Resolving log streams from query...");
|
| + this._authCallback = null;
|
| +
|
| + return Promise.all(queries.map(function(q) {
|
| + return q.getAll();
|
| + }.bind(this))).then(function(results) {
|
| + this._loadStatusBar(null);
|
| +
|
| + // Add query results (if any) to streams.
|
| + results.forEach(function(streams) {
|
| + (streams || []).forEach(function(stream) {
|
| + parts.streams.push(stream.stream);
|
| + });
|
| + });
|
| + parts.streams.sort(LogDogStream.cmp);
|
| +
|
| + // Remove any duplicates.
|
| + var seenStreams = {};
|
| + var result = [];
|
| + parts.streams.forEach(function(s) {
|
| + var fullName = s.fullName();
|
| + if (!seenStreams[fullName]) {
|
| + seenStreams[fullName] = s;
|
| + result.push(s);
|
| + }
|
| + });
|
| + return result;
|
| + }.bind(this)).catch(function(error) {
|
| + if (error instanceof LogDogError && error.isUnauthenticated()) {
|
| + // Retry on auth event.
|
| + this._loadStatusBar("Not authorized to execute query. Log in " +
|
| + "with an authorized account.");
|
| + return new Promise(function(resolve) {
|
| + this._authCallback = resolve;
|
| + }.bind(this)).then(issueQuery);
|
| + }
|
| +
|
| + throw error;
|
| + }.bind(this));
|
| + }.bind(this);
|
| + return issueQuery();
|
| + },
|
| +
|
| + _resetToStreams: function(streams) {
|
| + // Unique streams.
|
| + if (!streams.length) {
|
| + this._loadStatusBar("No log streams.");
|
| + return;
|
| + }
|
| +
|
| + console.log("Loading log streams:", streams);
|
| + this._loadStatusBar("Loading stream data...");
|
| + streams.sort(LogDogStream.cmp);
|
| +
|
| + // Create a _BufferedStream for each stream.
|
| + var bufStreams = streams.map(function(stream, idx) {
|
| + return new _BufferedStream(stream, this.$.client,
|
| + (streams.length > 1), function(bs) {
|
| + this._updateStreamStatus(bs, idx);
|
| + }.bind(this));
|
| + }.bind(this));
|
| + this._buffer = new _LogStreamBuffer();
|
| + this._buffer.setStreams(bufStreams)
|
| +
|
| + this._streamer = new _LogStreamer(this._buffer, this.burst, function(v) {
|
| + this._loadStatusBar(v);
|
| + }.bind(this));
|
| +
|
| + // Construct our initial status content.
|
| + this._setStreamStatus(bufStreams.map(function(bs, idx) {
|
| + return {
|
| + name: (" [.../+/" + bs.stream.name() + "]"),
|
| + desc: bs.description(),
|
| + };
|
| + }.bind(this)));
|
| +
|
| + // Kick off our log fetching.
|
| + this._scheduleWriteNextLogs();
|
| + },
|
| +
|
| + /** Cancels any currently-executing log stream fetch. */
|
| + _cancelFetch: function(clear) {
|
| + this._cancelScheduledWrite();
|
| + this._authCallback = null;
|
| +
|
| + if (clear) {
|
| + this._setStreamStatus(null);
|
| + this._loadStatusBar(null);
|
| + }
|
| + },
|
| +
|
| + /** Cancels any scheduled asynchronous write. */
|
| + _cancelScheduledWrite: function() {
|
| + if (this._scheduledWrite) {
|
| + this.cancelAsync(this._scheduledWrite);
|
| + this._scheduledWrite = null;
|
| + }
|
| + },
|
| +
|
| + /** Called when the bound log stream variables has changed. */
|
| + _streamsChanged: function(v, old) {
|
| + this.reset();
|
| + },
|
| +
|
| + /** Schedules the next asynchronous log write. */
|
| + _scheduleWriteNextLogs: function() {
|
| + // This is called after refresh, so use this opportunity to maybe scroll
|
| + // to the bottom.
|
| + this._maybeScrollToBottom();
|
| +
|
| + if (! this._scheduledWrite) {
|
| + this._scheduledWrite = this.async(function() {
|
| + this._writeNextLogs()
|
| + }.bind(this));
|
| + }
|
| },
|
|
|
| /**
|
| - * Called each time a scroll event is fired. Since this can be really
|
| - * frequent, this will kick off a "scroll handler" in the background at an
|
| - * interval. Multiple scroll events within that interval will only result
|
| - * in one scroll handler invocation.
|
| + * This is an iterative function that grabs the next set of logs and renders
|
| + * them. Afterwards, it will continue rescheduling itself until there are
|
| + * no more logs to render.
|
| */
|
| - _onScroll: function(e) {
|
| - if ( this._scrollTimeoutId ) {
|
| - return;
|
| - }
|
| -
|
| - window.setTimeout(function() {
|
| - this._handleScrollEvent(e);
|
| - }.bind(this), 100);
|
| - },
|
| -
|
| - /** Actual scroll event handler. */
|
| - _handleScrollEvent: function(e) {
|
| - this._resetScroll();
|
| -
|
| - // Update our button bar position to be relative to the parent's height.
|
| - this._adjustToTop(this.$.buttons);
|
| - this._adjustToTop(this.$.streamStatus);
|
| - },
|
| -
|
| - _adjustToTop: function(elem) {
|
| - // Update our button bar position to be relative to the parent's height.
|
| - var pageRect = this.$.mainView.getBoundingClientRect();
|
| - var elemRect = elem.getBoundingClientRect();
|
| - var adjusted = (elem.offsetTop + pageRect.top - elemRect.top);
|
| - if ( adjusted < 0 ) {
|
| - adjusted = 0;
|
| - }
|
| - elem.style.top = adjusted;
|
| - },
|
| -
|
| - /** Clears asynchornous scroll event status. */
|
| - _resetScroll: function() {
|
| - if ( this._scrollTimeoutId !== null ) {
|
| - window.clearTimeout(this._scrollTimeoutId);
|
| - this._scrollTimeoutId = null;
|
| - }
|
| - },
|
| -
|
| - _handleMouseWheel: function(e) {
|
| - this.follow = false;
|
| - },
|
| -
|
| - _handleDownClick: function(e) {
|
| - this._model.fetchLocation(this._v.Location.HEAD, true);
|
| - },
|
| -
|
| - _handleUpClick: function(e) {
|
| - this._model.fetchLocation(this._v.Location.TAIL, true);
|
| - },
|
| -
|
| - _handleBottomClick: function(e) {
|
| - this._model.fetchLocation(this._v.Location.BOTTOM, true);
|
| - },
|
| -
|
| - /** Called when the bound log stream variables has changed. */
|
| - _streamsChanged: function() {
|
| - if( ! this._model ) {
|
| - return;
|
| - }
|
| -
|
| - this._model.resolve(this.streams).then( function() {
|
| - // If we're not on mobile, start with playing state.
|
| - this.playing = (!this.mobile);
|
| -
|
| - // Perform the initial fetch after resolution.
|
| - this._model.setAutomatic( this.playing );
|
| - this._model.setTailing( ! this.backfill );
|
| - this._model.fetch(false);
|
| - }.bind(this) );
|
| - },
|
| -
|
| - _appendMetaLine: function(root, key, value) {
|
| - var line = document.createElement("div");
|
| - line.className = "log-entry-meta-line";
|
| -
|
| - if ( key != null ) {
|
| - var e = document.createElement("strong");
|
| - e.textContent = key;
|
| - line.appendChild(e);
|
| - }
|
| -
|
| - if ( value != null ) {
|
| - var e = document.createElement("span");
|
| - e.textContent = value;
|
| - line.appendChild(e);
|
| - }
|
| -
|
| - root.appendChild(line);
|
| - },
|
| -
|
| - _pushLogEntries: function(entries, insertion) {
|
| - // Mark that we've rendered logs (show bars now).
|
| - this._renderedLogs = true;
|
| -
|
| - // Build our log entry chunk.
|
| - var logEntryChunk = document.createElement("div");
|
| - logEntryChunk.className = "log-entry-chunk";
|
| -
|
| - var lastLogEntry = logEntryChunk;
|
| - var lines = new Array();
|
| -
|
| - entries.forEach(function(le) {
|
| - var text = le.text;
|
| - if (!(text && text.lines)) {
|
| + _writeNextLogs: function() {
|
| + this._cancelScheduledWrite();
|
| +
|
| + this._streamer.load().then(function(entries) {
|
| + // If there are no entries, then we're done.
|
| + if (! entries) {
|
| + // Cancel all fetching state. If our streamer is finished, also clear
|
| + // messages and status.
|
| + if (this._streamer.finished) {
|
| + if (this._streamer.someStreamsFailed) {
|
| + this._cancelFetch(false);
|
| + this._loadStatusBar("Some streams failed to load.");
|
| + } else {
|
| + this._cancelFetch(true);
|
| + }
|
| + } else {
|
| + // No more logs, but also we are not finished. Retry after auth.
|
| + this._authCallback = this._scheduleWriteNextLogs.bind(this);
|
| + }
|
| return;
|
| }
|
|
|
| - // If we're rendering metadata, render an element per log entry.
|
| - if( this.metadata ) {
|
| - var entryRow = document.createElement("div");
|
| - entryRow.className = "log-entry";
|
| -
|
| - // Metadata column.
|
| - var metadataBlock = document.createElement("div");
|
| - metadataBlock.className = "log-entry-meta";
|
| -
|
| - this._appendMetaLine(metadataBlock, "Timestamp:", le.timestamp);
|
| - this._appendMetaLine(metadataBlock, "Stream:", le.desc.name);
|
| - this._appendMetaLine(metadataBlock, "Index:", le.streamIndex);
|
| -
|
| - // Log column.
|
| - var logDataBlock = document.createElement("div");
|
| - logDataBlock.className = "log-entry-content";
|
| -
|
| - le.text.lines.forEach(function(line) {
|
| - lines.push(line.value);
|
| - });
|
| -
|
| - logDataBlock.textContent = lines.join("\n");
|
| - lines.length = 0;
|
| -
|
| - entryRow.appendChild(metadataBlock);
|
| - entryRow.appendChild(logDataBlock);
|
| -
|
| - logEntryChunk.appendChild(entryRow);
|
| - lastLogEntry = entryRow;
|
| - } else {
|
| - // Add this to the lines. We'll assign this directly to logEntryChunk
|
| - // after the loop.
|
| - le.text.lines.forEach(function(line) {
|
| - lines.push(line.value);
|
| - });
|
| + var logEntryChunk = document.createElement("div");
|
| + entries.forEach(function(le) {
|
| + this._appendLogEntry(logEntryChunk, le);
|
| + }.bind(this));
|
| +
|
| + // To have styles apply correctly, we need to add it twice, see
|
| + // https://github.com/Polymer/polymer/issues/3100.
|
| + Polymer.dom(this.root).appendChild(logEntryChunk);
|
| + this.$.logs.appendChild(logEntryChunk);
|
| +
|
| + // Yield so that our browser can refresh. We can't directly use
|
| + // this.async since a timeout of "0" causes immediate execution instead
|
| + // of yielding.
|
| + setTimeout(function() {
|
| + this._scheduleWriteNextLogs();
|
| + }.bind(this), 0);
|
| + }.bind(this));
|
| + },
|
| +
|
| + _appendLogEntry: function(root, le) {
|
| + var text = le.text;
|
| + if (!(text && text.lines)) {
|
| + return 0;
|
| + }
|
| +
|
| + // Create elements manually to avoid Polymer overhead for large logs.
|
| + var entryRow = document.createElement("div");
|
| + entryRow.className = "log-entry";
|
| +
|
| + // Metadata column.
|
| + var metadataBlock = document.createElement("div");
|
| + metadataBlock.className = "log-entry-meta";
|
| + entryRow.appendChild(metadataBlock);
|
| +
|
| + var timestampDiv = document.createElement("div");
|
| + timestampDiv.className = "log-entry-meta-line";
|
| + timestampDiv.textContent = le.timestamp;
|
| + metadataBlock.appendChild(timestampDiv);
|
| +
|
| + var nameDiv = document.createElement("div");
|
| + nameDiv.className = "log-entry-meta-line";
|
| + nameDiv.textContent = le.desc.name;
|
| + metadataBlock.appendChild(nameDiv);
|
| +
|
| + var streamDiv = document.createElement("div");
|
| + streamDiv.className = "log-entry-meta-line";
|
| + streamDiv.textContent = le.streamIndex;
|
| + metadataBlock.appendChild(streamDiv);
|
| +
|
| + // Log column.
|
| + var logDataBlock = document.createElement("div");
|
| + logDataBlock.className = "log-entry-content";
|
| + if (le.text) {
|
| + for (var i = 0; i < le.text.lines.length; i++) {
|
| + var lineDiv = document.createElement("div");
|
| + lineDiv.className = "log-entry-line";
|
| + lineDiv.textContent = le.text.lines[i].value;
|
| + logDataBlock.appendChild(lineDiv);
|
| }
|
| - }.bind(this));
|
| -
|
| - if ( ! this.metadata ) {
|
| - // Only one HTML element: the chunk.
|
| - logEntryChunk.textContent = lines.join("\n");
|
| - lastLogEntry = logEntryChunk;
|
| - }
|
| -
|
| - // To have styles apply correctly, we need to add it twice, see
|
| - // https://github.com/Polymer/polymer/issues/3100.
|
| - Polymer.dom(this.root).appendChild(logEntryChunk);
|
| -
|
| - // Add the log entry to the appropriate place.
|
| - var anchor, scrollToTop = false;
|
| - var forceScroll = false;
|
| - switch ( insertion ) {
|
| - case this._v.Location.HEAD:
|
| - // InsertionPoint.HEAD: PREPEND to "logSplit".
|
| - this.$.logs.insertBefore(logEntryChunk, this.$.logSplit);
|
| -
|
| - // If we're not split, scroll to the log bottom. Otherwise, scroll to
|
| - // the split.
|
| - anchor = lastLogEntry;
|
| - break;
|
| -
|
| - case this._v.Location.TAIL:
|
| - // InsertionPoint.TAIL: APPEND to "logSplit".
|
| - anchor = this.$.logSplit;
|
| -
|
| - // Identify the element *after* our insertion point and scroll to it.
|
| - // This provides a semblance of stability as we top-insert.
|
| - //
|
| - // As a special case, if the next element is the log bottom, just
|
| - // scroll to the split, since there is no content to stabilize.
|
| - if ( anchor.nextElementSibling !== this.$.logBottom ) {
|
| - anchor = anchor.nextElementSibling;
|
| - }
|
| -
|
| - // Insert logs by adding them before the sibling following the log
|
| - // split (append to this.$.logSplit).
|
| - this.$.logs.insertBefore(logEntryChunk, this.$.logSplit.nextSibling);
|
| -
|
| - // When tailing, always scroll to the anchor point.
|
| - scrollToTop = true;
|
| - forceScroll = true;
|
| - break;
|
| -
|
| - case this._v.Location.BOTTOM:
|
| - // InsertionPoint.BOTTOM: PREPEND to "logBottom".
|
| - anchor = this.$.logBottom;
|
| - this.$.logs.insertBefore(logEntryChunk, anchor);
|
| - break;
|
| - }
|
| -
|
| - this._maybeScrollToElement(anchor, scrollToTop, forceScroll);
|
| - },
|
| -
|
| - _clearLogEntries: function() {
|
| - // Remove all current log elements. */
|
| - for ( var cur = this.$.logs.firstChild; cur; ) {
|
| - var del = cur;
|
| - cur = cur.nextElementSibling;
|
| - if ( del.classList && del.classList.contains('log-entry-chunk') ) {
|
| - this.$.logs.removeChild(del);
|
| - }
|
| - }
|
| - },
|
| -
|
| - _locationIsVisible: function(l) {
|
| - var anchor;
|
| - switch( l ) {
|
| - case this._v.Location.HEAD:
|
| - case this._v.Location.TAIL:
|
| - anchor = this.$.logSplit;
|
| -
|
| - case this._v.Location.BOTTOM:
|
| - anchor = this.$.logBottom;
|
| -
|
| - default:
|
| - return false;
|
| - }
|
| - return this._elementInViewport(anchor);
|
| - },
|
| -
|
| - _updateControls: function(c) {
|
| - this._setCanSplit(c.canSplit);
|
| - this._setIsSplit(c.split);
|
| - this.toggleClass("logFetchButtonVisible",
|
| - (c.split && this._renderedLogs), this.$.logSplit);
|
| - this.toggleClass("logFetchButtonVisible",
|
| - (c.bottom && this._renderedLogs), this.$.logBottom);
|
| -
|
| - this._setShowStreamingControls( !c.fullyLoaded );
|
| - if ( c.fullyLoaded ) {
|
| - this.playing = false;
|
| - }
|
| -
|
| - switch( c.loadingState ) {
|
| - case this._v.LoadingState.NONE:
|
| - this._loadStatusBar(null);
|
| - break;
|
| - case this._v.LoadingState.RESOLVING:
|
| - this._loadStatusBar("Resolving stream names...");
|
| - break;
|
| - case this._v.LoadingState.LOADING:
|
| - this._loadStatusBar("Loading streams...");
|
| - break;
|
| - case this._v.LoadingState.RENDERING:
|
| - this._loadStatusBar("Rendering logs.");
|
| - break;
|
| - case this._v.LoadingState.NEEDS_AUTH:
|
| - this._loadStatusBar("Not authenticated. Please log in.");
|
| - break;
|
| - case this._v.LoadingState.ERROR:
|
| - this._loadStatusBar("Error loading streams (see console).");
|
| - break;
|
| - }
|
| -
|
| - this._setStreamStatus(c.streamStatus);
|
| - },
|
| -
|
| - /** Scrolls to the follow anchor point. */
|
| - _maybeScrollToFollow: function() {
|
| - // Determine our anchor element.
|
| - var e;
|
| - if ( this.isSplit && this.backfill ) {
|
| - // Centering on the split element, at the bottom of the page.
|
| - e = this.$.logSplit;
|
| - } else {
|
| - // Scroll to the bottom of the page.
|
| - e = this.$.logEnd;
|
| - }
|
| -
|
| - this._maybeScrollToElement(e, false, false);
|
| - },
|
| -
|
| - /**
|
| - * Scrolls to the specified element, centering it at the top or bottom of
|
| - * the view. By default,t his will only happen if "follow" is enabled;
|
| - * however, it can be forced via "force".
|
| - */
|
| - _maybeScrollToElement: function(element, topOfView, force) {
|
| - if (this.follow || force) {
|
| - if ( topOfView ) {
|
| - element.scrollIntoView({
|
| - behavior: "auto",
|
| - block: "end",
|
| - });
|
| - } else {
|
| - // Bug? "block: start" doesn't seem to work the same as false.
|
| - element.scrollIntoView(false);
|
| - }
|
| + }
|
| + entryRow.appendChild(logDataBlock);
|
| + root.appendChild(entryRow);
|
| +
|
| + return le.text.lines.length;
|
| + },
|
| +
|
| + _updateStreamStatus: function(bs, idx) {
|
| + var origStatus = this.streamStatus[idx];
|
| + this.splice("streamStatus", idx, 1, {
|
| + name: origStatus.name,
|
| + desc: bs.description(),
|
| + });
|
| + },
|
| +
|
| + /** Scrolls to the bottom if "follow" is enabled. */
|
| + _maybeScrollToBottom: function() {
|
| + if (this.follow) {
|
| + this.$.bottom.scrollIntoView({
|
| + "behavior": "smooth",
|
| + "block": "end",
|
| + });
|
| }
|
| },
|
|
|
| @@ -823,8 +572,7 @@
|
| */
|
| _showMetadataChanged: function(v) {
|
| this.toggleClass("showMeta", v, this.$.logs);
|
| - },
|
| -
|
| + },
|
| /**
|
| * Callback when "wrapLines" has changed. This adds/removes the
|
| * "wrapLines" CSS class to the log data.
|
| @@ -832,77 +580,14 @@
|
| _wrapLinesChanged: function(v) {
|
| this.toggleClass("wrapLines", v, this.$.logs);
|
| },
|
| -
|
| - /** Callback when "follow" has changed. */
|
| - _playingChanged: function(v) {
|
| - if( ! this._model ) {
|
| - return;
|
| - }
|
| -
|
| - // If we're playing, begin log fetching.
|
| - this._model.setAutomatic(v);
|
| - },
|
| -
|
| - _computePlayingIconName: function(playing) {
|
| - return ( (playing) ?
|
| - "av:pause-circle-outline" : "av:play-circle-outline" );
|
| - },
|
| -
|
| - /** Callback when "follow" has changed. */
|
| - _backfillChanged: function(v) {
|
| - if( ! this._model ) {
|
| - return;
|
| - }
|
| -
|
| - // If we're backfilling, then we're not tailing.
|
| - this._model.setTailing(!v);
|
| - },
|
| -
|
| - _computeBackfillIconName: function(backfill) {
|
| - return ( (backfill) ?
|
| - "editor:border-bottom" : "editor:border-top" );
|
| - },
|
| -
|
| /** Callback when "follow" has changed. */
|
| _followChanged: function(v) {
|
| - if ( ! this._model ) {
|
| - return;
|
| - }
|
| -
|
| - if ( v ) {
|
| - // If follow is toggled on, automatically begin playing.
|
| - this.playing = true;
|
| - this._maybeScrollToFollow();
|
| - }
|
| - },
|
| -
|
| - /** Callback when "split" button has been clicked. */
|
| - _splitClicked: function() {
|
| - if( ! this._model ) {
|
| - return;
|
| - }
|
| -
|
| - // After a split, toggle off playing.
|
| - this._model.split();
|
| - this._model.setTailing(true);
|
| - this.playing = false;
|
| - },
|
| -
|
| - /** Callback when "split" button has been clicked. */
|
| - _scrollToSplit: function() {
|
| - this._maybeScrollToElement(this.$.logSplit, true, true);
|
| - },
|
| -
|
| - _computeAnchorsNotClickable: function(playing, showStreamingControls,
|
| - rendering) {
|
| - // Anchors are not clickable if we're playing or the controls are
|
| - // not visible.
|
| - return ( playing || (!showStreamingControls) || rendering );
|
| - },
|
| -
|
| - /** Filter function to invert a value. */
|
| - _not: function(v) {
|
| - return (!v);
|
| + this._maybeScrollToBottom();
|
| + },
|
| +
|
| + /** Callback for when the mouse wheel has scrolled. Disables follow. */
|
| + _handleMouseWheel: function() {
|
| + this.follow = false;
|
| },
|
|
|
| /**
|
| @@ -922,30 +607,470 @@
|
| },
|
|
|
| _onSignin: function() {
|
| - if ( this._model ) {
|
| - this._model.notifyAuthenticationChanged();
|
| - }
|
| - },
|
| -
|
| - _elementInViewport: function(el) {
|
| - var top = el.offsetTop;
|
| - var left = el.offsetLeft;
|
| - var width = el.offsetWidth;
|
| - var height = el.offsetHeight;
|
| -
|
| - while(el.offsetParent) {
|
| - el = el.offsetParent;
|
| - top += el.offsetTop;
|
| - left += el.offsetLeft;
|
| - }
|
| -
|
| - return (
|
| - top < (window.pageYOffset + window.innerHeight) &&
|
| - left < (window.pageXOffset + window.innerWidth) &&
|
| - (top + height) > window.pageYOffset &&
|
| - (left + width) > window.pageXOffset
|
| - );
|
| - },
|
| -
|
| + var fn = this._authCallback;
|
| + if (fn) {
|
| + this._authCallback = null;
|
| + fn();
|
| + }
|
| + },
|
| });
|
| +
|
| + /**
|
| + * Continuously loads log streams from a _LogStreamBuffer and exposes them via
|
| + * callback.
|
| + */
|
| + function _LogStreamer(buffer, burst, statusCallback) {
|
| + this._buffer = buffer;
|
| + this._burst = (burst || 0);
|
| + this._missingDelay = 5000;
|
| + this._statusCallback = statusCallback;
|
| +
|
| + this.finished = false;
|
| + this.someStreamsFailed = false;
|
| +
|
| + this._currentLogBuffer = null;
|
| + }
|
| +
|
| + _LogStreamer.prototype.shutdown = function() {
|
| + this.finshed = true;
|
| + };
|
| +
|
| + _LogStreamer.prototype._setStatus = function(v) {
|
| + if (this._statusCallback) {
|
| + this._statusCallback(v);
|
| + }
|
| + };
|
| +
|
| + _LogStreamer.prototype.load = function() {
|
| + if (this.finished) {
|
| + this._setStatus(null);
|
| + return Promise.resolve(null);
|
| + }
|
| +
|
| + // If we have buffered logs, return them.
|
| + var current = this._currentLogBuffer;
|
| + if (current) {
|
| + // We will track how many log entries that we've rendered. If we exceed
|
| + // this amount, we will force a refresh so the logs appear streaming and
|
| + // the app remains responsive.
|
| + var rendered = 0;
|
| +
|
| + var entries = [];
|
| + for (var le = current.next(); (le); le = current.next()) {
|
| + entries.push(le);
|
| + if (le.text && le.text.lines) {
|
| + rendered += le.text.lines.length;
|
| + }
|
| +
|
| + if (this._burst > 0 && rendered >= this._burst) {
|
| + break;
|
| + }
|
| + }
|
| +
|
| + // Have we exhausted this buffer?
|
| + if (! current.peek()) {
|
| + this._currentLogBuffer = null;
|
| + }
|
| +
|
| + // Return the bundle of entries.
|
| + return Promise.resolve(entries);
|
| + }
|
| +
|
| + // We didn't have any buffered logs, so either all of our streams are
|
| + // finished or our buffer is empty and needs to be refreshed.
|
| + this._setStatus("Loading log stream data...");
|
| + return this._buffer.nextBuffer().then(function(buf) {
|
| + this.someStreamsFailed = (!!this._buffer._failures.length);
|
| +
|
| + // Check result.
|
| + if (buf === null) {
|
| + if (this._buffer.finished) {
|
| + // No more buffers, we are done.
|
| + console.log("All streams have been exhausted.");
|
| + this.finished = true;
|
| + this._setStatus(null);
|
| + return null;
|
| + }
|
| +
|
| + // The buffer was incomplete. Should we retry after a delay, or do
|
| + // we need to wait for an explicit edge (e.g., auth)?
|
| + if (this._buffer.autoRetry) {
|
| + // Sleep for 5 seconds and try again (waiting).
|
| + console.log("Log stream delayed; sleeping", this._missingDelay,
|
| + "and retry.");
|
| + this._setStatus("Missing log streams, retrying after delay...");
|
| + return new LuciSleepPromise(this._missingDelay).then(function() {
|
| + if (this.finished) {
|
| + console.log("Streamer is deactivated, discarding.");
|
| + return null;
|
| + }
|
| +
|
| + return this.load();
|
| + }.bind(this));
|
| + }
|
| +
|
| + this._setStatus("Some log streams could not be loaded.");
|
| + return null;
|
| + }
|
| +
|
| + // Install the new buffer and re-enter.
|
| + this._currentLogBuffer = buf;
|
| + return this.load();
|
| + }.bind(this)).catch(function(error) {
|
| + this._setStatus("[" + error.name + "] fetching streams: " +
|
| + error.message);
|
| + throw error;
|
| + }.bind(this));
|
| + };
|
| +
|
| + /**
|
| + * Manages an aggregate log stream buffer, consisting of logs punted from a
|
| + * set of zero or more _BufferedStream instances.
|
| + */
|
| + function _LogStreamBuffer() {
|
| + this._streams = null;
|
| + this._active = null;
|
| + this._nextBufferPromise = null;
|
| + this._failures = [];
|
| +
|
| + this.finished = false;
|
| + this._resetIterativeState();
|
| + }
|
| +
|
| + _LogStreamBuffer.prototype.setStreams = function(streams) {
|
| + // TODO(dnj): Make this do a delta with previous streams so we don't lose
|
| + // their already-loaded logs if the page changes.
|
| + this._streams = streams.map(function(bs, i) {
|
| + return {
|
| + bs: bs,
|
| + active: true,
|
| + buffer: new _BufferedLogs(),
|
| + };
|
| + });
|
| + this._active = this._streams;
|
| + this._nextBufferPromise = null;
|
| + };
|
| +
|
| + _LogStreamBuffer.prototype._resetIterativeState = function() {
|
| + this.autoRetry = false;
|
| + };
|
| +
|
| + /**
|
| + * Returns a Promise that resolves into a _BufferedLogs instance containing
|
| + * the next set of logs, in order, from the source log streams.
|
| + *
|
| + * The _BufferedLogs bundle may have status flags set, and should be checked.
|
| + *
|
| + * The Promise will also resolve to "null" if there are no more logs in the
|
| + * source streams.
|
| + *
|
| + * If there are errors fetching logs, the Promise will be rejected, and an
|
| + * error will be returned.
|
| + */
|
| + _LogStreamBuffer.prototype.nextBuffer = function() {
|
| + // If we're already are fetching the next buffer, return that Promise.
|
| + if (this._nextBufferPromise) {
|
| + return this._nextBufferPromise;
|
| + }
|
| +
|
| + // Filter our any finished streams from our active list. A stream is
|
| + // finished if it is finished streaming and we don't have a retained buffer
|
| + // from it.
|
| + this._active = this._active.filter(function(entry) {
|
| + return (entry.buffer.peek() || (! (entry.bs.finished || entry.bs.error)));
|
| + })
|
| +
|
| + if (! this._active.length) {
|
| + this.finished = true;
|
| + }
|
| + if (this.finished) {
|
| + // No active streams, so we're finished. Permanently set our promise to
|
| + // the finished state.
|
| + this._nextBufferPromise = Promise.resolve(null);
|
| + return this._nextBufferPromise;
|
| + }
|
| +
|
| + // Fill all buffers for all active streams. This may result in an RPC to
|
| + // load new buffer content for streams whose buffers are empty.
|
| + //
|
| + // RPC failures are handled here:
|
| + // - If the stream reports "not found", we will terminate early and set
|
| + // out status to "waiting". Our owner should retry after a delay.
|
| + // - Otherwise, we will set our status to "error". Our owner should report
|
| + // that an error has occurred while loading stream data.
|
| + this._resetIterativeState();
|
| +
|
| + var incomplete = false;
|
| + this._nextBufferPromise = Promise.all(this._active.map(function(entry) {
|
| + // If the entry's buffer still has data, use it immediately.
|
| + if (entry.buffer.peek()) {
|
| + return entry.buffer;
|
| + }
|
| +
|
| + // Get the next log buffer for each stream. This may result in an RPC.
|
| + return entry.bs.nextBuffer().then(function(buffer) {
|
| + // Retain this buffer, if valid. The stream may have returned a null
|
| + // buffer if it failed to fetch for a legitimate reason. In this case,
|
| + // we will not retain it (since we want entry.buffer to be valid), but
|
| + // will forward the "null" to our aggregate function.
|
| + if (buffer) {
|
| + entry.buffer = buffer;
|
| + } else {
|
| + incomplete = true;
|
| +
|
| + // If this stream is waiting, but not on auth, mark that we should
|
| + // automatically retry.
|
| + if (entry.bs.waiting && !entry.bs.auth) {
|
| + this.autoRetry = true;
|
| + }
|
| + }
|
| + return buffer;
|
| + }.bind(this)).catch(function(error) {
|
| + // Log stream source of error. Raise a generic "failed to buffer"
|
| + // error. This will become a permanent failure.
|
| + console.error("Error loading buffer for", entry.bs.stream.fullName(),
|
| + "(", entry.bs, "): ", error);
|
| + this._failures.push(entry.bs);
|
| + return null;
|
| + }.bind(this));
|
| + }.bind(this))).then(function(buffers) {
|
| + this._nextBufferPromise = null;
|
| +
|
| + // Check each buffer. If any are null, that stream failed to deliver.
|
| + if (incomplete) {
|
| + // We succeeded, but are incomplete. At least one stream failed to
|
| + // deliver and should have state flags set accordingly.
|
| + return null;
|
| + }
|
| +
|
| + // Remove any null buffers. These would be placed here when a stream fails
|
| + // to load. Aggregate as much data from each of our streams as possible.
|
| + buffers = buffers.filter(v => (!!v));
|
| + return this._aggregateBuffers(buffers);
|
| + }.bind(this));
|
| + return this._nextBufferPromise;
|
| + };
|
| +
|
| + _LogStreamBuffer.prototype._aggregateBuffers = function(buffers) {
|
| + switch (buffers.length) {
|
| + case 0:
|
| + // No buffers, so no logs.
|
| + return new _BufferedLogs(null);
|
| + case 1:
|
| + // As a special case, if we only have one buffer, and we assume that its
|
| + // entries are sorted, then that buffer is a return value.
|
| + return new _BufferedLogs(buffers[0].getAll());
|
| + }
|
| +
|
| + // Preload our peek array.
|
| + var incomplete = false;
|
| + var peek = buffers.map(function(buf) {
|
| + var le = buf.peek();
|
| + if (! le) {
|
| + incomplete = true;
|
| + }
|
| + return le;
|
| + });
|
| + if (incomplete) {
|
| + // One of our input buffers had no log entries.
|
| + return new _BufferedLogs(null);
|
| + }
|
| +
|
| + // Assemble our aggregate buffer array.
|
| + // TODO: A binary heap would be pretty great for this.
|
| + var entries = [];
|
| + while (true) {
|
| + // Choose the next stream.
|
| + var earliest = 0;
|
| + for (var i = 1; i < buffers.length; i++) {
|
| + if (_LogStreamBuffer.compareLogs(peek[i], peek[earliest]) < 0) {
|
| + earliest = i;
|
| + }
|
| + }
|
| +
|
| + // Get the next log from the earliest stream.
|
| + entries.push(buffers[earliest].next());
|
| +
|
| + // Repopulate that buffer's "peek" value. If the buffer has no more
|
| + // entries, then we're done.
|
| + var next = buffers[earliest].peek();
|
| + if (!next) {
|
| + return new _BufferedLogs(entries);
|
| + }
|
| + peek[earliest] = next;
|
| + }
|
| + };
|
| +
|
| + _LogStreamBuffer.compareLogs = function(a, b) {
|
| + // If they are part of the same stream, compare prefix indexes.
|
| + if (a.source.stream.samePrefixAs(b.source.stream)) {
|
| + return (a.prefixIndex - b.prefixIndex);
|
| + }
|
| +
|
| + // Compare based on timestamp.
|
| + return a.timestamp - b.timestamp;
|
| + };
|
| +
|
| +
|
| + /**
|
| + * A buffer of ordered log entries from all streams.
|
| + *
|
| + * Assumes total ownership of the input log buffer, which can be null to
|
| + * indicate no logs.
|
| + */
|
| + function _BufferedLogs(logs) {
|
| + this._logs = logs;
|
| + this._index = 0;
|
| + }
|
| +
|
| + _BufferedLogs.prototype.getAll = function() {
|
| + // Pop all logs.
|
| + var logs = this._logs;
|
| + this._logs = null;
|
| + return logs;
|
| + };
|
| +
|
| + _BufferedLogs.prototype.peek = function() {
|
| + return (this._logs) ? (this._logs[this._index]) : (null);
|
| + };
|
| +
|
| + _BufferedLogs.prototype.next = function() {
|
| + if (! (this._logs && this._logs.length)) {
|
| + return null;
|
| + }
|
| +
|
| + // Get the next log and increment our index.
|
| + var log = this._logs[this._index++];
|
| + if (this._index >= this._logs.length) {
|
| + this._logs = null;
|
| + }
|
| + return log;
|
| + };
|
| +
|
| +
|
| + /**
|
| + * Stateful log fetching manager for a single log stream.
|
| + */
|
| + function _BufferedStream(stream, client, oneOfMany, statusCallback) {
|
| + this.stream = stream;
|
| +
|
| + this.error = null;
|
| + this.finished = false;
|
| +
|
| + this._fetcher = new LogDogFetcher(client, stream);
|
| + this._oneOfMany = oneOfMany;
|
| + this._statusCallback = statusCallback;
|
| + this._lastFetchIndex = null;
|
| + }
|
| +
|
| + _BufferedStream.INITIAL_FETCH_SIZE = 4096;
|
| +
|
| + _BufferedStream.prototype._resetIterativeState = function() {
|
| + this.waiting = false;
|
| + this.auth = false;
|
| + this._fireStatusUpdated();
|
| +
|
| + this._currentFetch = null;
|
| + };
|
| +
|
| + _BufferedStream.prototype.nextBuffer = function() {
|
| + if (this._currentFetch) {
|
| + return this._currentFetch;
|
| + }
|
| +
|
| + // Reset per-round state and begin next round fetch.
|
| + this._resetIterativeState();
|
| +
|
| + // If this is the first fetch, and we're not the only log stream being
|
| + // rendered, fetch a small amount so we can (probably) start rendering
|
| + // without waiting for a lot of huge chunks.
|
| + this._fetcher.byteCount = (
|
| + (this._lastFetchIndex === null) && this._oneOfMany) ?
|
| + (_BufferedStream.INITIAL_FETCH_SIZE) : (null);
|
| +
|
| + this._currentFetch = this._fetcher.next().then(function(result) {
|
| + this._currentFetch = null;
|
| +
|
| + // Update our stream information.
|
| + this.finished = this._fetcher.finished;
|
| +
|
| + // Augment each returned log entry with self-descriptive metadata.
|
| + var logs = result.entries;
|
| + if (logs && logs.length) {
|
| + logs.forEach(function(le) {
|
| + le.desc = result.desc;
|
| + le.state = result.state;
|
| + le.source = this;
|
| + }.bind(this));
|
| +
|
| + // Record the latest fetch index.
|
| + this._lastFetchIndex = logs[logs.length - 1].streamIndex;
|
| + }
|
| +
|
| + this._fireStatusUpdated();
|
| + return new _BufferedLogs(logs);
|
| + }.bind(this)).catch(function(error) {;
|
| + // If this is a "not found" error, we assume that the stream is valid, but
|
| + // hasn't been ingested into LogDog yet. Return "null".
|
| + if (error instanceof LogDogError) {
|
| + if (error.isUnauthenticated()) {
|
| + this.waiting = true;
|
| + this.auth = true;
|
| + } else if (error.isNotFound()) {
|
| + this.waiting = true;
|
| + }
|
| +
|
| + // If this is an error that we understand, recover from it, return
|
| + // null, and set our status flags.
|
| + if (this.waiting) {
|
| + // Recover from this error.
|
| + this._currentFetch = null;
|
| + this._fireStatusUpdated();
|
| + return null;
|
| + }
|
| + }
|
| +
|
| + // Retain this error forever.
|
| + this.error = error;
|
| + throw error;
|
| + }.bind(this));
|
| + return this._currentFetch;
|
| + };
|
| +
|
| + _BufferedStream.prototype._fireStatusUpdated = function() {
|
| + if (this._statusCallback) {
|
| + this._statusCallback(this);
|
| + }
|
| + };
|
| +
|
| + _BufferedStream.prototype.description = function() {
|
| + if (this._waiting) {
|
| + return "(Waiting)";
|
| + }
|
| +
|
| + var pieces = []
|
| + var tidx = this._fetcher.terminalIndex();
|
| + if (this._lastFetchIndex) {
|
| + if (tidx >= 0) {
|
| + pieces.push(this._lastFetchIndex + " / " + tidx);
|
| + } else {
|
| + pieces.push(this._lastFetchIndex + "...");
|
| + }
|
| + }
|
| +
|
| + if (this.error) {
|
| + pieces.push("(Error)");
|
| + } else if (this.auth) {
|
| + pieces.push("(Auth Error)");
|
| + } else if (this.waiting) {
|
| + pieces.push("(Waiting)");
|
| + } else if (!this._fetcher.state) {
|
| + pieces.push("(Fetching)");
|
| + } else if (this._fetcher.finished) {
|
| + pieces.push("(Finished)");
|
| + } else {
|
| + pieces.push("(Streaming)");
|
| + }
|
| + return pieces.join(" ");
|
| + };
|
| </script>
|
|
|