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

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

Issue 2570963003: Revert of Rewrite LogDog log viewer app. (Closed)
Patch Set: 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/iron-icons/iron-icons.html"> 9 <link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html" >
10 <link rel="import" href="../bower_components/iron-icons/av-icons.html"> 10
11 <link rel="import" href="../bower_components/iron-icons/editor-icons.html"> 11 <link rel="import" href="../logdog-stream/logdog-stream.html">
12 <link rel="import" href="../bower_components/paper-button/paper-button.html"> 12 <link rel="import" href="../logdog-stream/logdog-error.html">
13 <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button .html"> 13 <link rel="import" href="../luci-sleep-promise/luci-sleep-promise.html">
14 <link rel="import" href=""> 14 <link rel="import" href="logdog-stream-fetcher.html">
15 <link rel="import" href="logdog-stream-query.html">
15 16
16 <!-- 17 <!--
17 An element for rendering muxed LogDog log streams. 18 An element for rendering muxed LogDog log streams.
18 --> 19 -->
19 <dom-module id="logdog-stream-view"> 20 <dom-module id="logdog-stream-view">
20 21
21 <template> 22 <template>
22 <style is="custom-style"> 23 <style>
23 #mainView { 24 .buttons {
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 {
35 background-color: white; 25 background-color: white;
36 } 26 }
37 27
38 .paper-button-highlight[toggles][active] { 28 #stream-status {
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 {
48 position: fixed; 29 position: fixed;
49 right: 16px; 30 right: 16px;
50 background-color: #EEEEEE; 31 background-color: #EEEEEE;
51 opacity: 0.7; 32 opacity: 0.7;
52 } 33 }
53 34
54 #logContent { 35 #logContent {
55 padding-top: 54px; /* Pad around buttons */ 36 padding-top: 20px;
56 background-color: white; 37 background-color: white;
57 } 38 }
58 39
59 .log-entry { 40 .log-entry {
60 padding: 0 0 0 0; 41 padding: 0 0 0 0;
61 clear: left; 42 clear: left;
62 } 43 }
63 44
64 .log-entry-meta { 45 .log-entry-meta {
65 background-color: lightgray; 46 vertical-align: top;
66 padding: 5px; 47 padding: 0 8px 0 0;
67 border-width: 2px 0px 0px 0px; 48 margin: 0 0 0 0;
68 border-color: darkgray; 49 float: left;
69 border-style: dotted;
70 user-select: none;
71
72 font-style: italic; 50 font-style: italic;
73 font-family: Courier New, Courier, monospace; 51 font-family: Courier New, Courier, monospace;
74 font-size: 10px; 52 font-size: 10px;
75 53
76 /* Can be toggled to "flex" by applying .showMeta class to #logs. */ 54 /* Fixed width, break word if necessary. */
55 width: 150px;
56 word-break: break-word;
57
58 /* Can be toggled by applying .showMeta class to #logs. */
77 display: none; 59 display: none;
78 } 60 }
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 }
88 .showMeta .log-entry-meta { 61 .showMeta .log-entry-meta {
89 display: flex; 62 display: block;
90 } 63 }
91 64
92 /* .log-entry-content { */ 65 .log-entry-content {
93 .log-entry-chunk {
94 padding: 0 0 0 0; 66 padding: 0 0 0 0;
95 margin: 0 0 0 0; 67 margin: 0 0 0 0;
96 float: none; 68 float: none;
97 font-family: Courier New, Courier, monospace; 69 font-family: Courier New, Courier, monospace;
98 font-size: 16px; 70 font-size: 16px;
99 list-style: none; 71 list-style: none;
72 }
100 73
101 border-bottom: 1px solid #CCCCCC; 74 .log-entry-line {
75 padding-left: 0;
102 76
103 /* Can be toggled by applying .wrapLines class to #logs. */ 77 /* Can be toggled by applying .wrapLines class to #logs. */
104 white-space: pre; 78 white-space: pre;
105 } 79 }
106 80 .wrapLines .log-entry-line {
107 /*.wrapLines .log-entry-content { */
108 .wrapLines .log-entry-chunk {
109 white-space: pre-wrap; 81 white-space: pre-wrap;
110 word-break: break-word; 82 word-break: break-word;
111 } 83 }
112 84
113 .logFetchButtonContainer { 85 .log-entry-line:nth-last-child(2) {
114 height: auto; 86 border-bottom: 1px solid #CCCCCC;
115 display: none;
116 flex-direction: row;
117 background-color: rgba(0, 0, 0, 0.2);
118 padding: 2px;
119 } 87 }
120 88
121 .logFetchButtonVisible { 89 #bottom {
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 {
138 background-color: lightcoral; 90 background-color: lightcoral;
139 } 91 height: 2px;
140 92 margin-bottom: 10px;
141 #logEnd {
142 margin-bottom: 30px;
143 background-color: gray;
144 }
145
146 .clickable-log-anchor {
147 height: 24px;
148 } 93 }
149 94
150 #status-bar { 95 #status-bar {
151 /* Overlay at the bottom of the page. */ 96 /* Overlay at the bottom of the page. */
152 position: fixed; 97 position: fixed;
153 z-index: 9999; 98 z-index: 9999;
154 overflow: hidden; 99 overflow: hidden;
155 bottom: 0; 100 bottom: 0;
156 left: 0; 101 left: 0;
157 width: 100%; 102 width: 100%;
158 103
159 text-align: center; 104 text-align: center;
160 font-size: 16px; 105 font-size: 16px;
161 background-color: rgba(245, 245, 220, 0.7); 106 background-color: rgba(245, 245, 220, 0.7);
162 } 107 }
163 </style> 108 </style>
164 109
110 <google-signin-aware
111 id="aware"
112 on-google-signin-aware-success="_onSignin"></google-signin-aware>
113
165 <rpc-client 114 <rpc-client
166 id="client" 115 id="client"
167 auto-token 116 auto-token
168 host="[[host]]"></rpc-client> 117 host="[[host]]"></rpc-client>
169 118
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
178 <!-- Stream view options. --> 119 <!-- Stream view options. -->
179 <div id="mainView"> 120 <div class="main-view">
180 <div id="buttons"> 121 <div class="buttons">
181 <!-- If we have exactly one stream, we will enable users to split. --> 122 <paper-checkbox checked="{{showMetadata}}">
182 <template is="dom-if" if="{{showStreamingControls}}"> 123 Show Metadata
183 <template is="dom-if" if="{{canSplit}}"> 124 </paper-checkbox>
184 <paper-icon-button title="Split, load logs from end" 125 <paper-checkbox checked="{{wrapLines}}">
185 icon="editor:vertical-align-bottom" 126 Wrap Lines
186 on-tap="_splitClicked"> 127 </paper-checkbox>
187 </paper-icon-button> 128 <paper-checkbox checked="{{follow}}">
188 </template> 129 Follow
189 130 </paper-checkbox>
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>
228 </div> 131 </div>
229 132
230 <!-- Display current fetching status, if stream data is still loading. --> 133 <!-- Display current fetching status, if stream data is still loading. -->
231 <div id="streamStatus"> 134 <template is="dom-if" if="{{streamStatus}}">
232 <template is="dom-if" if="{{streamStatus}}"> 135 <div id="stream-status">
233 <table> 136 <table>
234 <template is="dom-repeat" items="{{streamStatus}}"> 137 <template is="dom-repeat" items="{{streamStatus}}">
235 <tr> 138 <tr>
236 <td>{{item.name}}</td> 139 <td>{{item.name}}</td>
237 <td>{{item.desc}}</td> 140 <td>{{item.desc}}</td>
238 </tr> 141 </tr>
239 </template> 142 </template>
240 </table> 143 </table>
241 </template> 144 </div>
242 </div> 145 </template>
243 146
244 <!-- Muxed log content. --> 147 <!-- Muxed log content. -->
245 <div id="logContent" 148 <div id="logContent" on-mousewheel="_handleMouseWheel">
246 on-mousewheel="_handleMouseWheel">
247 <div id="logs"> 149 <div id="logs">
248 <!-- Content will be populated with JavaScript as logs are loaded. 150 <!-- Content will be populated with JavaScript as logs are loaded.
249 151
250 <div class="log-entry"> 152 <div class="log-entry">
251 <div class="log-entry-meta"> 153 <div class="log-entry-meta">
252 <div class="log-entry-meta-line">(Meta 0)</div> 154 <div class="log-entry-meta-line">(Meta 0)</div>
253 ... 155 ...
254 <div class="log-entry-meta-line">(Meta N)</div> 156 <div class="log-entry-meta-line">(Meta N)</div>
255 </div> 157 </div>
256 <div class="log-entry-content"> 158 <div class="log-entry-content">
257 LINE #0 159 <div class="log-entry-line">LINE #0</div>
258 ... 160 ...
259 LINE #N 161 <div class="log-entry-line">LINE #N</div>
260 </div> 162 </div>
261 </div> 163 </div>
262 ... 164 ...
263 165
166 -->
167 </div>
264 168
265 Note that we can't use templating to show/hide the log dividers, 169 <!-- Current red bottom line. -->
266 since our positional log insertion requires them to be present and 170 <div id="bottom"></div>
267 move along with insertions as points of reference.
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>
299 </div>
300 </div> 171 </div>
301 172
302 </div> 173 </div>
303 174
304 <template is="dom-if" if="{{statusBar}}"> 175 <template is="dom-if" if="{{statusBar}}">
305 <div id="status-bar">{{statusBar.value}}</div> 176 <div id="status-bar">{{statusBar.value}}</div>
306 </template> 177 </template>
307 </template> 178 </template>
308 179
309 </dom-module> 180 </dom-module>
(...skipping 18 matching lines...) Expand all
328 * project. For example, for stream "foo/bar/+/baz" in project "chromium", 199 * project. For example, for stream "foo/bar/+/baz" in project "chromium",
329 * the stream path would be: "chromium/foo/bar/+/baz". 200 * the stream path would be: "chromium/foo/bar/+/baz".
330 */ 201 */
331 streams: { 202 streams: {
332 type: Array, 203 type: Array,
333 value: [], 204 value: [],
334 notify: true, 205 notify: true,
335 observer: "_streamsChanged", 206 observer: "_streamsChanged",
336 }, 207 },
337 208
338 toolbarAnchor: {
339 type: Object,
340 value: null,
341 },
342
343 mobile: {
344 type: Boolean,
345 value: false,
346 },
347
348 /** 209 /**
349 * The number of logs to load before forcing a page refresh. 210 * The number of logs to load before forcing a page refresh.
350 * 211 *
351 * The smaller the value, the smoother the page will behave while logs are 212 * The smaller the value, the smoother the page will behave while logs are
352 * loading. However, the logs will also load slower because of forced 213 * loading. However, the logs will also load slower because of forced
353 * renders in between elements. 214 * renders in between elements.
354 */ 215 */
355 burst: { 216 burst: {
356 type: Number, 217 type: Number,
357 value: 1000, 218 value: 1000,
358 notify: true, 219 notify: true,
359 }, 220 },
360 221
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
373 /** If true, show log metadata column. */ 222 /** If true, show log metadata column. */
374 showMetadata: { 223 showMetadata: {
375 type: Boolean, 224 type: Boolean,
376 value: false, 225 value: false,
377 observer: "_showMetadataChanged", 226 observer: "_showMetadataChanged",
378 }, 227 },
379 228
380 /** If true, wrap log lines to the screen. */ 229 /** If true, wrap log lines to the screen. */
381 wrapLines: { 230 wrapLines: {
382 type: Boolean, 231 type: Boolean,
383 value: false, 232 value: false,
384 observer: "_wrapLinesChanged", 233 observer: "_wrapLinesChanged",
385 }, 234 },
386 235
387 /** 236 /**
388 * If true, automatically scroll the page to the bottom of the logs 237 * If true, automatically scroll the page to the bottom of the logs
389 * while they are streaming. 238 * while they are streaming.
390 */ 239 */
391 follow: { 240 follow: {
392 type: Boolean, 241 type: Boolean,
393 value: false, 242 value: false,
394 observer: "_followChanged", 243 observer: "_followChanged",
395 }, 244 },
396 245
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
443 /** 246 /**
444 * The current stream status. This is an Array of objects: 247 * The current stream status. This is an Array of objects:
445 * obj.name is the name of the stream. 248 * obj.name is the name of the stream.
446 * obj.desc is the status description of the stream. 249 * obj.desc is the status description of the stream.
447 */ 250 */
448 streamStatus: { 251 streamStatus: {
449 type: String, 252 type: String,
450 value: null, 253 value: null,
451 notify: true, 254 notify: true,
452 readOnly: true, 255 readOnly: true,
453 }, 256 },
454 257
455 /** 258 /**
456 * The text content of the status element at the bottom of the page. 259 * The text content of the status element at the bottom of the page.
457 */ 260 */
458 statusBar: { 261 statusBar: {
459 type: String, 262 type: String,
460 value: null, 263 value: null,
461 readOnly: true, 264 readOnly: true,
462 }, 265 },
463 }, 266 },
464 267
465 created: function() { 268 ready: function() {
466 this._scrollTimeoutId = null; 269 this._scheduledWrite = null;
467 270 this._buffer = null;
468 // Create "onScrollHandler", which just invokes "_onScroll" while bound 271 this._currentLogBuffer = null;
469 // to "this". We create it here so we can unregister it later, since 272 this._authCallback = null;
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);
492 }, 273 },
493 274
494 detached: function() { 275 detached: function() {
495 // Unregsiter event handlers. 276 this.stop();
496 window.removeEventListener('scroll', this._onScrollHandler);
497
498 // Reset state.
499 this.reset();
500 }, 277 },
501 278
502 stop: function() { 279 stop: function() {
503 this.reset(); 280 this._cancelFetch(true);
504 }, 281 },
505 282
506 /** Clears state and begins fetching log data. */ 283 /** Clears state and begins fetching log data. */
507 reset: function() { 284 reset: function() {
508 if ( this._model ) { 285 this._resetLogState();
509 this._model.reset(); 286
510 } 287 this._resolveStreams().then(function(streams) {
511 this._resetScroll(); 288 this._resetToStreams(streams);
512 this._model = null; 289 }.bind(this)).catch(function(error) {
513 this._renderedLogs = false; 290 this._loadStatusBar("Failed to resolve streams:" + error);
291 throw error;
292 }.bind(this));
293 },
294
295 /** Clears all current logs. */
296 _resetLogState: function() {
297 this._cancelFetch(true);
298
299 // Remove all current log elements. */
300 while (this.$.logs.hasChildNodes()) {
301 this.$.logs.removeChild(this.$.logs.lastChild);
302 }
303
304 // Clear our buffer and streamer state.
305 this._buffer = null;
306 this._currentLogBuffer = null;
307 if (this._streamer) {
308 this._streamer.shutdown();
309 }
310 this._streamer = null;
311 },
312
313 _resolveStreams: function() {
314 // Separate our configured streams into full stream paths and queries.
315 var parts = {
316 queries: [],
317 streams: [],
318 };
319 var query = new LogDogQuery(this.project);
320 this.streams.map(LogDogStream.splitProject).forEach(function(v) {
321 if (LogDogQuery.isQuery(v.path)) {
322 parts.queries.push(v);
323 } else {
324 parts.streams.push(v);
325 }
326 });
327
328 // Resolve any outstanding queries into full stream paths.
329 //
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 }
514 }, 456 },
515 457
516 /** 458 /**
517 * Called each time a scroll event is fired. Since this can be really 459 * This is an iterative function that grabs the next set of logs and renders
518 * frequent, this will kick off a "scroll handler" in the background at an 460 * them. Afterwards, it will continue rescheduling itself until there are
519 * interval. Multiple scroll events within that interval will only result 461 * no more logs to render.
520 * in one scroll handler invocation.
521 */ 462 */
522 _onScroll: function(e) { 463 _writeNextLogs: function() {
523 if ( this._scrollTimeoutId ) { 464 this._cancelScheduledWrite();
524 return; 465
525 } 466 this._streamer.load().then(function(entries) {
526 467 // If there are no entries, then we're done.
527 window.setTimeout(function() { 468 if (! entries) {
528 this._handleScrollEvent(e); 469 // Cancel all fetching state. If our streamer is finished, also clear
529 }.bind(this), 100); 470 // messages and status.
530 }, 471 if (this._streamer.finished) {
531 472 if (this._streamer.someStreamsFailed) {
532 /** Actual scroll event handler. */ 473 this._cancelFetch(false);
533 _handleScrollEvent: function(e) { 474 this._loadStatusBar("Some streams failed to load.");
534 this._resetScroll(); 475 } else {
535 476 this._cancelFetch(true);
536 // Update our button bar position to be relative to the parent's height. 477 }
537 this._adjustToTop(this.$.buttons); 478 } else {
538 this._adjustToTop(this.$.streamStatus); 479 // No more logs, but also we are not finished. Retry after auth.
539 }, 480 this._authCallback = this._scheduleWriteNextLogs.bind(this);
540 481 }
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; 482 return;
627 } 483 }
628 484
629 // If we're rendering metadata, render an element per log entry. 485 var logEntryChunk = document.createElement("div");
630 if( this.metadata ) { 486 entries.forEach(function(le) {
631 var entryRow = document.createElement("div"); 487 this._appendLogEntry(logEntryChunk, le);
632 entryRow.className = "log-entry"; 488 }.bind(this));
633 489
634 // Metadata column. 490 // To have styles apply correctly, we need to add it twice, see
635 var metadataBlock = document.createElement("div"); 491 // https://github.com/Polymer/polymer/issues/3100.
636 metadataBlock.className = "log-entry-meta"; 492 Polymer.dom(this.root).appendChild(logEntryChunk);
637 493 this.$.logs.appendChild(logEntryChunk);
638 this._appendMetaLine(metadataBlock, "Timestamp:", le.timestamp); 494
639 this._appendMetaLine(metadataBlock, "Stream:", le.desc.name); 495 // Yield so that our browser can refresh. We can't directly use
640 this._appendMetaLine(metadataBlock, "Index:", le.streamIndex); 496 // this.async since a timeout of "0" causes immediate execution instead
641 497 // of yielding.
642 // Log column. 498 setTimeout(function() {
643 var logDataBlock = document.createElement("div"); 499 this._scheduleWriteNextLogs();
644 logDataBlock.className = "log-entry-content"; 500 }.bind(this), 0);
645 501 }.bind(this));
646 le.text.lines.forEach(function(line) { 502 },
647 lines.push(line.value); 503
648 }); 504 _appendLogEntry: function(root, le) {
649 505 var text = le.text;
650 logDataBlock.textContent = lines.join("\n"); 506 if (!(text && text.lines)) {
651 lines.length = 0; 507 return 0;
652 508 }
653 entryRow.appendChild(metadataBlock); 509
654 entryRow.appendChild(logDataBlock); 510 // Create elements manually to avoid Polymer overhead for large logs.
655 511 var entryRow = document.createElement("div");
656 logEntryChunk.appendChild(entryRow); 512 entryRow.className = "log-entry";
657 lastLogEntry = entryRow; 513
658 } else { 514 // Metadata column.
659 // Add this to the lines. We'll assign this directly to logEntryChunk 515 var metadataBlock = document.createElement("div");
660 // after the loop. 516 metadataBlock.className = "log-entry-meta";
661 le.text.lines.forEach(function(line) { 517 entryRow.appendChild(metadataBlock);
662 lines.push(line.value); 518
663 }); 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);
664 } 543 }
665 }.bind(this)); 544 }
666 545 entryRow.appendChild(logDataBlock);
667 if ( ! this.metadata ) { 546 root.appendChild(entryRow);
668 // Only one HTML element: the chunk. 547
669 logEntryChunk.textContent = lines.join("\n"); 548 return le.text.lines.length;
670 lastLogEntry = logEntryChunk; 549 },
671 } 550
672 551 _updateStreamStatus: function(bs, idx) {
673 // To have styles apply correctly, we need to add it twice, see 552 var origStatus = this.streamStatus[idx];
674 // https://github.com/Polymer/polymer/issues/3100. 553 this.splice("streamStatus", idx, 1, {
675 Polymer.dom(this.root).appendChild(logEntryChunk); 554 name: origStatus.name,
676 555 desc: bs.description(),
677 // Add the log entry to the appropriate place. 556 });
678 var anchor, scrollToTop = false; 557 },
679 var forceScroll = false; 558
680 switch ( insertion ) { 559 /** Scrolls to the bottom if "follow" is enabled. */
681 case this._v.Location.HEAD: 560 _maybeScrollToBottom: function() {
682 // InsertionPoint.HEAD: PREPEND to "logSplit". 561 if (this.follow) {
683 this.$.logs.insertBefore(logEntryChunk, this.$.logSplit); 562 this.$.bottom.scrollIntoView({
684 563 "behavior": "smooth",
685 // If we're not split, scroll to the log bottom. Otherwise, scroll to 564 "block": "end",
686 // the split. 565 });
687 anchor = lastLogEntry; 566 }
688 break; 567 },
689 568
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() {
723 // Remove all current log elements. */
724 for ( var cur = this.$.logs.firstChild; cur; ) {
725 var del = cur;
726 cur = cur.nextElementSibling;
727 if ( del.classList && del.classList.contains('log-entry-chunk') ) {
728 this.$.logs.removeChild(del);
729 }
730 }
731 },
732
733 _locationIsVisible: function(l) {
734 var anchor;
735 switch( l ) {
736 case this._v.Location.HEAD:
737 case this._v.Location.TAIL:
738 anchor = this.$.logSplit;
739
740 case this._v.Location.BOTTOM:
741 anchor = this.$.logBottom;
742
743 default:
744 return false;
745 }
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 });
813 } else {
814 // Bug? "block: start" doesn't seem to work the same as false.
815 element.scrollIntoView(false);
816 }
817 }
818 },
819
820 /** 569 /**
821 * Callback when "showMetadata" has changed. This adds/removes the 570 * Callback when "showMetadata" has changed. This adds/removes the
822 * "showMeta" CSS class from the metadata column. 571 * "showMeta" CSS class from the metadata column.
823 */ 572 */
824 _showMetadataChanged: function(v) { 573 _showMetadataChanged: function(v) {
825 this.toggleClass("showMeta", v, this.$.logs); 574 this.toggleClass("showMeta", v, this.$.logs);
826 }, 575 },
827
828 /** 576 /**
829 * Callback when "wrapLines" has changed. This adds/removes the 577 * Callback when "wrapLines" has changed. This adds/removes the
830 * "wrapLines" CSS class to the log data. 578 * "wrapLines" CSS class to the log data.
831 */ 579 */
832 _wrapLinesChanged: function(v) { 580 _wrapLinesChanged: function(v) {
833 this.toggleClass("wrapLines", v, this.$.logs); 581 this.toggleClass("wrapLines", v, this.$.logs);
834 }, 582 },
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) ?
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
866 /** Callback when "follow" has changed. */ 583 /** Callback when "follow" has changed. */
867 _followChanged: function(v) { 584 _followChanged: function(v) {
868 if ( ! this._model ) { 585 this._maybeScrollToBottom();
869 return; 586 },
870 }
871 587
872 if ( v ) { 588 /** Callback for when the mouse wheel has scrolled. Disables follow. */
873 // If follow is toggled on, automatically begin playing. 589 _handleMouseWheel: function() {
874 this.playing = true; 590 this.follow = false;
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);
906 }, 591 },
907 592
908 /** 593 /**
909 * Loads text content into the status bar. 594 * Loads text content into the status bar.
910 * 595 *
911 * If null is passed, the status bar will be cleared. If text is passed, the 596 * If null is passed, the status bar will be cleared. If text is passed, the
912 * status bar will become visible with the supplied content. 597 * status bar will become visible with the supplied content.
913 */ 598 */
914 _loadStatusBar: function(v) { 599 _loadStatusBar: function(v) {
915 var st = null; 600 var st = null;
916 if (v) { 601 if (v) {
917 st = { 602 st = {
918 value: v, 603 value: v,
919 }; 604 };
920 } 605 }
921 this._setStatusBar(st); 606 this._setStatusBar(st);
922 }, 607 },
923 608
924 _onSignin: function() { 609 _onSignin: function() {
925 if ( this._model ) { 610 var fn = this._authCallback;
926 this._model.notifyAuthenticationChanged(); 611 if (fn) {
612 this._authCallback = null;
613 fn();
927 } 614 }
928 }, 615 },
929
930 _elementInViewport: function(el) {
931 var top = el.offsetTop;
932 var left = el.offsetLeft;
933 var width = el.offsetWidth;
934 var height = el.offsetHeight;
935
936 while(el.offsetParent) {
937 el = el.offsetParent;
938 top += el.offsetTop;
939 left += el.offsetLeft;
940 }
941
942 return (
943 top < (window.pageYOffset + window.innerHeight) &&
944 left < (window.pageXOffset + window.innerWidth) &&
945 (top + height) > window.pageYOffset &&
946 (left + width) > window.pageXOffset
947 );
948 },
949
950 }); 616 });
617
618 /**
619 * Continuously loads log streams from a _LogStreamBuffer and exposes them via
620 * callback.
621 */
622 function _LogStreamer(buffer, burst, statusCallback) {
623 this._buffer = buffer;
624 this._burst = (burst || 0);
625 this._missingDelay = 5000;
626 this._statusCallback = statusCallback;
627
628 this.finished = false;
629 this.someStreamsFailed = false;
630
631 this._currentLogBuffer = null;
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 }
669
670 // Have we exhausted this buffer?
671 if (! current.peek()) {
672 this._currentLogBuffer = null;
673 }
674
675 // Return the bundle of entries.
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 };
951 </script> 1076 </script>
OLDNEW
« no previous file with comments | « web/inc/logdog-stream-view/logdog-stream-query.html ('k') | web/inc/logdog-stream-view/query.ts » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698