Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(7)

Side by Side Diff: web/inc/logdog-stream-view/logdog-stream-view.html

Issue 2543323004: Rewrite LogDog log viewer app. (Closed)
Patch Set: Control all fetch sizes, fix follow on initial click, fix small fetch when auth is retrid. Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 <!-- 1 <!--
2 Copyright 2016 The LUCI Authors. All rights reserved. 2 Copyright 2016 The LUCI Authors. All rights reserved.
3 Use of this source code is governed under the Apache License, Version 2.0 3 Use of this source code is governed under the Apache License, Version 2.0
4 that can be found in the LICENSE file. 4 that can be found in the LICENSE file.
5 --> 5 -->
6 6
7 <link rel="import" href="../bower_components/polymer/polymer.html"> 7 <link rel="import" href="../bower_components/polymer/polymer.html">
8 <link rel="import" href="../bower_components/google-signin/google-signin-aware.h tml"> 8 <link rel="import" href="../bower_components/google-signin/google-signin-aware.h tml">
9 <link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html" > 9 <link rel="import" href="../bower_components/iron-icons/iron-icons.html">
10 10 <link rel="import" href="../bower_components/iron-icons/av-icons.html">
11 <link rel="import" href="../logdog-stream/logdog-stream.html"> 11 <link rel="import" href="../bower_components/iron-icons/editor-icons.html">
12 <link rel="import" href="../logdog-stream/logdog-error.html"> 12 <link rel="import" href="../bower_components/paper-button/paper-button.html">
13 <link rel="import" href="../luci-sleep-promise/luci-sleep-promise.html"> 13 <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button .html">
14 <link rel="import" href="logdog-stream-fetcher.html"> 14 <link rel="import" href="">
15 <link rel="import" href="logdog-stream-query.html">
16 15
17 <!-- 16 <!--
18 An element for rendering muxed LogDog log streams. 17 An element for rendering muxed LogDog log streams.
19 --> 18 -->
20 <dom-module id="logdog-stream-view"> 19 <dom-module id="logdog-stream-view">
21 20
22 <template> 21 <template>
23 <style> 22 <style is="custom-style">
24 .buttons { 23 #mainView {
24 position: relative;
25 }
26
27 #buttons {
28 position: fixed;
29 height: auto;
30 padding: 5px;
31 background-color: rgba(0, 0, 0, 0.1);
32 z-index: 100;
33 }
34 #buttons > paper-button {
25 background-color: white; 35 background-color: white;
26 } 36 }
27 37
28 #stream-status { 38 .paper-button-highlight[toggles][active] {
39 background-color: #cd6a51;
40 }
41
42 .paper-icon-button-highlight[toggles][active] {
43 background-color: #cd6a51;
44 border-radius: 80%;
45 }
46
47 #streamStatus {
29 position: fixed; 48 position: fixed;
30 right: 16px; 49 right: 16px;
31 background-color: #EEEEEE; 50 background-color: #EEEEEE;
32 opacity: 0.7; 51 opacity: 0.7;
33 } 52 }
34 53
35 #logContent { 54 #logContent {
36 padding-top: 20px; 55 padding-top: 54px; /* Pad around buttons */
37 background-color: white; 56 background-color: white;
38 } 57 }
39 58
40 .log-entry { 59 .log-entry {
41 padding: 0 0 0 0; 60 padding: 0 0 0 0;
42 clear: left; 61 clear: left;
43 } 62 }
44 63
45 .log-entry-meta { 64 .log-entry-meta {
46 vertical-align: top; 65 background-color: lightgray;
47 padding: 0 8px 0 0; 66 padding: 5px;
48 margin: 0 0 0 0; 67 border-width: 2px 0px 0px 0px;
49 float: left; 68 border-color: darkgray;
69 border-style: dotted;
70 user-select: none;
71
50 font-style: italic; 72 font-style: italic;
51 font-family: Courier New, Courier, monospace; 73 font-family: Courier New, Courier, monospace;
52 font-size: 10px; 74 font-size: 10px;
53 75
54 /* Fixed width, break word if necessary. */ 76 /* Can be toggled to "flex" by applying .showMeta class to #logs. */
55 width: 150px;
56 word-break: break-word;
57
58 /* Can be toggled by applying .showMeta class to #logs. */
59 display: none; 77 display: none;
60 } 78 }
79 .log-entry-meta-line {
80 padding: 5px;
81 border-width: 1px;
82 border-style: solid;
83 border-color: gray;
84 border-radius: 10px;
85 margin-right: 10px;
86 text-align: center;
87 }
61 .showMeta .log-entry-meta { 88 .showMeta .log-entry-meta {
62 display: block; 89 display: flex;
63 } 90 }
64 91
65 .log-entry-content { 92 /* .log-entry-content { */
93 .log-entry-chunk {
66 padding: 0 0 0 0; 94 padding: 0 0 0 0;
67 margin: 0 0 0 0; 95 margin: 0 0 0 0;
68 float: none; 96 float: none;
69 font-family: Courier New, Courier, monospace; 97 font-family: Courier New, Courier, monospace;
70 font-size: 16px; 98 font-size: 16px;
71 list-style: none; 99 list-style: none;
72 }
73 100
74 .log-entry-line { 101 border-bottom: 1px solid #CCCCCC;
75 padding-left: 0;
76 102
77 /* Can be toggled by applying .wrapLines class to #logs. */ 103 /* Can be toggled by applying .wrapLines class to #logs. */
78 white-space: pre; 104 white-space: pre;
79 } 105 }
80 .wrapLines .log-entry-line { 106
107 /*.wrapLines .log-entry-content { */
108 .wrapLines .log-entry-chunk {
81 white-space: pre-wrap; 109 white-space: pre-wrap;
82 word-break: break-word; 110 word-break: break-word;
83 } 111 }
84 112
85 .log-entry-line:nth-last-child(2) { 113 .logFetchButtonContainer {
86 border-bottom: 1px solid #CCCCCC; 114 height: auto;
115 display: none;
116 flex-direction: row;
117 background-color: rgba(0, 0, 0, 0.2);
118 padding: 2px;
87 } 119 }
88 120
89 #bottom { 121 .logFetchButtonVisible {
122 display: flex !important;
123 }
124
125 .logFetchButton {
126 width: 100%;
127 height: 18px;
128 }
129
130 .logSplitUpButton {
131 background: yellow;
132 }
133 .logSplitDownButton {
134 background: green;
135 }
136
137 .logBottomButton {
90 background-color: lightcoral; 138 background-color: lightcoral;
91 height: 2px; 139 }
92 margin-bottom: 10px; 140
141 #logEnd {
142 margin-bottom: 30px;
143 background-color: gray;
144 }
145
146 .clickable-log-anchor {
147 height: 24px;
93 } 148 }
94 149
95 #status-bar { 150 #status-bar {
96 /* Overlay at the bottom of the page. */ 151 /* Overlay at the bottom of the page. */
97 position: fixed; 152 position: fixed;
98 z-index: 9999; 153 z-index: 9999;
99 overflow: hidden; 154 overflow: hidden;
100 bottom: 0; 155 bottom: 0;
101 left: 0; 156 left: 0;
102 width: 100%; 157 width: 100%;
103 158
104 text-align: center; 159 text-align: center;
105 font-size: 16px; 160 font-size: 16px;
106 background-color: rgba(245, 245, 220, 0.7); 161 background-color: rgba(245, 245, 220, 0.7);
107 } 162 }
108 </style> 163 </style>
109 164
110 <google-signin-aware
111 id="aware"
112 on-google-signin-aware-success="_onSignin"></google-signin-aware>
113
114 <rpc-client 165 <rpc-client
115 id="client" 166 id="client"
116 auto-token 167 auto-token
117 host="[[host]]"></rpc-client> 168 host="[[host]]"></rpc-client>
118 169
170 <!--
171 This must be after "rpc-client" so we get the signin event after it
172 does.
173 -->
174 <google-signin-aware
175 id="aware"
176 on-google-signin-aware-success="_onSignin"></google-signin-aware>
177
119 <!-- Stream view options. --> 178 <!-- Stream view options. -->
120 <div class="main-view"> 179 <div id="mainView">
121 <div class="buttons"> 180 <div id="buttons">
122 <paper-checkbox checked="{{showMetadata}}"> 181 <!-- If we have exactly one stream, we will enable users to split. -->
123 Show Metadata 182 <template is="dom-if" if="{{showStreamingControls}}">
124 </paper-checkbox> 183 <template is="dom-if" if="{{canSplit}}">
125 <paper-checkbox checked="{{wrapLines}}"> 184 <paper-icon-button title="Split, load logs from end"
126 Wrap Lines 185 icon="editor:vertical-align-bottom"
127 </paper-checkbox> 186 on-tap="_splitClicked">
128 <paper-checkbox checked="{{follow}}"> 187 </paper-icon-button>
129 Follow 188 </template>
130 </paper-checkbox> 189
190 <template is="dom-if" if="{{isSplit}}">
191 <paper-icon-button title="Scroll to split"
192 icon="editor:vertical-align-center" on-tap="_scrollToSplit">
193 </paper-icon-button>
194 </template>
195
196 <paper-icon-button class="paper-icon-button-highlight" toggles
197 title="Automatically scroll to new logs." icon="icons:update"
198 active="{{follow}}">
199 </paper-icon-button>
200
201 <paper-icon-button class="paper-icon-button-highlight" toggles
202 title="Automatically load logs." icon="{{playingIconName}}"
203 active="{{playing}}">
204 </paper-icon-button>
205
206 <template is="dom-if" if="{{isSplit}}">
207 <paper-icon-button toggles
208 title="Load new logs, or backfill from top."
209 icon="{{backfillIconName}}"
210 active="{{backfill}}">
211 </paper-icon-button>
212 </template>
213 </template>
214
215 <template is="dom-if" if="{{_not(playing)}}">
216 <paper-button class="paper-button-highlight" toggles raised
217 active="{{wrapLines}}">
218 Wrap
219 </paper-button>
220
221 <template is="dom-if" if="{{metadata}}">
222 <paper-button class="paper-button-highlight" toggles raised
223 active="{{showMetadata}}">
224 Metadata
225 </paper-button>
226 </template>
227 </template>
131 </div> 228 </div>
132 229
133 <!-- Display current fetching status, if stream data is still loading. --> 230 <!-- Display current fetching status, if stream data is still loading. -->
134 <template is="dom-if" if="{{streamStatus}}"> 231 <div id="streamStatus">
135 <div id="stream-status"> 232 <template is="dom-if" if="{{streamStatus}}">
136 <table> 233 <table>
137 <template is="dom-repeat" items="{{streamStatus}}"> 234 <template is="dom-repeat" items="{{streamStatus}}">
138 <tr> 235 <tr>
139 <td>{{item.name}}</td> 236 <td>{{item.name}}</td>
140 <td>{{item.desc}}</td> 237 <td>{{item.desc}}</td>
141 </tr> 238 </tr>
142 </template> 239 </template>
143 </table> 240 </table>
144 </div> 241 </template>
145 </template> 242 </div>
146 243
147 <!-- Muxed log content. --> 244 <!-- Muxed log content. -->
148 <div id="logContent" on-mousewheel="_handleMouseWheel"> 245 <div id="logContent"
246 on-mousewheel="_handleMouseWheel">
149 <div id="logs"> 247 <div id="logs">
150 <!-- Content will be populated with JavaScript as logs are loaded. 248 <!-- Content will be populated with JavaScript as logs are loaded.
151 249
152 <div class="log-entry"> 250 <div class="log-entry">
153 <div class="log-entry-meta"> 251 <div class="log-entry-meta">
154 <div class="log-entry-meta-line">(Meta 0)</div> 252 <div class="log-entry-meta-line">(Meta 0)</div>
155 ... 253 ...
156 <div class="log-entry-meta-line">(Meta N)</div> 254 <div class="log-entry-meta-line">(Meta N)</div>
157 </div> 255 </div>
158 <div class="log-entry-content"> 256 <div class="log-entry-content">
159 <div class="log-entry-line">LINE #0</div> 257 LINE #0
160 ... 258 ...
161 <div class="log-entry-line">LINE #N</div> 259 LINE #N
162 </div> 260 </div>
163 </div> 261 </div>
164 ... 262 ...
165 263
264
265 Note that we can't use templating to show/hide the log dividers,
266 since our positional log insertion requires them to be present and
267 move along with insertions as points of reference.
166 --> 268 -->
269
270 <div id="logSplit" class="logFetchButtonContainer">
271 <!-- Insert point (prepend for head, append for tail). -->
272 <paper-button id="logSplitUp"
273 class="logFetchButton logSplitUpButton giant"
274 disabled="[[streamAnchorsNotClickable]]"
275 on-click="_handleUpClick">
276 <iron-icon icon="file-upload"></iron-icon>
277 </paper-button>
278 <paper-button id="logSplitDown"
279 class="logFetchButton logSplitDownButton giant"
280 disabled="[[streamAnchorsNotClickable]]"
281 on-click="_handleDownClick">
282 <iron-icon icon="file-download"></iron-icon>
283 </paper-button>
284 </div>
285
286 <div id="logBottom" class="logFetchButtonContainer">
287 <!--
288 Bottom of the log stream (red bottom line). When tail is complete,
289 all future logs get prepended to this.
290 -->
291 <paper-button id="logBottomButton"
292 class="logFetchButton logBottomButton giant"
293 disabled="[[streamAnchorsNotClickable]]"
294 on-click="_handleBottomClick">
295 <iron-icon icon="arrow-drop-down"></iron-icon>
296 </paper-button>
297 </div>
298 <div id="logEnd"></div>
167 </div> 299 </div>
168
169 <!-- Current red bottom line. -->
170 <div id="bottom"></div>
171 </div> 300 </div>
172 301
173 </div> 302 </div>
174 303
175 <template is="dom-if" if="{{statusBar}}"> 304 <template is="dom-if" if="{{statusBar}}">
176 <div id="status-bar">{{statusBar.value}}</div> 305 <div id="status-bar">{{statusBar.value}}</div>
177 </template> 306 </template>
178 </template> 307 </template>
179 308
180 </dom-module> 309 </dom-module>
(...skipping 18 matching lines...) Expand all
199 * project. For example, for stream "foo/bar/+/baz" in project "chromium", 328 * project. For example, for stream "foo/bar/+/baz" in project "chromium",
200 * the stream path would be: "chromium/foo/bar/+/baz". 329 * the stream path would be: "chromium/foo/bar/+/baz".
201 */ 330 */
202 streams: { 331 streams: {
203 type: Array, 332 type: Array,
204 value: [], 333 value: [],
205 notify: true, 334 notify: true,
206 observer: "_streamsChanged", 335 observer: "_streamsChanged",
207 }, 336 },
208 337
338 toolbarAnchor: {
339 type: Object,
340 value: null,
341 },
342
343 mobile: {
344 type: Boolean,
345 value: false,
346 },
347
209 /** 348 /**
210 * The number of logs to load before forcing a page refresh. 349 * The number of logs to load before forcing a page refresh.
211 * 350 *
212 * The smaller the value, the smoother the page will behave while logs are 351 * The smaller the value, the smoother the page will behave while logs are
213 * loading. However, the logs will also load slower because of forced 352 * loading. However, the logs will also load slower because of forced
214 * renders in between elements. 353 * renders in between elements.
215 */ 354 */
216 burst: { 355 burst: {
217 type: Number, 356 type: Number,
218 value: 1000, 357 value: 1000,
219 notify: true, 358 notify: true,
220 }, 359 },
221 360
361 /**
362 * If true, render metadata blocks alongside their log entries.
363 *
364 * This will cause significantly more HTML elements during rendering (so
365 * that each metadata element can show up next to its row) and greatly
366 * slow the viewer down.
367 */
368 metadata: {
369 type: Boolean,
370 value: false,
371 },
372
222 /** If true, show log metadata column. */ 373 /** If true, show log metadata column. */
223 showMetadata: { 374 showMetadata: {
224 type: Boolean, 375 type: Boolean,
225 value: false, 376 value: false,
226 observer: "_showMetadataChanged", 377 observer: "_showMetadataChanged",
227 }, 378 },
228 379
229 /** If true, wrap log lines to the screen. */ 380 /** If true, wrap log lines to the screen. */
230 wrapLines: { 381 wrapLines: {
231 type: Boolean, 382 type: Boolean,
232 value: false, 383 value: false,
233 observer: "_wrapLinesChanged", 384 observer: "_wrapLinesChanged",
234 }, 385 },
235 386
236 /** 387 /**
237 * If true, automatically scroll the page to the bottom of the logs 388 * If true, automatically scroll the page to the bottom of the logs
238 * while they are streaming. 389 * while they are streaming.
239 */ 390 */
240 follow: { 391 follow: {
241 type: Boolean, 392 type: Boolean,
242 value: false, 393 value: false,
243 observer: "_followChanged", 394 observer: "_followChanged",
244 }, 395 },
245 396
397 canSplit: {
398 type: Boolean,
399 value: false,
400 readOnly: true,
401 },
402
403 showStreamingControls: {
404 type: Boolean,
405 value: true,
406 readOnly: true,
407 },
408
409 isSplit: {
410 type: Boolean,
411 value: false,
412 readOnly: true,
413 },
414
415 streamAnchorsNotClickable: {
416 type: Boolean,
417 computed:
418 '_computeAnchorsNotClickable(playing, showStreamingControls)',
419 },
420
421 playing: {
422 type: Boolean,
423 value: false,
424 observer: "_playingChanged",
425 },
426
427 playingIconName: {
428 type: String,
429 computed: '_computePlayingIconName(playing)',
430 },
431
432 backfill: {
433 type: Boolean,
434 value: false,
435 observer: "_backfillChanged",
436 },
437
438 backfillIconName: {
439 type: String,
440 computed: '_computeBackfillIconName(backfill)',
441 },
442
246 /** 443 /**
247 * The current stream status. This is an Array of objects: 444 * The current stream status. This is an Array of objects:
248 * obj.name is the name of the stream. 445 * obj.name is the name of the stream.
249 * obj.desc is the status description of the stream. 446 * obj.desc is the status description of the stream.
250 */ 447 */
251 streamStatus: { 448 streamStatus: {
252 type: String, 449 type: String,
253 value: null, 450 value: null,
254 notify: true, 451 notify: true,
255 readOnly: true, 452 readOnly: true,
256 }, 453 },
257 454
258 /** 455 /**
259 * The text content of the status element at the bottom of the page. 456 * The text content of the status element at the bottom of the page.
260 */ 457 */
261 statusBar: { 458 statusBar: {
262 type: String, 459 type: String,
263 value: null, 460 value: null,
264 readOnly: true, 461 readOnly: true,
265 }, 462 },
266 }, 463 },
267 464
268 ready: function() { 465 created: function() {
269 this._scheduledWrite = null; 466 this._scrollTimeoutId = null;
270 this._buffer = null; 467
271 this._currentLogBuffer = null; 468 // Create "onScrollHandler", which just invokes "_onScroll" while bound
272 this._authCallback = null; 469 // to "this". We create it here so we can unregister it later, since
470 // "bind" returns a modified value.
471 this._onScrollHandler = function(e) { this._onScroll(e); }.bind(this);
472 },
473
474 attached: function() {
475 require(["inc/rpc/client", "inc/logdog-stream-view/viewer"],
476 function(client, viewer) {
477 // Instantiate our view, and install callbacks.
478 this._v = viewer;
479 this._model = new viewer.Model({
480 client: new client.luci_rpc.Client(this.$.client),
481 mobile: this.mobile,
482
483 pushLogEntries: this._pushLogEntries.bind(this),
484 clearLogEntries: this._clearLogEntries.bind(this),
485 updateControls: this._updateControls.bind(this),
486 locationIsVisible: this._locationIsVisible.bind(this),
487 });
488 this._streamsChanged();
489 }.bind(this));
490
491 window.addEventListener('scroll', this._onScrollHandler);
273 }, 492 },
274 493
275 detached: function() { 494 detached: function() {
276 this.stop(); 495 // Unregsiter event handlers.
496 window.removeEventListener('scroll', this._onScrollHandler);
497
498 // Reset state.
499 this.reset();
277 }, 500 },
278 501
279 stop: function() { 502 stop: function() {
280 this._cancelFetch(true); 503 this.reset();
281 }, 504 },
282 505
283 /** Clears state and begins fetching log data. */ 506 /** Clears state and begins fetching log data. */
284 reset: function() { 507 reset: function() {
285 this._resetLogState(); 508 if ( this._model ) {
286 509 this._model.reset();
287 this._resolveStreams().then(function(streams) { 510 }
288 this._resetToStreams(streams); 511 this._resetScroll();
289 }.bind(this)).catch(function(error) { 512 this._model = null;
290 this._loadStatusBar("Failed to resolve streams:" + error); 513 this._renderedLogs = false;
291 throw error; 514 },
515
516 /**
517 * Called each time a scroll event is fired. Since this can be really
518 * frequent, this will kick off a "scroll handler" in the background at an
519 * interval. Multiple scroll events within that interval will only result
520 * in one scroll handler invocation.
521 */
522 _onScroll: function(e) {
523 if ( this._scrollTimeoutId ) {
524 return;
525 }
526
527 window.setTimeout(function() {
528 this._handleScrollEvent(e);
529 }.bind(this), 100);
530 },
531
532 /** Actual scroll event handler. */
533 _handleScrollEvent: function(e) {
534 this._resetScroll();
535
536 // Update our button bar position to be relative to the parent's height.
537 this._adjustToTop(this.$.buttons);
538 this._adjustToTop(this.$.streamStatus);
539 },
540
541 _adjustToTop: function(elem) {
542 // Update our button bar position to be relative to the parent's height.
543 var pageRect = this.$.mainView.getBoundingClientRect();
544 var elemRect = elem.getBoundingClientRect();
545 var adjusted = (elem.offsetTop + pageRect.top - elemRect.top);
546 if ( adjusted < 0 ) {
547 adjusted = 0;
548 }
549 elem.style.top = adjusted;
550 },
551
552 /** Clears asynchornous scroll event status. */
553 _resetScroll: function() {
554 if ( this._scrollTimeoutId !== null ) {
555 window.clearTimeout(this._scrollTimeoutId);
556 this._scrollTimeoutId = null;
557 }
558 },
559
560 _handleMouseWheel: function(e) {
561 this.follow = false;
562 },
563
564 _handleDownClick: function(e) {
565 this._model.fetchLocation(this._v.Location.HEAD, true);
566 },
567
568 _handleUpClick: function(e) {
569 this._model.fetchLocation(this._v.Location.TAIL, true);
570 },
571
572 _handleBottomClick: function(e) {
573 this._model.fetchLocation(this._v.Location.BOTTOM, true);
574 },
575
576 /** Called when the bound log stream variables has changed. */
577 _streamsChanged: function() {
578 if( ! this._model ) {
579 return;
580 }
581
582 this._model.resolve(this.streams).then( function() {
583 // If we're not on mobile, start with playing state.
584 this.playing = (!this.mobile);
585
586 // Perform the initial fetch after resolution.
587 this._model.setAutomatic( this.playing );
588 this._model.setTailing( ! this.backfill );
589 this._model.fetch(false);
590 }.bind(this) );
591 },
592
593 _appendMetaLine: function(root, key, value) {
594 var line = document.createElement("div");
595 line.className = "log-entry-meta-line";
596
597 if ( key != null ) {
598 var e = document.createElement("strong");
599 e.textContent = key;
600 line.appendChild(e);
601 }
602
603 if ( value != null ) {
604 var e = document.createElement("span");
605 e.textContent = value;
606 line.appendChild(e);
607 }
608
609 root.appendChild(line);
610 },
611
612 _pushLogEntries: function(entries, insertion) {
613 // Mark that we've rendered logs (show bars now).
614 this._renderedLogs = true;
615
616 // Build our log entry chunk.
617 var logEntryChunk = document.createElement("div");
618 logEntryChunk.className = "log-entry-chunk";
619
620 var lastLogEntry = logEntryChunk;
621 var lines = new Array();
622
623 entries.forEach(function(le) {
624 var text = le.text;
625 if (!(text && text.lines)) {
626 return;
627 }
628
629 // If we're rendering metadata, render an element per log entry.
630 if( this.metadata ) {
631 var entryRow = document.createElement("div");
632 entryRow.className = "log-entry";
633
634 // Metadata column.
635 var metadataBlock = document.createElement("div");
636 metadataBlock.className = "log-entry-meta";
637
638 this._appendMetaLine(metadataBlock, "Timestamp:", le.timestamp);
639 this._appendMetaLine(metadataBlock, "Stream:", le.desc.name);
640 this._appendMetaLine(metadataBlock, "Index:", le.streamIndex);
641
642 // Log column.
643 var logDataBlock = document.createElement("div");
644 logDataBlock.className = "log-entry-content";
645
646 le.text.lines.forEach(function(line) {
647 lines.push(line.value);
648 });
649
650 logDataBlock.textContent = lines.join("\n");
651 lines.length = 0;
652
653 entryRow.appendChild(metadataBlock);
654 entryRow.appendChild(logDataBlock);
655
656 logEntryChunk.appendChild(entryRow);
657 lastLogEntry = entryRow;
658 } else {
659 // Add this to the lines. We'll assign this directly to logEntryChunk
660 // after the loop.
661 le.text.lines.forEach(function(line) {
662 lines.push(line.value);
663 });
664 }
292 }.bind(this)); 665 }.bind(this));
293 }, 666
294 667 if ( ! this.metadata ) {
295 /** Clears all current logs. */ 668 // Only one HTML element: the chunk.
296 _resetLogState: function() { 669 logEntryChunk.textContent = lines.join("\n");
297 this._cancelFetch(true); 670 lastLogEntry = logEntryChunk;
298 671 }
672
673 // To have styles apply correctly, we need to add it twice, see
674 // https://github.com/Polymer/polymer/issues/3100.
675 Polymer.dom(this.root).appendChild(logEntryChunk);
676
677 // Add the log entry to the appropriate place.
678 var anchor, scrollToTop = false;
679 var forceScroll = false;
680 switch ( insertion ) {
681 case this._v.Location.HEAD:
682 // InsertionPoint.HEAD: PREPEND to "logSplit".
683 this.$.logs.insertBefore(logEntryChunk, this.$.logSplit);
684
685 // If we're not split, scroll to the log bottom. Otherwise, scroll to
686 // the split.
687 anchor = lastLogEntry;
688 break;
689
690 case this._v.Location.TAIL:
691 // InsertionPoint.TAIL: APPEND to "logSplit".
692 anchor = this.$.logSplit;
693
694 // Identify the element *after* our insertion point and scroll to it.
695 // This provides a semblance of stability as we top-insert.
696 //
697 // As a special case, if the next element is the log bottom, just
698 // scroll to the split, since there is no content to stabilize.
699 if ( anchor.nextElementSibling !== this.$.logBottom ) {
700 anchor = anchor.nextElementSibling;
701 }
702
703 // Insert logs by adding them before the sibling following the log
704 // split (append to this.$.logSplit).
705 this.$.logs.insertBefore(logEntryChunk, this.$.logSplit.nextSibling);
706
707 // When tailing, always scroll to the anchor point.
708 scrollToTop = true;
709 forceScroll = true;
710 break;
711
712 case this._v.Location.BOTTOM:
713 // InsertionPoint.BOTTOM: PREPEND to "logBottom".
714 anchor = this.$.logBottom;
715 this.$.logs.insertBefore(logEntryChunk, anchor);
716 break;
717 }
718
719 this._maybeScrollToElement(anchor, scrollToTop, forceScroll);
720 },
721
722 _clearLogEntries: function() {
299 // Remove all current log elements. */ 723 // Remove all current log elements. */
300 while (this.$.logs.hasChildNodes()) { 724 for ( var cur = this.$.logs.firstChild; cur; ) {
301 this.$.logs.removeChild(this.$.logs.lastChild); 725 var del = cur;
302 } 726 cur = cur.nextElementSibling;
303 727 if ( del.classList && del.classList.contains('log-entry-chunk') ) {
304 // Clear our buffer and streamer state. 728 this.$.logs.removeChild(del);
305 this._buffer = null; 729 }
306 this._currentLogBuffer = null; 730 }
307 if (this._streamer) { 731 },
308 this._streamer.shutdown(); 732
309 } 733 _locationIsVisible: function(l) {
310 this._streamer = null; 734 var anchor;
311 }, 735 switch( l ) {
312 736 case this._v.Location.HEAD:
313 _resolveStreams: function() { 737 case this._v.Location.TAIL:
314 // Separate our configured streams into full stream paths and queries. 738 anchor = this.$.logSplit;
315 var parts = { 739
316 queries: [], 740 case this._v.Location.BOTTOM:
317 streams: [], 741 anchor = this.$.logBottom;
318 }; 742
319 var query = new LogDogQuery(this.project); 743 default:
320 this.streams.map(LogDogStream.splitProject).forEach(function(v) { 744 return false;
321 if (LogDogQuery.isQuery(v.path)) { 745 }
322 parts.queries.push(v); 746 return this._elementInViewport(anchor);
747 },
748
749 _updateControls: function(c) {
750 this._setCanSplit(c.canSplit);
751 this._setIsSplit(c.split);
752 this.toggleClass("logFetchButtonVisible",
753 (c.split && this._renderedLogs), this.$.logSplit);
754 this.toggleClass("logFetchButtonVisible",
755 (c.bottom && this._renderedLogs), this.$.logBottom);
756
757 this._setShowStreamingControls( !c.fullyLoaded );
758 if ( c.fullyLoaded ) {
759 this.playing = false;
760 }
761
762 switch( c.loadingState ) {
763 case this._v.LoadingState.NONE:
764 this._loadStatusBar(null);
765 break;
766 case this._v.LoadingState.RESOLVING:
767 this._loadStatusBar("Resolving stream names...");
768 break;
769 case this._v.LoadingState.LOADING:
770 this._loadStatusBar("Loading streams...");
771 break;
772 case this._v.LoadingState.RENDERING:
773 this._loadStatusBar("Rendering logs.");
774 break;
775 case this._v.LoadingState.NEEDS_AUTH:
776 this._loadStatusBar("Not authenticated. Please log in.");
777 break;
778 case this._v.LoadingState.ERROR:
779 this._loadStatusBar("Error loading streams (see console).");
780 break;
781 }
782
783 this._setStreamStatus(c.streamStatus);
784 },
785
786 /** Scrolls to the follow anchor point. */
787 _maybeScrollToFollow: function() {
788 // Determine our anchor element.
789 var e;
790 if ( this.isSplit && this.backfill ) {
791 // Centering on the split element, at the bottom of the page.
792 e = this.$.logSplit;
793 } else {
794 // Scroll to the bottom of the page.
795 e = this.$.logEnd;
796 }
797
798 this._maybeScrollToElement(e, false, false);
799 },
800
801 /**
802 * Scrolls to the specified element, centering it at the top or bottom of
803 * the view. By default,t his will only happen if "follow" is enabled;
804 * however, it can be forced via "force".
805 */
806 _maybeScrollToElement: function(element, topOfView, force) {
807 if (this.follow || force) {
808 if ( topOfView ) {
809 element.scrollIntoView({
810 behavior: "auto",
811 block: "end",
812 });
323 } else { 813 } else {
324 parts.streams.push(v); 814 // Bug? "block: start" doesn't seem to work the same as false.
325 } 815 element.scrollIntoView(false);
326 }); 816 }
327 817 }
328 // Resolve any outstanding queries into full stream paths. 818 },
329 // 819
330 // If we get an authentication error, register to have our query
331 // resolution callback invoked on signin changes until it works (or
332 // indefinitely).
333 var queries = parts.queries.map(function(v) {
334 var params = new LogDogQueryParams(v.project).
335 path(v.path).
336 streamType("text");
337 return new LogDogQuery(this.$.client, params);
338 }.bind(this));
339
340 var issueQuery = function() {
341 this._loadStatusBar("Resolving log streams from query...");
342 this._authCallback = null;
343
344 return Promise.all(queries.map(function(q) {
345 return q.getAll();
346 }.bind(this))).then(function(results) {
347 this._loadStatusBar(null);
348
349 // Add query results (if any) to streams.
350 results.forEach(function(streams) {
351 (streams || []).forEach(function(stream) {
352 parts.streams.push(stream.stream);
353 });
354 });
355 parts.streams.sort(LogDogStream.cmp);
356
357 // Remove any duplicates.
358 var seenStreams = {};
359 var result = [];
360 parts.streams.forEach(function(s) {
361 var fullName = s.fullName();
362 if (!seenStreams[fullName]) {
363 seenStreams[fullName] = s;
364 result.push(s);
365 }
366 });
367 return result;
368 }.bind(this)).catch(function(error) {
369 if (error instanceof LogDogError && error.isUnauthenticated()) {
370 // Retry on auth event.
371 this._loadStatusBar("Not authorized to execute query. Log in " +
372 "with an authorized account.");
373 return new Promise(function(resolve) {
374 this._authCallback = resolve;
375 }.bind(this)).then(issueQuery);
376 }
377
378 throw error;
379 }.bind(this));
380 }.bind(this);
381 return issueQuery();
382 },
383
384 _resetToStreams: function(streams) {
385 // Unique streams.
386 if (!streams.length) {
387 this._loadStatusBar("No log streams.");
388 return;
389 }
390
391 console.log("Loading log streams:", streams);
392 this._loadStatusBar("Loading stream data...");
393 streams.sort(LogDogStream.cmp);
394
395 // Create a _BufferedStream for each stream.
396 var bufStreams = streams.map(function(stream, idx) {
397 return new _BufferedStream(stream, this.$.client,
398 (streams.length > 1), function(bs) {
399 this._updateStreamStatus(bs, idx);
400 }.bind(this));
401 }.bind(this));
402 this._buffer = new _LogStreamBuffer();
403 this._buffer.setStreams(bufStreams)
404
405 this._streamer = new _LogStreamer(this._buffer, this.burst, function(v) {
406 this._loadStatusBar(v);
407 }.bind(this));
408
409 // Construct our initial status content.
410 this._setStreamStatus(bufStreams.map(function(bs, idx) {
411 return {
412 name: (" [.../+/" + bs.stream.name() + "]"),
413 desc: bs.description(),
414 };
415 }.bind(this)));
416
417 // Kick off our log fetching.
418 this._scheduleWriteNextLogs();
419 },
420
421 /** Cancels any currently-executing log stream fetch. */
422 _cancelFetch: function(clear) {
423 this._cancelScheduledWrite();
424 this._authCallback = null;
425
426 if (clear) {
427 this._setStreamStatus(null);
428 this._loadStatusBar(null);
429 }
430 },
431
432 /** Cancels any scheduled asynchronous write. */
433 _cancelScheduledWrite: function() {
434 if (this._scheduledWrite) {
435 this.cancelAsync(this._scheduledWrite);
436 this._scheduledWrite = null;
437 }
438 },
439
440 /** Called when the bound log stream variables has changed. */
441 _streamsChanged: function(v, old) {
442 this.reset();
443 },
444
445 /** Schedules the next asynchronous log write. */
446 _scheduleWriteNextLogs: function() {
447 // This is called after refresh, so use this opportunity to maybe scroll
448 // to the bottom.
449 this._maybeScrollToBottom();
450
451 if (! this._scheduledWrite) {
452 this._scheduledWrite = this.async(function() {
453 this._writeNextLogs()
454 }.bind(this));
455 }
456 },
457
458 /**
459 * This is an iterative function that grabs the next set of logs and renders
460 * them. Afterwards, it will continue rescheduling itself until there are
461 * no more logs to render.
462 */
463 _writeNextLogs: function() {
464 this._cancelScheduledWrite();
465
466 this._streamer.load().then(function(entries) {
467 // If there are no entries, then we're done.
468 if (! entries) {
469 // Cancel all fetching state. If our streamer is finished, also clear
470 // messages and status.
471 if (this._streamer.finished) {
472 if (this._streamer.someStreamsFailed) {
473 this._cancelFetch(false);
474 this._loadStatusBar("Some streams failed to load.");
475 } else {
476 this._cancelFetch(true);
477 }
478 } else {
479 // No more logs, but also we are not finished. Retry after auth.
480 this._authCallback = this._scheduleWriteNextLogs.bind(this);
481 }
482 return;
483 }
484
485 var logEntryChunk = document.createElement("div");
486 entries.forEach(function(le) {
487 this._appendLogEntry(logEntryChunk, le);
488 }.bind(this));
489
490 // To have styles apply correctly, we need to add it twice, see
491 // https://github.com/Polymer/polymer/issues/3100.
492 Polymer.dom(this.root).appendChild(logEntryChunk);
493 this.$.logs.appendChild(logEntryChunk);
494
495 // Yield so that our browser can refresh. We can't directly use
496 // this.async since a timeout of "0" causes immediate execution instead
497 // of yielding.
498 setTimeout(function() {
499 this._scheduleWriteNextLogs();
500 }.bind(this), 0);
501 }.bind(this));
502 },
503
504 _appendLogEntry: function(root, le) {
505 var text = le.text;
506 if (!(text && text.lines)) {
507 return 0;
508 }
509
510 // Create elements manually to avoid Polymer overhead for large logs.
511 var entryRow = document.createElement("div");
512 entryRow.className = "log-entry";
513
514 // Metadata column.
515 var metadataBlock = document.createElement("div");
516 metadataBlock.className = "log-entry-meta";
517 entryRow.appendChild(metadataBlock);
518
519 var timestampDiv = document.createElement("div");
520 timestampDiv.className = "log-entry-meta-line";
521 timestampDiv.textContent = le.timestamp;
522 metadataBlock.appendChild(timestampDiv);
523
524 var nameDiv = document.createElement("div");
525 nameDiv.className = "log-entry-meta-line";
526 nameDiv.textContent = le.desc.name;
527 metadataBlock.appendChild(nameDiv);
528
529 var streamDiv = document.createElement("div");
530 streamDiv.className = "log-entry-meta-line";
531 streamDiv.textContent = le.streamIndex;
532 metadataBlock.appendChild(streamDiv);
533
534 // Log column.
535 var logDataBlock = document.createElement("div");
536 logDataBlock.className = "log-entry-content";
537 if (le.text) {
538 for (var i = 0; i < le.text.lines.length; i++) {
539 var lineDiv = document.createElement("div");
540 lineDiv.className = "log-entry-line";
541 lineDiv.textContent = le.text.lines[i].value;
542 logDataBlock.appendChild(lineDiv);
543 }
544 }
545 entryRow.appendChild(logDataBlock);
546 root.appendChild(entryRow);
547
548 return le.text.lines.length;
549 },
550
551 _updateStreamStatus: function(bs, idx) {
552 var origStatus = this.streamStatus[idx];
553 this.splice("streamStatus", idx, 1, {
554 name: origStatus.name,
555 desc: bs.description(),
556 });
557 },
558
559 /** Scrolls to the bottom if "follow" is enabled. */
560 _maybeScrollToBottom: function() {
561 if (this.follow) {
562 this.$.bottom.scrollIntoView({
563 "behavior": "smooth",
564 "block": "end",
565 });
566 }
567 },
568
569 /** 820 /**
570 * Callback when "showMetadata" has changed. This adds/removes the 821 * Callback when "showMetadata" has changed. This adds/removes the
571 * "showMeta" CSS class from the metadata column. 822 * "showMeta" CSS class from the metadata column.
572 */ 823 */
573 _showMetadataChanged: function(v) { 824 _showMetadataChanged: function(v) {
574 this.toggleClass("showMeta", v, this.$.logs); 825 this.toggleClass("showMeta", v, this.$.logs);
575 }, 826 },
827
576 /** 828 /**
577 * Callback when "wrapLines" has changed. This adds/removes the 829 * Callback when "wrapLines" has changed. This adds/removes the
578 * "wrapLines" CSS class to the log data. 830 * "wrapLines" CSS class to the log data.
579 */ 831 */
580 _wrapLinesChanged: function(v) { 832 _wrapLinesChanged: function(v) {
581 this.toggleClass("wrapLines", v, this.$.logs); 833 this.toggleClass("wrapLines", v, this.$.logs);
582 }, 834 },
835
836 /** Callback when "follow" has changed. */
837 _playingChanged: function(v) {
838 if( ! this._model ) {
839 return;
840 }
841
842 // If we're playing, begin log fetching.
843 this._model.setAutomatic(v);
844 },
845
846 _computePlayingIconName: function(playing) {
847 return ( (playing) ?
Ryan Tseng 2016/12/06 03:38:11 is this backwards?
848 "av:pause-circle-outline" : "av:play-circle-outline" );
849 },
850
851 /** Callback when "follow" has changed. */
852 _backfillChanged: function(v) {
853 if( ! this._model ) {
854 return;
855 }
856
857 // If we're backfilling, then we're not tailing.
858 this._model.setTailing(!v);
859 },
860
861 _computeBackfillIconName: function(backfill) {
862 return ( (backfill) ?
863 "editor:border-bottom" : "editor:border-top" );
864 },
865
583 /** Callback when "follow" has changed. */ 866 /** Callback when "follow" has changed. */
584 _followChanged: function(v) { 867 _followChanged: function(v) {
585 this._maybeScrollToBottom(); 868 if ( ! this._model ) {
586 }, 869 return;
870 }
587 871
588 /** Callback for when the mouse wheel has scrolled. Disables follow. */ 872 if ( v ) {
589 _handleMouseWheel: function() { 873 // If follow is toggled on, automatically begin playing.
590 this.follow = false; 874 this.playing = true;
875 this._maybeScrollToFollow();
876 }
877 },
878
879 /** Callback when "split" button has been clicked. */
880 _splitClicked: function() {
881 if( ! this._model ) {
882 return;
883 }
884
885 // After a split, toggle off playing.
886 this._model.split();
887 this._model.setTailing(true);
888 this.playing = false;
889 },
890
891 /** Callback when "split" button has been clicked. */
892 _scrollToSplit: function() {
893 this._maybeScrollToElement(this.$.logSplit, true, true);
894 },
895
896 _computeAnchorsNotClickable: function(playing, showStreamingControls,
897 rendering) {
898 // Anchors are not clickable if we're playing or the controls are
899 // not visible.
900 return ( playing || (!showStreamingControls) || rendering );
901 },
902
903 /** Filter function to invert a value. */
904 _not: function(v) {
905 return (!v);
591 }, 906 },
592 907
593 /** 908 /**
594 * Loads text content into the status bar. 909 * Loads text content into the status bar.
595 * 910 *
596 * If null is passed, the status bar will be cleared. If text is passed, the 911 * If null is passed, the status bar will be cleared. If text is passed, the
597 * status bar will become visible with the supplied content. 912 * status bar will become visible with the supplied content.
598 */ 913 */
599 _loadStatusBar: function(v) { 914 _loadStatusBar: function(v) {
600 var st = null; 915 var st = null;
601 if (v) { 916 if (v) {
602 st = { 917 st = {
603 value: v, 918 value: v,
604 }; 919 };
605 } 920 }
606 this._setStatusBar(st); 921 this._setStatusBar(st);
607 }, 922 },
608 923
609 _onSignin: function() { 924 _onSignin: function() {
610 var fn = this._authCallback; 925 if ( this._model ) {
611 if (fn) { 926 this._model.notifyAuthenticationChanged();
612 this._authCallback = null;
613 fn();
614 } 927 }
615 }, 928 },
616 });
617 929
618 /** 930 _elementInViewport: function(el) {
619 * Continuously loads log streams from a _LogStreamBuffer and exposes them via 931 var top = el.offsetTop;
620 * callback. 932 var left = el.offsetLeft;
621 */ 933 var width = el.offsetWidth;
622 function _LogStreamer(buffer, burst, statusCallback) { 934 var height = el.offsetHeight;
623 this._buffer = buffer;
624 this._burst = (burst || 0);
625 this._missingDelay = 5000;
626 this._statusCallback = statusCallback;
627 935
628 this.finished = false; 936 while(el.offsetParent) {
629 this.someStreamsFailed = false; 937 el = el.offsetParent;
630 938 top += el.offsetTop;
631 this._currentLogBuffer = null; 939 left += el.offsetLeft;
632 }
633
634 _LogStreamer.prototype.shutdown = function() {
635 this.finshed = true;
636 };
637
638 _LogStreamer.prototype._setStatus = function(v) {
639 if (this._statusCallback) {
640 this._statusCallback(v);
641 }
642 };
643
644 _LogStreamer.prototype.load = function() {
645 if (this.finished) {
646 this._setStatus(null);
647 return Promise.resolve(null);
648 }
649
650 // If we have buffered logs, return them.
651 var current = this._currentLogBuffer;
652 if (current) {
653 // We will track how many log entries that we've rendered. If we exceed
654 // this amount, we will force a refresh so the logs appear streaming and
655 // the app remains responsive.
656 var rendered = 0;
657
658 var entries = [];
659 for (var le = current.next(); (le); le = current.next()) {
660 entries.push(le);
661 if (le.text && le.text.lines) {
662 rendered += le.text.lines.length;
663 }
664
665 if (this._burst > 0 && rendered >= this._burst) {
666 break;
667 }
668 } 940 }
669 941
670 // Have we exhausted this buffer? 942 return (
671 if (! current.peek()) { 943 top < (window.pageYOffset + window.innerHeight) &&
672 this._currentLogBuffer = null; 944 left < (window.pageXOffset + window.innerWidth) &&
673 } 945 (top + height) > window.pageYOffset &&
946 (left + width) > window.pageXOffset
947 );
948 },
674 949
675 // Return the bundle of entries. 950 });
676 return Promise.resolve(entries);
677 }
678
679 // We didn't have any buffered logs, so either all of our streams are
680 // finished or our buffer is empty and needs to be refreshed.
681 this._setStatus("Loading log stream data...");
682 return this._buffer.nextBuffer().then(function(buf) {
683 this.someStreamsFailed = (!!this._buffer._failures.length);
684
685 // Check result.
686 if (buf === null) {
687 if (this._buffer.finished) {
688 // No more buffers, we are done.
689 console.log("All streams have been exhausted.");
690 this.finished = true;
691 this._setStatus(null);
692 return null;
693 }
694
695 // The buffer was incomplete. Should we retry after a delay, or do
696 // we need to wait for an explicit edge (e.g., auth)?
697 if (this._buffer.autoRetry) {
698 // Sleep for 5 seconds and try again (waiting).
699 console.log("Log stream delayed; sleeping", this._missingDelay,
700 "and retry.");
701 this._setStatus("Missing log streams, retrying after delay...");
702 return new LuciSleepPromise(this._missingDelay).then(function() {
703 if (this.finished) {
704 console.log("Streamer is deactivated, discarding.");
705 return null;
706 }
707
708 return this.load();
709 }.bind(this));
710 }
711
712 this._setStatus("Some log streams could not be loaded.");
713 return null;
714 }
715
716 // Install the new buffer and re-enter.
717 this._currentLogBuffer = buf;
718 return this.load();
719 }.bind(this)).catch(function(error) {
720 this._setStatus("[" + error.name + "] fetching streams: " +
721 error.message);
722 throw error;
723 }.bind(this));
724 };
725
726 /**
727 * Manages an aggregate log stream buffer, consisting of logs punted from a
728 * set of zero or more _BufferedStream instances.
729 */
730 function _LogStreamBuffer() {
731 this._streams = null;
732 this._active = null;
733 this._nextBufferPromise = null;
734 this._failures = [];
735
736 this.finished = false;
737 this._resetIterativeState();
738 }
739
740 _LogStreamBuffer.prototype.setStreams = function(streams) {
741 // TODO(dnj): Make this do a delta with previous streams so we don't lose
742 // their already-loaded logs if the page changes.
743 this._streams = streams.map(function(bs, i) {
744 return {
745 bs: bs,
746 active: true,
747 buffer: new _BufferedLogs(),
748 };
749 });
750 this._active = this._streams;
751 this._nextBufferPromise = null;
752 };
753
754 _LogStreamBuffer.prototype._resetIterativeState = function() {
755 this.autoRetry = false;
756 };
757
758 /**
759 * Returns a Promise that resolves into a _BufferedLogs instance containing
760 * the next set of logs, in order, from the source log streams.
761 *
762 * The _BufferedLogs bundle may have status flags set, and should be checked.
763 *
764 * The Promise will also resolve to "null" if there are no more logs in the
765 * source streams.
766 *
767 * If there are errors fetching logs, the Promise will be rejected, and an
768 * error will be returned.
769 */
770 _LogStreamBuffer.prototype.nextBuffer = function() {
771 // If we're already are fetching the next buffer, return that Promise.
772 if (this._nextBufferPromise) {
773 return this._nextBufferPromise;
774 }
775
776 // Filter our any finished streams from our active list. A stream is
777 // finished if it is finished streaming and we don't have a retained buffer
778 // from it.
779 this._active = this._active.filter(function(entry) {
780 return (entry.buffer.peek() || (! (entry.bs.finished || entry.bs.error)));
781 })
782
783 if (! this._active.length) {
784 this.finished = true;
785 }
786 if (this.finished) {
787 // No active streams, so we're finished. Permanently set our promise to
788 // the finished state.
789 this._nextBufferPromise = Promise.resolve(null);
790 return this._nextBufferPromise;
791 }
792
793 // Fill all buffers for all active streams. This may result in an RPC to
794 // load new buffer content for streams whose buffers are empty.
795 //
796 // RPC failures are handled here:
797 // - If the stream reports "not found", we will terminate early and set
798 // out status to "waiting". Our owner should retry after a delay.
799 // - Otherwise, we will set our status to "error". Our owner should report
800 // that an error has occurred while loading stream data.
801 this._resetIterativeState();
802
803 var incomplete = false;
804 this._nextBufferPromise = Promise.all(this._active.map(function(entry) {
805 // If the entry's buffer still has data, use it immediately.
806 if (entry.buffer.peek()) {
807 return entry.buffer;
808 }
809
810 // Get the next log buffer for each stream. This may result in an RPC.
811 return entry.bs.nextBuffer().then(function(buffer) {
812 // Retain this buffer, if valid. The stream may have returned a null
813 // buffer if it failed to fetch for a legitimate reason. In this case,
814 // we will not retain it (since we want entry.buffer to be valid), but
815 // will forward the "null" to our aggregate function.
816 if (buffer) {
817 entry.buffer = buffer;
818 } else {
819 incomplete = true;
820
821 // If this stream is waiting, but not on auth, mark that we should
822 // automatically retry.
823 if (entry.bs.waiting && !entry.bs.auth) {
824 this.autoRetry = true;
825 }
826 }
827 return buffer;
828 }.bind(this)).catch(function(error) {
829 // Log stream source of error. Raise a generic "failed to buffer"
830 // error. This will become a permanent failure.
831 console.error("Error loading buffer for", entry.bs.stream.fullName(),
832 "(", entry.bs, "): ", error);
833 this._failures.push(entry.bs);
834 return null;
835 }.bind(this));
836 }.bind(this))).then(function(buffers) {
837 this._nextBufferPromise = null;
838
839 // Check each buffer. If any are null, that stream failed to deliver.
840 if (incomplete) {
841 // We succeeded, but are incomplete. At least one stream failed to
842 // deliver and should have state flags set accordingly.
843 return null;
844 }
845
846 // Remove any null buffers. These would be placed here when a stream fails
847 // to load. Aggregate as much data from each of our streams as possible.
848 buffers = buffers.filter(v => (!!v));
849 return this._aggregateBuffers(buffers);
850 }.bind(this));
851 return this._nextBufferPromise;
852 };
853
854 _LogStreamBuffer.prototype._aggregateBuffers = function(buffers) {
855 switch (buffers.length) {
856 case 0:
857 // No buffers, so no logs.
858 return new _BufferedLogs(null);
859 case 1:
860 // As a special case, if we only have one buffer, and we assume that its
861 // entries are sorted, then that buffer is a return value.
862 return new _BufferedLogs(buffers[0].getAll());
863 }
864
865 // Preload our peek array.
866 var incomplete = false;
867 var peek = buffers.map(function(buf) {
868 var le = buf.peek();
869 if (! le) {
870 incomplete = true;
871 }
872 return le;
873 });
874 if (incomplete) {
875 // One of our input buffers had no log entries.
876 return new _BufferedLogs(null);
877 }
878
879 // Assemble our aggregate buffer array.
880 // TODO: A binary heap would be pretty great for this.
881 var entries = [];
882 while (true) {
883 // Choose the next stream.
884 var earliest = 0;
885 for (var i = 1; i < buffers.length; i++) {
886 if (_LogStreamBuffer.compareLogs(peek[i], peek[earliest]) < 0) {
887 earliest = i;
888 }
889 }
890
891 // Get the next log from the earliest stream.
892 entries.push(buffers[earliest].next());
893
894 // Repopulate that buffer's "peek" value. If the buffer has no more
895 // entries, then we're done.
896 var next = buffers[earliest].peek();
897 if (!next) {
898 return new _BufferedLogs(entries);
899 }
900 peek[earliest] = next;
901 }
902 };
903
904 _LogStreamBuffer.compareLogs = function(a, b) {
905 // If they are part of the same stream, compare prefix indexes.
906 if (a.source.stream.samePrefixAs(b.source.stream)) {
907 return (a.prefixIndex - b.prefixIndex);
908 }
909
910 // Compare based on timestamp.
911 return a.timestamp - b.timestamp;
912 };
913
914
915 /**
916 * A buffer of ordered log entries from all streams.
917 *
918 * Assumes total ownership of the input log buffer, which can be null to
919 * indicate no logs.
920 */
921 function _BufferedLogs(logs) {
922 this._logs = logs;
923 this._index = 0;
924 }
925
926 _BufferedLogs.prototype.getAll = function() {
927 // Pop all logs.
928 var logs = this._logs;
929 this._logs = null;
930 return logs;
931 };
932
933 _BufferedLogs.prototype.peek = function() {
934 return (this._logs) ? (this._logs[this._index]) : (null);
935 };
936
937 _BufferedLogs.prototype.next = function() {
938 if (! (this._logs && this._logs.length)) {
939 return null;
940 }
941
942 // Get the next log and increment our index.
943 var log = this._logs[this._index++];
944 if (this._index >= this._logs.length) {
945 this._logs = null;
946 }
947 return log;
948 };
949
950
951 /**
952 * Stateful log fetching manager for a single log stream.
953 */
954 function _BufferedStream(stream, client, oneOfMany, statusCallback) {
955 this.stream = stream;
956
957 this.error = null;
958 this.finished = false;
959
960 this._fetcher = new LogDogFetcher(client, stream);
961 this._oneOfMany = oneOfMany;
962 this._statusCallback = statusCallback;
963 this._lastFetchIndex = null;
964 }
965
966 _BufferedStream.INITIAL_FETCH_SIZE = 4096;
967
968 _BufferedStream.prototype._resetIterativeState = function() {
969 this.waiting = false;
970 this.auth = false;
971 this._fireStatusUpdated();
972
973 this._currentFetch = null;
974 };
975
976 _BufferedStream.prototype.nextBuffer = function() {
977 if (this._currentFetch) {
978 return this._currentFetch;
979 }
980
981 // Reset per-round state and begin next round fetch.
982 this._resetIterativeState();
983
984 // If this is the first fetch, and we're not the only log stream being
985 // rendered, fetch a small amount so we can (probably) start rendering
986 // without waiting for a lot of huge chunks.
987 this._fetcher.byteCount = (
988 (this._lastFetchIndex === null) && this._oneOfMany) ?
989 (_BufferedStream.INITIAL_FETCH_SIZE) : (null);
990
991 this._currentFetch = this._fetcher.next().then(function(result) {
992 this._currentFetch = null;
993
994 // Update our stream information.
995 this.finished = this._fetcher.finished;
996
997 // Augment each returned log entry with self-descriptive metadata.
998 var logs = result.entries;
999 if (logs && logs.length) {
1000 logs.forEach(function(le) {
1001 le.desc = result.desc;
1002 le.state = result.state;
1003 le.source = this;
1004 }.bind(this));
1005
1006 // Record the latest fetch index.
1007 this._lastFetchIndex = logs[logs.length - 1].streamIndex;
1008 }
1009
1010 this._fireStatusUpdated();
1011 return new _BufferedLogs(logs);
1012 }.bind(this)).catch(function(error) {;
1013 // If this is a "not found" error, we assume that the stream is valid, but
1014 // hasn't been ingested into LogDog yet. Return "null".
1015 if (error instanceof LogDogError) {
1016 if (error.isUnauthenticated()) {
1017 this.waiting = true;
1018 this.auth = true;
1019 } else if (error.isNotFound()) {
1020 this.waiting = true;
1021 }
1022
1023 // If this is an error that we understand, recover from it, return
1024 // null, and set our status flags.
1025 if (this.waiting) {
1026 // Recover from this error.
1027 this._currentFetch = null;
1028 this._fireStatusUpdated();
1029 return null;
1030 }
1031 }
1032
1033 // Retain this error forever.
1034 this.error = error;
1035 throw error;
1036 }.bind(this));
1037 return this._currentFetch;
1038 };
1039
1040 _BufferedStream.prototype._fireStatusUpdated = function() {
1041 if (this._statusCallback) {
1042 this._statusCallback(this);
1043 }
1044 };
1045
1046 _BufferedStream.prototype.description = function() {
1047 if (this._waiting) {
1048 return "(Waiting)";
1049 }
1050
1051 var pieces = []
1052 var tidx = this._fetcher.terminalIndex();
1053 if (this._lastFetchIndex) {
1054 if (tidx >= 0) {
1055 pieces.push(this._lastFetchIndex + " / " + tidx);
1056 } else {
1057 pieces.push(this._lastFetchIndex + "...");
1058 }
1059 }
1060
1061 if (this.error) {
1062 pieces.push("(Error)");
1063 } else if (this.auth) {
1064 pieces.push("(Auth Error)");
1065 } else if (this.waiting) {
1066 pieces.push("(Waiting)");
1067 } else if (!this._fetcher.state) {
1068 pieces.push("(Fetching)");
1069 } else if (this._fetcher.finished) {
1070 pieces.push("(Finished)");
1071 } else {
1072 pieces.push("(Streaming)");
1073 }
1074 return pieces.join(" ");
1075 };
1076 </script> 951 </script>
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698