Merge lp:~benji/juju-gui/websocket-logging into lp:juju-gui/experimental
- websocket-logging
- Merge into trunk
Proposed by
Benji York
Status: | Merged |
---|---|
Merged at revision: | 701 |
Proposed branch: | lp:~benji/juju-gui/websocket-logging |
Merge into: | lp:juju-gui/experimental |
Diff against target: |
499 lines (+309/-11) 9 files modified
Makefile (+2/-0) app/app.js (+13/-1) app/assets/javascripts/reconnecting-websocket.js (+10/-8) app/modules-debug.js (+4/-0) app/websocket-logging.js (+123/-0) lib/websocketreplay.py (+8/-2) test/index.html (+1/-0) test/test_websocket_logging.js (+109/-0) test/test_websocketreplay.py (+39/-0) |
To merge this branch: | bzr merge lp:~benji/juju-gui/websocket-logging |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+167034@code.launchpad.net |
Commit message
Description of the change
Adds a websocket recording and download feature.
When the websocket_capture feature flag is enabled (e.g.,
/:flags:
user presses Control-Shift-d, the log will be downloaded to their local
machine.
To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote : | # |
Revision history for this message
Nicola Larosa (teknico) wrote : | # |
LGTM, nice, thanks.
- 706. By Benji York
-
changes stemming from review
- 707. By Benji York
-
add some yuidoc directives
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'Makefile' |
2 | --- Makefile 2013-05-29 19:06:28 +0000 |
3 | +++ Makefile 2013-06-03 17:41:26 +0000 |
4 | @@ -317,6 +317,7 @@ |
5 | |
6 | LINK_DEBUG_FILES=$(call shared-link-files-list,debug) \ |
7 | build-debug/juju-ui/app.js \ |
8 | + build-debug/juju-ui/websocket-logging.js \ |
9 | build-debug/juju-ui/models \ |
10 | build-debug/juju-ui/store \ |
11 | build-debug/juju-ui/subapps \ |
12 | @@ -363,6 +364,7 @@ |
13 | $(LINK_DEBUG_FILES): |
14 | $(call link-files,debug) |
15 | ln -sf "$(PWD)/app/app.js" build-debug/juju-ui/ |
16 | + ln -sf "$(PWD)/app/websocket-logging.js" build-debug/juju-ui/ |
17 | ln -sf "$(PWD)/app/models" build-debug/juju-ui/ |
18 | ln -sf "$(PWD)/app/store" build-debug/juju-ui/ |
19 | ln -sf "$(PWD)/app/subapps" build-debug/juju-ui/ |
20 | |
21 | === modified file 'app/app.js' |
22 | --- app/app.js 2013-05-31 18:07:02 +0000 |
23 | +++ app/app.js 2013-06-03 17:41:26 +0000 |
24 | @@ -226,7 +226,6 @@ |
25 | |
26 | 'S-d': { |
27 | callback: function(evt) { |
28 | - /* global saveAs: false */ |
29 | this.env.exportEnvironment(function(r) { |
30 | var exportData = JSON.stringify(r.result, undefined, 2); |
31 | var exportBlob = new Blob([exportData], |
32 | @@ -235,8 +234,16 @@ |
33 | }); |
34 | }, |
35 | help: 'Export the environment' |
36 | + }, |
37 | + |
38 | + 'C-S-d': { |
39 | + callback: function(evt) { |
40 | + Y.fire('saveWebsocketLog'); |
41 | + }, |
42 | + help: 'Save the websocket log to a file' |
43 | } |
44 | |
45 | + |
46 | }, |
47 | |
48 | /** |
49 | @@ -354,6 +361,10 @@ |
50 | }); |
51 | } |
52 | |
53 | + if (window.flags.websocket_capture) { |
54 | + this.websocketLogging = new Y.juju.WebsocketLogging(); |
55 | + } |
56 | + |
57 | this.renderEnvironment = true; |
58 | // If this property has a value other than '/' then |
59 | // navigate to it after logging in. |
60 | @@ -1301,6 +1312,7 @@ |
61 | 'juju-views', |
62 | 'juju-view-login', |
63 | 'juju-landscape', |
64 | + 'juju-websocket-logging', |
65 | 'io', |
66 | 'json-parse', |
67 | 'app-base', |
68 | |
69 | === modified file 'app/assets/javascripts/reconnecting-websocket.js' |
70 | --- app/assets/javascripts/reconnecting-websocket.js 2012-07-24 18:18:43 +0000 |
71 | +++ app/assets/javascripts/reconnecting-websocket.js 2013-06-03 17:41:26 +0000 |
72 | @@ -38,7 +38,7 @@ |
73 | * onopen // sometime later... |
74 | * onmessage |
75 | * onmessage |
76 | - * etc... |
77 | + * etc... |
78 | * |
79 | * It is API compatible with the standard WebSocket API. |
80 | * |
81 | @@ -46,7 +46,7 @@ |
82 | */ |
83 | |
84 | YUI.add("reconnecting-websocket", function(Y) { |
85 | - |
86 | + |
87 | function ReconnectingWebSocket(url, protocols) { |
88 | |
89 | // These can be altered by calling code. |
90 | @@ -58,7 +58,7 @@ |
91 | var ws; |
92 | var forcedClose = false; |
93 | var timedOut = false; |
94 | - |
95 | + |
96 | this.url = url; |
97 | this.protocols = protocols; |
98 | this.readyState = WebSocket.CONNECTING; |
99 | @@ -81,7 +81,7 @@ |
100 | if (self.debug || ReconnectingWebSocket.debugAll) { |
101 | console.debug('ReconnectingWebSocket', 'attempt-connect', url); |
102 | } |
103 | - |
104 | + |
105 | var localWs = ws; |
106 | var timeout = setTimeout(function() { |
107 | if (self.debug || ReconnectingWebSocket.debugAll) { |
108 | @@ -91,7 +91,7 @@ |
109 | localWs.close(); |
110 | timedOut = false; |
111 | }, self.timeoutInterval); |
112 | - |
113 | + |
114 | ws.onopen = function(event) { |
115 | clearTimeout(timeout); |
116 | if (self.debug || ReconnectingWebSocket.debugAll) { |
117 | @@ -101,7 +101,7 @@ |
118 | reconnectAttempt = false; |
119 | self.onopen(event); |
120 | }; |
121 | - |
122 | + |
123 | ws.onclose = function(event) { |
124 | clearTimeout(timeout); |
125 | ws = null; |
126 | @@ -125,7 +125,8 @@ |
127 | if (self.debug || ReconnectingWebSocket.debugAll) { |
128 | console.debug('ReconnectingWebSocket', 'onmessage', url, event.data); |
129 | } |
130 | - self.onmessage(event); |
131 | + Y.fire('websocketReceive', event.data); |
132 | + self.onmessage(event); |
133 | }; |
134 | ws.onerror = function(event) { |
135 | if (self.debug || ReconnectingWebSocket.debugAll) { |
136 | @@ -141,6 +142,7 @@ |
137 | if (self.debug || ReconnectingWebSocket.debugAll) { |
138 | console.debug('ReconnectingWebSocket', 'send', url, data); |
139 | } |
140 | + Y.fire('websocketSend', data); |
141 | return ws.send(data); |
142 | } else { |
143 | throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; |
144 | @@ -174,4 +176,4 @@ |
145 | |
146 | }, "0.1.0", { |
147 | requires: ["base"] |
148 | -}); |
149 | \ No newline at end of file |
150 | +}); |
151 | |
152 | === modified file 'app/modules-debug.js' |
153 | --- app/modules-debug.js 2013-05-24 14:10:58 +0000 |
154 | +++ app/modules-debug.js 2013-06-03 17:41:26 +0000 |
155 | @@ -323,6 +323,10 @@ |
156 | fullpath: '/juju-ui/store/charm.js' |
157 | }, |
158 | |
159 | + 'juju-websocket-logging': { |
160 | + fullpath: '/juju-ui/websocket-logging.js' |
161 | + }, |
162 | + |
163 | 'juju-controllers': { |
164 | use: [ |
165 | 'juju-env', |
166 | |
167 | === added directory 'app/subapps/websocketLogging' |
168 | === added file 'app/websocket-logging.js' |
169 | --- app/websocket-logging.js 1970-01-01 00:00:00 +0000 |
170 | +++ app/websocket-logging.js 2013-06-03 17:41:26 +0000 |
171 | @@ -0,0 +1,123 @@ |
172 | +/* |
173 | +This file is part of the Juju GUI, which lets users view and manage Juju |
174 | +environments within a graphical interface (https://launchpad.net/juju-gui). |
175 | +Copyright (C) 2013 Canonical Ltd. |
176 | + |
177 | +This program is free software: you can redistribute it and/or modify it under |
178 | +the terms of the GNU Affero General Public License version 3, as published by |
179 | +the Free Software Foundation. |
180 | + |
181 | +This program is distributed in the hope that it will be useful, but WITHOUT |
182 | +ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
183 | +SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero |
184 | +General Public License for more details. |
185 | + |
186 | +You should have received a copy of the GNU Affero General Public License along |
187 | +with this program. If not, see <http://www.gnu.org/licenses/>. |
188 | +*/ |
189 | + |
190 | +'use strict'; |
191 | + |
192 | +YUI.add('juju-websocket-logging', function(Y) { |
193 | + var juju = Y.namespace('juju'); |
194 | + |
195 | + /** |
196 | + A logger of websocket traffic for debugging purposes. |
197 | + |
198 | + @class Browser |
199 | + @extends {juju.SubApp} |
200 | + */ |
201 | + juju.WebsocketLogging = Y.Base.create('WebsocketLogging', Y.Base, [], { |
202 | + |
203 | + // The stored messages, their direction, and time. |
204 | + log: undefined, |
205 | + |
206 | + /** |
207 | + Websocket logger initialization. |
208 | + |
209 | + @method initializer |
210 | + @param {Object} cfg general init config object. |
211 | + @return {undefined} Side-effects only. |
212 | + */ |
213 | + initializer: function(cfg) { |
214 | + this.log = []; |
215 | + this.setUpEventListeners(); |
216 | + }, |
217 | + |
218 | + /** |
219 | + Bind the event listeners. |
220 | + |
221 | + @method setUpEventListeners |
222 | + @return {undefined} Side-effects only. |
223 | + */ |
224 | + setUpEventListeners: function() { |
225 | + var self = this; |
226 | + Y.on('websocketSend', function(data) { |
227 | + self.logMessage('to server', data); |
228 | + }); |
229 | + Y.on('websocketReceive', function(data) { |
230 | + self.logMessage('to client', data); |
231 | + }); |
232 | + Y.on('saveWebsocketLog', function() {self.saveLog(self.log);}); |
233 | + }, |
234 | + |
235 | + /** |
236 | + Generate a date/time string representing the current time. |
237 | + |
238 | + @method timestamp |
239 | + @static |
240 | + @return {String} The current time in ISO 8601 format. |
241 | + */ |
242 | + timestamp: function() { |
243 | + return new Date().toISOString(); |
244 | + }, |
245 | + |
246 | + /** |
247 | + Record a websocket message, its direction, and the current time. |
248 | + |
249 | + @method logMessage |
250 | + @param {String} direction The direction the message was sent. Either 'to |
251 | + server' or 'to client'. |
252 | + @param {String} message The message that was sent over the websocket. |
253 | + @return {undefined} Side-effects only. |
254 | + */ |
255 | + logMessage: function(direction, message) { |
256 | + this.log.push([this.timestamp(), direction, message].join('\n')); |
257 | + }, |
258 | + |
259 | + /** |
260 | + Save a log of websocket traffic |
261 | + |
262 | + @method saveLog |
263 | + @static |
264 | + @param {Array} log An array of log lines. |
265 | + @return {undefined} Side-effects only. |
266 | + */ |
267 | + saveLog: function(log) { |
268 | + var data = log.join('\n'); |
269 | + var mimeType = 'text/plain;charset=utf-8'; |
270 | + var blob; |
271 | + try { |
272 | + blob = new Blob([data], |
273 | + {type: mimeType}); |
274 | + } catch (e) { |
275 | + // BlobBuilder has been deprecated in favour of Blob, but some browsers |
276 | + // (including phantomjs) have not added the replacement Blob |
277 | + // constructor API, so we have this fall-back code. |
278 | + var BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder; |
279 | + var builder = new BlobBuilder(); |
280 | + builder.append(data); |
281 | + blob = builder.getBlob(mimeType); |
282 | + } |
283 | + // The saveAs() function is a global implemented in |
284 | + // app/assets/javascripts/FileSaver.js. |
285 | + saveAs(blob, 'websocket-log.txt'); |
286 | + } |
287 | + |
288 | + }); |
289 | + |
290 | +}, '0.1.0', { |
291 | + requires: [ |
292 | + 'sub-app' |
293 | + ] |
294 | +}); |
295 | |
296 | === modified file 'lib/websocketreplay.py' |
297 | --- lib/websocketreplay.py 2013-05-29 16:45:11 +0000 |
298 | +++ lib/websocketreplay.py 2013-06-03 17:41:26 +0000 |
299 | @@ -85,16 +85,22 @@ |
300 | direction the message was sent |
301 | """ |
302 | seen_request_ids = set() |
303 | + direction = None |
304 | for line in source: |
305 | line = line.strip() |
306 | # The data format is a bit icky, but since we only send json-encoded |
307 | # messages, pulling them out of the log is straight-forward. |
308 | - if line.startswith('{'): |
309 | + if line.startswith('to '): |
310 | + direction = line |
311 | + elif line.startswith('{'): |
312 | message = json.loads(line) |
313 | - direction = infer_direction(message, seen_request_ids) |
314 | + # If the log does not contain direction info we have to guess. |
315 | + if direction is None: |
316 | + direction = infer_direction(message, seen_request_ids) |
317 | if 'request_id' in message: |
318 | seen_request_ids.add(message['request_id']) |
319 | yield Frame(message, direction) |
320 | + message = direction = None |
321 | |
322 | |
323 | def print_with_color(color, *args, **kws): |
324 | |
325 | === modified file 'test/index.html' |
326 | --- test/index.html 2013-05-24 14:23:27 +0000 |
327 | +++ test/index.html 2013-06-03 17:41:26 +0000 |
328 | @@ -106,6 +106,7 @@ |
329 | <script src="test_unit_view.js"></script> |
330 | <script src="test_utils.js"></script> |
331 | <script src="test_viewport_module.js"></script> |
332 | + <script src="test_websocket_logging.js"></script> |
333 | <script> |
334 | YUI_config = { |
335 | async: false, |
336 | |
337 | === added file 'test/test_websocket_logging.js' |
338 | --- test/test_websocket_logging.js 1970-01-01 00:00:00 +0000 |
339 | +++ test/test_websocket_logging.js 2013-06-03 17:41:26 +0000 |
340 | @@ -0,0 +1,109 @@ |
341 | +/* |
342 | +This file is part of the Juju GUI, which lets users view and manage Juju |
343 | +environments within a graphical interface (https://launchpad.net/juju-gui). |
344 | +Copyright (C) 2013 Canonical Ltd. |
345 | + |
346 | +This program is free software: you can redistribute it and/or modify it under |
347 | +the terms of the GNU Affero General Public License version 3, as published by |
348 | +the Free Software Foundation. |
349 | + |
350 | +This program is distributed in the hope that it will be useful, but WITHOUT |
351 | +ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
352 | +SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero |
353 | +General Public License for more details. |
354 | + |
355 | +You should have received a copy of the GNU Affero General Public License along |
356 | +with this program. If not, see <http://www.gnu.org/licenses/>. |
357 | +*/ |
358 | + |
359 | +'use strict'; |
360 | + |
361 | +var saveAs; |
362 | + |
363 | +(function() { |
364 | + describe('websocket recording', function() { |
365 | + var Y, WebsocketLogging, websocketLogging; |
366 | + |
367 | + before(function(done) { |
368 | + Y = YUI(GlobalConfig).use( |
369 | + 'juju-websocket-logging', function(Y) { |
370 | + WebsocketLogging = Y.namespace('juju').WebsocketLogging; |
371 | + done(); |
372 | + }); |
373 | + }); |
374 | + |
375 | + beforeEach(function() { |
376 | + websocketLogging = new WebsocketLogging(); |
377 | + // Provide a noop saveAs global which is normally provided by the app. |
378 | + saveAs = function() {}; |
379 | + }); |
380 | + |
381 | + afterEach(function() { |
382 | + // We need to clean up all the event handlers. |
383 | + Y.detach('websocketSend'); |
384 | + Y.detach('websocketReceive'); |
385 | + Y.detach('saveWebsocketLog'); |
386 | + }); |
387 | + |
388 | + it('can save a log', function() { |
389 | + // When asked to save a log, the global saveAs function is called with a |
390 | + // blob containing the serialized log entries. |
391 | + saveAs = function(blob, fileName) { |
392 | + assert.isTrue(blob !== undefined); |
393 | + assert.equal(blob.type, 'text/plain;charset=utf-8'); |
394 | + assert.equal(blob.size, 6); |
395 | + assert.equal(fileName, 'websocket-log.txt'); |
396 | + }; |
397 | + websocketLogging.saveLog(['my', 'log']); |
398 | + }); |
399 | + |
400 | + it('responds to the saveWebsocketLog event', function(done) { |
401 | + // When the saveWebsocketLog event is fired, the saveLog method is called |
402 | + // with the current log of websocket events. |
403 | + var logEntry = 'this is a log entry'; |
404 | + websocketLogging.saveLog = function(log) { |
405 | + assert.deepEqual(log, [logEntry]); |
406 | + done(); |
407 | + }; |
408 | + websocketLogging.log.push(logEntry); |
409 | + // Since we changed one of the event handlers, we have to re-bind the |
410 | + // event listeners. |
411 | + Y.detach('saveWebsocketLog'); |
412 | + websocketLogging.setUpEventListeners(); |
413 | + Y.fire('saveWebsocketLog'); |
414 | + }); |
415 | + |
416 | + it('logs messages', function() { |
417 | + // We want the timestamp to be fixed in time so we know what to expect |
418 | + // for the assertion below. |
419 | + websocketLogging.timestamp = function() { |
420 | + return '2013-05-31T16:15:05.923Z'; |
421 | + }; |
422 | + websocketLogging.logMessage('to server', 'ping'); |
423 | + websocketLogging.logMessage('to client', 'pong'); |
424 | + assert.deepEqual(websocketLogging.log, |
425 | + ['2013-05-31T16:15:05.923Z\nto server\nping', |
426 | + '2013-05-31T16:15:05.923Z\nto client\npong']); |
427 | + }); |
428 | + |
429 | + it('logs server messages on the websocketReceive event', function(done) { |
430 | + websocketLogging.logMessage = function(direction, message) { |
431 | + assert.equal(direction, 'to client'); |
432 | + assert.equal(message, 'message to the client'); |
433 | + done(); |
434 | + }; |
435 | + Y.fire('websocketReceive', 'message to the client'); |
436 | + }); |
437 | + |
438 | + it('logs client messages on the websocketSend event', function(done) { |
439 | + websocketLogging.logMessage = function(direction, message) { |
440 | + assert.equal(direction, 'to server'); |
441 | + assert.equal(message, 'message to the server'); |
442 | + done(); |
443 | + }; |
444 | + Y.fire('websocketSend', 'message to the server'); |
445 | + }); |
446 | + |
447 | + }); |
448 | + |
449 | +})(); |
450 | |
451 | === modified file 'test/test_websocketreplay.py' |
452 | --- test/test_websocketreplay.py 2013-05-29 16:45:11 +0000 |
453 | +++ test/test_websocketreplay.py 2013-06-03 17:41:26 +0000 |
454 | @@ -86,6 +86,45 @@ |
455 | direction='to client')], |
456 | list(read_frames(log))) |
457 | |
458 | + def test_included_direction_is_used(self): |
459 | + # If the log contains direction info (whether or not the message was |
460 | + # sent from the client and to the server or vice versa) that info will |
461 | + # be used. |
462 | + log = """\ |
463 | + junk |
464 | + to mars |
465 | + {"foo": 1} |
466 | + junk |
467 | + to venus |
468 | + {"foo": 2} |
469 | + junk""".split('\n') |
470 | + self.assertEqual( |
471 | + [Frame( |
472 | + message={u'foo': 1}, |
473 | + direction='to mars'), |
474 | + Frame( |
475 | + message={u'foo': 2}, |
476 | + direction='to venus')], |
477 | + list(read_frames(log))) |
478 | + |
479 | + def test_missing_direction_is_infered(self): |
480 | + # If the log does not contain direction info, the directionality of |
481 | + # each message will deduced from the message's contents. |
482 | + log = """\ |
483 | + junk |
484 | + {"foo": 1, "request_id": 1} |
485 | + junk |
486 | + {"foo": 2} |
487 | + junk""".split('\n') |
488 | + self.assertEqual( |
489 | + [Frame( |
490 | + message={u'foo': 1, u'request_id': 1}, |
491 | + direction='to server'), |
492 | + Frame( |
493 | + message={u'foo': 2}, |
494 | + direction='to client')], |
495 | + list(read_frames(log))) |
496 | + |
497 | def test_result_is_iterator(self): |
498 | # The object returned from read_frames() is an iterator. This is so |
499 | # we can consume it incrementally without having to keep up with how |
LGTM with minors.
https:/ /codereview. appspot. com/9953045/ diff/1/ Makefile
File Makefile (right):
https:/ /codereview. appspot. com/9953045/ diff/1/ Makefile# newcode320 juju-ui/ websocketLoggin g.js \ we-liked- hyphens.
Makefile:320: build-debug/
camelCaseFileNames? I-thought-
https:/ /codereview. appspot. com/9953045/ diff/1/ app/app. js
File app/app.js (right):
https:/ /codereview. appspot. com/9953045/ diff/1/ app/app. js#newcode240
app/app.js:240: 'C-S-d': {
I assume that combination is a vimism.
https:/ /codereview. appspot. com/9953045/ diff/1/ app/assets/ javascripts/ reconnecting- websocket. js javascripts/ reconnecting- websocket. js (right):
File app/assets/
https:/ /codereview. appspot. com/9953045/ diff/1/ app/assets/ javascripts/ reconnecting- websocket. js#newcode49 javascripts/ reconnecting- websocket. js:49:
app/assets/
bless you.
https:/ /codereview. appspot. com/9953045/ diff/1/ app/websocketLo gging.js gging.js (right):
File app/websocketLo
https:/ /codereview. appspot. com/9953045/ diff/1/ app/websocketLo gging.js# newcode4 gging.js: 4: Copyright (C) 2012-2013 Canonical Ltd.
app/websocketLo
i think new files should just list the year they were created. yeah, i
know, that's picky...
https:/ /codereview. appspot. com/9953045/ diff/1/ app/websocketLo gging.js# newcode91 gging.js: 91: @param {Object} cfg general init config
app/websocketLo
object.
I think this was c-n-p as it should be 'log' and description.
Why do you even pass log since you bind 'this'? Just for clarity I
predict you'll say.
https:/ /codereview. appspot. com/9953045/ diff/1/ app/websocketLo gging.js# newcode110 gging.js: 110: /* global saveAs: false */
app/websocketLo
I don't understand this comment. Is it a lint directive?
It might be nice to state where saveAs normally is found.
https:/ /codereview. appspot. com/9953045/ diff/1/ test/test_ websocket_ logging. js websocket_ logging. js (right):
File test/test_
https:/ /codereview. appspot. com/9953045/ diff/1/ test/test_ websocket_ logging. js#newcode4 websocket_ logging. js:4: Copyright (C) 2012-2013 Canonical Ltd.
test/test_
s/2012-//
https:/ /codereview. appspot. com/9953045/