Merge lp:~benji/juju-gui/websocket-logging into lp:juju-gui/experimental

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
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+167034@code.launchpad.net

Description of the change

Adds a websocket recording and download feature.

When the websocket_capture feature flag is enabled (e.g.,
/:flags:/websocket_capture/) the websocket traffic will be recorded. When the
user presses Control-Shift-d, the log will be downloaded to their local
machine.

https://codereview.appspot.com/9953045/

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

LGTM with minors.

https://codereview.appspot.com/9953045/diff/1/Makefile
File Makefile (right):

https://codereview.appspot.com/9953045/diff/1/Makefile#newcode320
Makefile:320: build-debug/juju-ui/websocketLogging.js \
camelCaseFileNames? I-thought-we-liked-hyphens.

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
File app/assets/javascripts/reconnecting-websocket.js (right):

https://codereview.appspot.com/9953045/diff/1/app/assets/javascripts/reconnecting-websocket.js#newcode49
app/assets/javascripts/reconnecting-websocket.js:49:
bless you.

https://codereview.appspot.com/9953045/diff/1/app/websocketLogging.js
File app/websocketLogging.js (right):

https://codereview.appspot.com/9953045/diff/1/app/websocketLogging.js#newcode4
app/websocketLogging.js:4: Copyright (C) 2012-2013 Canonical Ltd.
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/websocketLogging.js#newcode91
app/websocketLogging.js:91: @param {Object} cfg general init config
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/websocketLogging.js#newcode110
app/websocketLogging.js:110: /* global saveAs: false */
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
File test/test_websocket_logging.js (right):

https://codereview.appspot.com/9953045/diff/1/test/test_websocket_logging.js#newcode4
test/test_websocket_logging.js:4: Copyright (C) 2012-2013 Canonical Ltd.
s/2012-//

https://codereview.appspot.com/9953045/

Revision history for this message
Nicola Larosa (teknico) wrote :
lp:~benji/juju-gui/websocket-logging updated
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

Subscribers

People subscribed via source and target branches