Merge lp:~mpontillo/maas/session-service into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Rejected
Rejected by: MAAS Lander
Proposed branch: lp:~mpontillo/maas/session-service
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 544 lines (+462/-5)
10 files modified
src/maasserver/static/js/angular/directives/version_reloader.js (+10/-5)
src/maasserver/static/js/angular/maas.js (+6/-0)
src/maasserver/static/js/angular/services/log.js (+95/-0)
src/maasserver/static/js/angular/services/metrics.js (+94/-0)
src/maasserver/static/js/angular/services/session.js (+26/-0)
src/maasserver/static/js/angular/services/tests/test_log.js (+112/-0)
src/maasserver/static/js/angular/services/tests/test_metrics.js (+68/-0)
src/maasserver/static/js/angular/services/tests/test_session.js (+29/-0)
src/maasserver/static/js/angular/testing/setup.js (+19/-0)
src/maasserver/views/combo.py (+3/-0)
To merge this branch: bzr merge lp:~mpontillo/maas/session-service
Reviewer Review Type Date Requested Status
MAAS Maintainers Pending
Review via email: mp+315325@code.launchpad.net

Commit message

Add client-side services for logging, sessions, and metrics.

 * LogService
   - Adds log levels and the ability to enable/disable logging.
   - Adds formatted timestamps to the beginning of each log message.

 * SessionService
   - Provides a way to persist data inside an Angular "session" across
     scopes being destroyed and recreated.

 * MetricsService
   - Add summary logging for when $digest cycles occur.
   - Add logging for when route changes occur (such as navigation to another page).

Drive-by fix for logging in version reloader. (Fix a bug in the logging, and change it to use the LogService.)

Description of the change

The branch name is confusing; at first this was just a SessionService, but it has been refactored into three different services, for clean separation of responsibilities.

First, this branch adds a SessionService to the $rootScope, which can be used to persist data for various views (such as the current state of the "group by" box on the network listing page). It has a facility for a particular scope (such as the NetworksListController) to store and retrieve a dictionary, which is scoped to the current 'angular session'. (Cookies could have been used for this, but the cookies API changed between Angular 1.2 and 1.3, so I wanted to avoid that for now.)

Next, while troubleshooting the networks listing page, I wanted to know when $digest was being called, and roughly how long it was taking. (And I was frustrated that it was difficult to get that data.) So I figured out a clean way to summarize calls to $digest without spamming the log too much, and added a MetricsService to handle that.

Having done all that, I noticed that the new cleaner logging clashed with the logging in the version reloader directive (which was somewhat buggy, in that it logged for no reason). So I cleaned that up so that it's more useful now, and made that use the SessionService for logging, to be consistent (and so that I could centralize logging configuration in the SessionService).

Lastly, I then realized that it was difficult to tell which $digest logs were relevant to which page as I navigated around. So I added logging for route updates as well.

The LogService was later refactored out as its own service, since it didn't belong in services intended for persisting session information or metrics. With that, I also added support for all the different log levels, and allow the log level to be specified.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

Transitioned to Git.

lp:maas has now moved from Bzr to Git.
Please propose your branches with Launchpad using Git.

git clone https://git.launchpad.net/maas

Unmerged revisions

5654. By Mike Pontillo

Add tests for MetricsService.

5653. By Mike Pontillo

Add test for __log().

5652. By Mike Pontillo

Add test for formatMilliseconds.

5651. By Mike Pontillo

Add tests for LogSerivce.

5650. By Mike Pontillo

80 column rule.

5649. By Mike Pontillo

Minor cleanup.

5648. By Mike Pontillo

Add tests for SessionSerivce.

5647. By Mike Pontillo

SRP: Split SessionService into three different services.

5646. By Mike Pontillo

Fix unintentional change.

5645. By Mike Pontillo

Add missing file.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/static/js/angular/directives/version_reloader.js'
--- src/maasserver/static/js/angular/directives/version_reloader.js 2016-09-21 14:49:23 +0000
+++ src/maasserver/static/js/angular/directives/version_reloader.js 2017-01-23 06:29:49 +0000
@@ -9,8 +9,8 @@
99
1010
11angular.module('MAAS').directive('maasVersionReloader', [11angular.module('MAAS').directive('maasVersionReloader', [
12 '$window', 'GeneralManager', 'ManagerHelperService',12 '$window', 'GeneralManager', 'ManagerHelperService', 'LogService',
13 function($window, GeneralManager, ManagerHelperService) {13 function($window, GeneralManager, ManagerHelperService, LogService) {
14 return {14 return {
15 restrict: "A",15 restrict: "A",
16 controller: function($scope) {16 controller: function($scope) {
@@ -25,12 +25,17 @@
25 ManagerHelperService.loadManager($scope, GeneralManager).then(25 ManagerHelperService.loadManager($scope, GeneralManager).then(
26 function() {26 function() {
27 GeneralManager.enableAutoReload(true);27 GeneralManager.enableAutoReload(true);
28 LogService.info(
29 'Version reloader: Monitoring MAAS "' +
30 $scope.site + '"; version', $scope.version.text,
31 "via", $window.location.href);
28 $scope.$watch("version.text",32 $scope.$watch("version.text",
29 function(newValue, oldValue) {33 function(newValue, oldValue) {
30 console.log(
31 "Detected new MAAS version; " +
32 "forcing reload.");
33 if(newValue !== oldValue) {34 if(newValue !== oldValue) {
35 LogService.info(
36 "MAAS version changed from '" +
37 oldValue + "' to '" + newValue +
38 "'; forcing reload.");
34 $scope.reloadPage();39 $scope.reloadPage();
35 }40 }
36 });41 });
3742
=== modified file 'src/maasserver/static/js/angular/maas.js'
--- src/maasserver/static/js/angular/maas.js 2016-12-14 11:07:08 +0000
+++ src/maasserver/static/js/angular/maas.js 2017-01-23 06:29:49 +0000
@@ -148,6 +148,12 @@
148 }148 }
149 });149 });
150150
151// Make sure the MetricsService is created for every run, so we can get
152// accurate numbers.
153angular.module('MAAS').run(['MetricsService', function (MetricsService) {
154 MetricsService.setConfigDone();
155}]);
156
151// Force users to #/intro when it has not been completed.157// Force users to #/intro when it has not been completed.
152angular.module('MAAS').run(['$rootScope', '$location',158angular.module('MAAS').run(['$rootScope', '$location',
153 function ($rootScope, $location) {159 function ($rootScope, $location) {
154160
=== added file 'src/maasserver/static/js/angular/services/log.js'
--- src/maasserver/static/js/angular/services/log.js 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/angular/services/log.js 2017-01-23 06:29:49 +0000
@@ -0,0 +1,95 @@
1/* Copyright 2017 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Log Service
5 *
6 * Allows logging to be enabled or disabled, and allows a particular log level
7 * to be set, which will allow logs of a specified importance (or more) to be
8 * seen.
9 *
10 * Also appends a time index (in seconds, accurate to milliseconds) to the
11 * beginning of the log string.
12 */
13
14angular.module('MAAS').service('LogService', [
15 '$window', function($window) {
16 var self = this;
17
18 // Global enable/disable for MAAS logging. If set to `false`, this
19 // value takes precedence over the logLevel.
20 self.logging = true;
21
22 // Specifies the log level.
23 // Level 1: error() logging
24 // Level 2: error() and warn() logging
25 // Level 3: all of the above, plus info()
26 // Level 4: all of the above, plus log()
27 // Level 5: all of the above, plus debug()
28 self.logLevel = 5;
29
30 self.metrics = {};
31
32 // Returns a monotonic time (in milliseconds) since page load.
33 self.now = function() {
34 return $window.performance ? $window.performance.now() : 0;
35 };
36
37 // Standard logging functions.
38 self._debug = console.debug;
39 self._log = console.log;
40 self._info = console.info;
41 self._warn = console.warn;
42 self._error = console.error;
43
44 // Formats the specified time (in milliseconds) in seconds.
45 this.formatMilliseconds = function(milliseconds) {
46 return parseFloat(milliseconds / 1000.0).toFixed(3);
47 };
48
49 // Internal function to log using the specified destination, with the
50 // specified list of arguments.
51 this.__log = function(destination, args) {
52 if(self.logging === true) {
53 // Add time index to the beginning of the log.
54 Array.prototype.unshift.call(args,
55 "[" + self.formatMilliseconds(self.now()) + "]");
56 destination.apply(self, args);
57 }
58 };
59
60 // Wrapper to check the log level and then call self._debug().
61 this.debug = function() {
62 if(self.logLevel >= 5) {
63 self.__log(self._debug, arguments);
64 }
65 };
66
67 // Wrapper to check the log level and then call self._log().
68 this.log = function() {
69 if(self.logLevel >= 4) {
70 self.__log(self._log, arguments);
71 }
72 };
73
74 // Wrapper to check the log level and then call self._info().
75 this.info = function() {
76 if(self.logLevel >= 3) {
77 self.__log(self._info, arguments);
78 }
79 };
80
81 // Wrapper to check the log level and then call self._warn().
82 this.warn = function() {
83 if(self.logLevel >= 2) {
84 self.__log(self._warn, arguments);
85 }
86 };
87
88 // Wrapper to check the log level and then call self._err().
89 this.error = function() {
90 if(self.logLevel >= 1) {
91 self.__log(self._error, arguments);
92 }
93 };
94 }
95]);
096
=== added file 'src/maasserver/static/js/angular/services/metrics.js'
--- src/maasserver/static/js/angular/services/metrics.js 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/angular/services/metrics.js 2017-01-23 06:29:49 +0000
@@ -0,0 +1,94 @@
1/* Copyright 2017 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Metrics Service
5 *
6 * Records and logs metrics about various events. Currently, that includes:
7 * - The $digest lifecycle.
8 * - Any route changes (navigations).
9 */
10
11angular.module('MAAS').service('MetricsService', [
12 '$rootScope', '$window', '$timeout', 'LogService', function(
13 $rootScope, $window, $timeout, LogService) {
14
15 var self = this;
16
17 self.metrics = {};
18
19 // Initial metric values.
20 self.metrics.digestCount = 0;
21 self.metrics.openDigests = [];
22 self.metrics.digestCycles = 0;
23
24 this.info = LogService.info;
25 this.debug = LogService.debug;
26
27 this.formatMilliseconds = LogService.formatMilliseconds;
28
29 // Returns a monotonic time in milliseconds.
30 this.now = function() {
31 return LogService.now();
32 };
33
34 // Returns the current URL string.
35 this.location = function() {
36 return $window.location;
37 };
38
39 // Called when the MAAS module starts running, to record the time it
40 // took to initialize.
41 this.setConfigDone = function() {
42 self.metrics.configDone = self.now();
43 };
44
45 // Called from $rootScope on $digest every $watch cycle.
46 this.updateDigestMetrics = function() {
47 var now = self.now();
48 var digestCount = ++self.metrics.digestCount;
49 // Keep a stack of "open" $digest calls. A $digest cycle is
50 // considered done after Angular has a chance to process all the
51 // timeouts generated by each call to $digest.
52 self.metrics.openDigests.push({'time': now, 'count': digestCount});
53 self.metrics.lastDigest = now;
54 // Since we're interested in how long $digest cycles are taking,
55 // ask Angular to call us back at its earliest convenience.
56 // The result is that if multiple $digest calls kick off in
57 // succession, the last $timeout will be called after they are
58 // *all*, complete, which allows us to calculate a rough estimate
59 // of how long the complete cycle took.
60 $timeout(function() {
61 var digest = self.metrics.openDigests.pop();
62 // If we've popped the last $digest call off the stack, that
63 // means we're done with a complete cycle.
64 if(self.metrics.openDigests.length == 0) {
65 var cycles = ++self.metrics.digestCycles;
66 var finished = self.now();
67 var started = digest.time;
68 var duration = finished - started;
69 var startCount = digest.count;
70 var finishCount = self.metrics.digestCount;
71 var count = finishCount - startCount + 1;
72 var countMessage = (count == 1) ?
73 count + " digest" : count + " digests";
74 var seconds = self.formatMilliseconds(duration);
75 self.debug("$digest cycle " + cycles + ": " +
76 countMessage + " completed in ~" +
77 seconds + " seconds.");
78 }
79 }, 0, false);
80 };
81
82 this.logRouteChange = function(event, current, previous) {
83 var currentLocation = self.location();
84 self.info(
85 "$routeChangeSuccess: " + currentLocation +
86 "; templateURL=" + current.templateUrl);
87 };
88
89 // Add the MetricsService to the $rootScope so that $watch works.
90 $rootScope.MetricsService = this;
91 $rootScope.$watch('MetricsService.updateDigestMetrics()');
92 $rootScope.$on("$routeChangeSuccess", this.logRouteChange);
93}]);
94
095
=== added file 'src/maasserver/static/js/angular/services/session.js'
--- src/maasserver/static/js/angular/services/session.js 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/angular/services/session.js 2017-01-23 06:29:49 +0000
@@ -0,0 +1,26 @@
1/* Copyright 2017 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Session Service
5 *
6 * Allows persistence of arbitrary data between a scope's destruction and
7 * subsequent creation.
8 *
9 */
10
11angular.module('MAAS').service('SessionService', [
12 function() {
13
14 var _scopes = {};
15
16 // Retrieves the session for the specified scope.
17 // The scope should be specified in terms of a controller name for a
18 // particular page. If a session does not exist, one will be created.
19 this.getSessionFor = function(scope) {
20 if (!angular.isObject(_scopes[scope])) {
21 _scopes[scope] = {}
22 }
23 return _scopes[scope];
24 };
25}]);
26
027
=== added file 'src/maasserver/static/js/angular/services/tests/test_log.js'
--- src/maasserver/static/js/angular/services/tests/test_log.js 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/angular/services/tests/test_log.js 2017-01-23 06:29:49 +0000
@@ -0,0 +1,112 @@
1/* Copyright 2015 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Unit tests for SessionService.
5 */
6
7describe("LogService", function() {
8
9 beforeEach(function() {
10 spyOn(console, 'debug');
11 spyOn(console, 'log');
12 spyOn(console, 'info');
13 spyOn(console, 'warn');
14 spyOn(console, 'error');
15 });
16
17 // Load the MAAS module.
18 beforeEach(module("MAAS"));
19
20 // Get the $injector so the test can grab the LogService.
21 var $injector;
22 var LogService;
23 beforeEach(inject(function(_$injector_) {
24 $injector = _$injector_;
25 LogService = $injector.get("LogService");
26 }));
27
28 var scenarios = [
29 {
30 'it': "console.error",
31 'logLevel': 1,
32 'function': 'error'
33 },
34 {
35 'it': "console.warn",
36 'logLevel': 2,
37 'function': 'warn'
38 },
39 {
40 'it': "console.info",
41 'logLevel': 3,
42 'function': 'info'
43 },
44 {
45 'it': "console.log",
46 'logLevel': 4,
47 'function': 'log'
48 },
49 {
50 'it': "console.debug",
51 'logLevel': 5,
52 'function': 'debug'
53 }
54 ];
55
56 // Do positive tests for appropriate log levels.
57 angular.forEach(scenarios, function(scenario) {
58 it("calls " + scenario.it + " for logLevel=" + scenario.logLevel,
59 function() {
60 LogService.logging = true;
61 LogService.logLevel = scenario.logLevel;
62 var logFunction = LogService[scenario.function];
63 var message = makeName();
64 logFunction(message);
65 expect(console[scenario.function]).toHaveBeenCalled();
66 });
67 });
68
69 // Do negative tests for log levels that do not include the level.
70 angular.forEach(scenarios, function(scenario) {
71 it("doesn't call " + scenario.it + " for logLevel=" +
72 (scenario.logLevel - 1), function() {
73 LogService.logging = true;
74 LogService.logLevel = scenario.logLevel - 1;
75 var logFunction = LogService[scenario.function];
76 var message = makeName();
77 logFunction(message);
78 expect(console[scenario.function]).not.toHaveBeenCalled();
79 });
80 });
81
82 describe("formatMilliseconds", function() {
83
84 it("formats milliseconds into equivalent decimal seconds", function() {
85 var result = LogService.formatMilliseconds(1234);
86 expect(result).toEqual("1.234");
87 });
88
89 it("zero-pads values", function() {
90 var result = LogService.formatMilliseconds(1000);
91 expect(result).toEqual("1.000");
92 });
93 });
94
95 describe("__log", function() {
96
97 it("appends timestamp to the beginning of the log", function() {
98 LogService.logging = true;
99 var message = makeName();
100 var outMessage = null;
101 LogService.now = function() {
102 return 0;
103 };
104 LogService.__log(function() {
105 // __log() will call the destination log function after
106 // appending the formatted time to the beginning.
107 outMessage = [arguments[0], arguments[1]];
108 }, [message]);
109 expect(outMessage).toEqual(["[0.000]", message]);
110 });
111 });
112});
0113
=== added file 'src/maasserver/static/js/angular/services/tests/test_metrics.js'
--- src/maasserver/static/js/angular/services/tests/test_metrics.js 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/angular/services/tests/test_metrics.js 2017-01-23 06:29:49 +0000
@@ -0,0 +1,68 @@
1/* Copyright 2015 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Unit tests for MetricsService.
5 */
6
7describe("MetricsService", function() {
8
9 // Load the MAAS module.
10 beforeEach(module("MAAS"));
11
12 // Get the $injector so the test can grab the MetricsService.
13 var $injector;
14 var $rootScope;
15 beforeEach(inject(function(_$injector_) {
16 $injector = _$injector_;
17 $rootScope = $injector.get("$rootScope");
18 }));
19
20 describe("init", function() {
21 // Note: we can't use spyOn() here to check if $watch and/or $on were
22 // called, since the init happens too early for that.
23
24 it("addsWorkingWatchToRootScope", function () {
25 var MetricsService = $injector.get("MetricsService");
26 expect($rootScope.MetricsService).toBe(MetricsService);
27 spyOn(MetricsService, 'updateDigestMetrics');
28 // Check if the $watch worked.
29 $rootScope.$apply();
30 expect(MetricsService.updateDigestMetrics).toHaveBeenCalled();
31 });
32
33 it("logsRouteChangeAsInformational", function () {
34 var MetricsService = $injector.get("MetricsService");
35 spyOn(MetricsService, 'info');
36 // Getting the actual route to change in a unit test is annoying,
37 // so we'll just call logRouteChange directly.
38 MetricsService.logRouteChange({}, {'templateUrl': 'test.html'});
39 expect(MetricsService.info).toHaveBeenCalled();
40 });
41
42 it("setsConfigDone", function () {
43 var MetricsService = $injector.get("MetricsService");
44 // This is called via run() on the MAAS module.
45 expect(MetricsService.metrics.configDone).toBeDefined();
46 });
47
48 });
49
50 describe("updateDigestMetrics", function() {
51 it("updatesOpenDigests", function() {
52 var $timeout = $injector.get("$timeout");
53 var MetricsService = $injector.get("MetricsService");
54 // Calling updateDigestMetrics() for the first time should queue
55 // an open $digest.
56 MetricsService.updateDigestMetrics();
57 expect(
58 MetricsService.metrics.openDigests.length).toBe(1);
59 // The $apply should queue another open $digest.
60 $rootScope.$apply();
61 expect(
62 MetricsService.metrics.openDigests.length).toBeGreaterThan(1);
63 // This should close all the open $digests.
64 $timeout.flush();
65 expect(MetricsService.metrics.openDigests.length).toEqual(0);
66 });
67 });
68});
069
=== added file 'src/maasserver/static/js/angular/services/tests/test_session.js'
--- src/maasserver/static/js/angular/services/tests/test_session.js 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/angular/services/tests/test_session.js 2017-01-23 06:29:49 +0000
@@ -0,0 +1,29 @@
1/* Copyright 2015 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Unit tests for SessionService.
5 */
6
7describe("SessionService", function() {
8
9 // Load the MAAS module.
10 beforeEach(module("MAAS"));
11
12 // Get the $injector so the test can grab the BrowserService.
13 var $injector;
14 beforeEach(inject(function(_$injector_) {
15 $injector = _$injector_;
16 }));
17
18 it("persistsArbitraryData", function() {
19 var SessionService = $injector.get("SessionService");
20 var scope = makeName();
21 var session = SessionService.getSessionFor(scope);
22 expect(session).toEqual({});
23 var dataKey = makeName();
24 var dataValue = makeName();
25 session[dataKey] = dataValue;
26 session = SessionService.getSessionFor(scope);
27 expect(session[dataKey]).toBe(dataValue);
28 });
29});
030
=== added file 'src/maasserver/static/js/angular/testing/setup.js'
--- src/maasserver/static/js/angular/testing/setup.js 1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/angular/testing/setup.js 2017-01-23 06:29:49 +0000
@@ -0,0 +1,19 @@
1/* Copyright 2017 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Test Setup
5 *
6 * Special setup that occurs only for unit testing.
7 */
8
9angular.module('MAAS').run(['LogService', function (LogService) {
10 // Silence logging by default in the tests.
11 LogService.logging = false;
12
13 // Make our own monotonic clock for testing, since the unit test suite
14 // won't have $window.performance.
15 var time = 0;
16 LogService.now = function() {
17 return time++;
18 };
19}]);
020
=== modified file 'src/maasserver/views/combo.py'
--- src/maasserver/views/combo.py 2016-12-14 11:07:08 +0000
+++ src/maasserver/views/combo.py 2017-01-23 06:29:49 +0000
@@ -90,6 +90,9 @@
90 "js/angular/services/browser.js",90 "js/angular/services/browser.js",
91 "js/angular/services/converter.js",91 "js/angular/services/converter.js",
92 "js/angular/services/json.js",92 "js/angular/services/json.js",
93 "js/angular/services/session.js",
94 "js/angular/services/log.js",
95 "js/angular/services/metrics.js",
93 "js/angular/directives/accordion.js",96 "js/angular/directives/accordion.js",
94 "js/angular/directives/boot_images.js",97 "js/angular/directives/boot_images.js",
95 "js/angular/directives/call_to_action.js",98 "js/angular/directives/call_to_action.js",