lib/webdriver/logging.js

1// Licensed to the Software Freedom Conservancy (SFC) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The SFC licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18/**
19 * @fileoverview Defines WebDriver's logging system. The logging system is
20 * broken into major components: local and remote logging.
21 *
22 * The local logging API, which is anchored by the
23 * {@link webdriver.logging.Logger Logger} class, is similar to Java's
24 * logging API. Loggers, retrieved by {@link webdriver.logging.getLogger}, use
25 * hierarchical, dot-delimited namespaces
26 * (e.g. "" > "webdriver" > "webdriver.logging"). Recorded log messages are
27 * represented by the {@link webdriver.logging.LogRecord LogRecord} class. You
28 * can capture log records by
29 * {@linkplain webdriver.logging.Logger#addHandler attaching} a handler function
30 * to the desired logger. For convenience, you can quickly enable logging to
31 * the console by simply calling
32 * {@link webdriver.logging.installConsoleHandler()}.
33 *
34 * The [remote logging API](https://github.com/SeleniumHQ/selenium/wiki/Logging)
35 * allows you to retrieve logs from a remote WebDriver server. This API uses the
36 * {@link Preferences} class to define desired log levels prior to create a
37 * WebDriver session:
38 *
39 * var prefs = new webdriver.logging.Preferences();
40 * prefs.setLevel(webdriver.logging.Type.BROWSER,
41 * webdriver.logging.Level.DEBUG);
42 *
43 * var caps = webdriver.Capabilities.chrome();
44 * caps.setLoggingPrefs(prefs);
45 * // ...
46 *
47 * Remote log entries are represented by the {@link Entry} class and may be
48 * retrieved via {@link webdriver.WebDriver.Logs}:
49 *
50 * driver.manage().logs().get(webdriver.logging.Type.BROWSER)
51 * .then(function(entries) {
52 * entries.forEach(function(entry) {
53 * console.log('[%s] %s', entry.level.name, entry.message);
54 * });
55 * });
56 *
57 * **NOTE:** Only a few browsers support the remote logging API (notably
58 * Firefox and Chrome). Firefox supports basic logging functionality, while
59 * Chrome exposes robust
60 * [performance logging](https://sites.google.com/a/chromium.org/chromedriver/logging)
61 * options. Remote logging is still considered a non-standard feature, and the
62 * APIs exposed by this module for it are non-frozen. Once logging is officially
63 * defined by the [W3C WebDriver spec](http://www.w3.org/TR/webdriver/), this
64 * module will be updated to use a consistent API for local and remote logging.
65 */
66
67goog.module('webdriver.logging');
68goog.module.declareLegacyNamespace();
69
70var LogManager = goog.require('goog.debug.LogManager');
71var LogRecord = goog.require('goog.debug.LogRecord');
72var Logger = goog.require('goog.debug.Logger');
73var Objects = goog.require('goog.object');
74var padNumber = goog.require('goog.string').padNumber;
75
76
77/** @const */
78exports.LogRecord = LogRecord;
79
80
81/** @const */
82exports.Logger = Logger;
83
84
85/** @const */
86exports.Level = Logger.Level;
87
88
89/**
90 * DEBUG is a message level for debugging messages and has the same log level
91 * as the {@link Logger.Level.CONFIG} message level.
92 * @const {!Logger.Level}
93 */
94Logger.Level.DEBUG = new Logger.Level('DEBUG', Logger.Level.CONFIG.value);
95
96
97/**
98 * Finds a named logger.
99 *
100 * @param {string=} opt_name The dot-delimited logger name, such as
101 * "webdriver.logging.Logger". Defaults to the name of the root logger.
102 * @return {!Logger} The named logger.
103 */
104function getLogger(opt_name) {
105 return LogManager.getLogger(opt_name || Logger.ROOT_LOGGER_NAME);
106}
107exports.getLogger = getLogger;
108
109
110/**
111 * Logs all messages to the Console API.
112 */
113function consoleHandler(record) {
114 if (typeof console === 'undefined') {
115 return;
116 }
117 record = /** @type {!LogRecord} */(record);
118 var timestamp = new Date(record.getMillis());
119 var msg =
120 '[' + timestamp.getUTCFullYear() + '-' +
121 padNumber(timestamp.getUTCMonth() + 1, 2) + '-' +
122 padNumber(timestamp.getUTCDate(), 2) + 'T' +
123 padNumber(timestamp.getUTCHours(), 2) + ':' +
124 padNumber(timestamp.getUTCMinutes(), 2) + ':' +
125 padNumber(timestamp.getUTCSeconds(), 2) + 'Z]' +
126 '[' + record.getLevel().name + ']' +
127 '[' + record.getLoggerName() + '] ' +
128 record.getMessage();
129
130 var level = record.getLevel().value;
131 if (level >= Logger.Level.SEVERE.value) {
132 console.error(msg);
133 } else if (level >= Logger.Level.WARNING.value) {
134 console.warn(msg);
135 } else {
136 console.log(msg);
137 }
138}
139
140
141/**
142 * Adds the console handler to the given logger. The console handler will log
143 * all messages using the JavaScript Console API.
144 *
145 * @param {Logger=} opt_logger The logger to add the handler to; defaults
146 * to the root logger.
147 * @see exports.removeConsoleHandler
148 */
149exports.addConsoleHandler = function(opt_logger) {
150 var logger = opt_logger || getLogger();
151 logger.addHandler(consoleHandler);
152};
153
154
155/**
156 * Installs the console log handler on the root logger.
157 * @see exports.addConsoleHandler
158 */
159exports.installConsoleHandler = function() {
160 exports.addConsoleHandler();
161};
162
163
164/**
165 * Removes the console log handler from the given logger.
166 *
167 * @param {Logger=} opt_logger The logger to remove the handler from; defaults
168 * to the root logger.
169 * @see exports.addConsoleHandler
170 */
171exports.removeConsoleHandler = function(opt_logger) {
172 var logger = opt_logger || getLogger();
173 logger.removeHandler(consoleHandler);
174};
175
176
177/**
178 * Converts a level name or value to a {@link webdriver.logging.Level} value.
179 * If the name/value is not recognized, {@link webdriver.logging.Level.ALL}
180 * will be returned.
181 * @param {(number|string)} nameOrValue The log level name, or value, to
182 * convert .
183 * @return {!Logger.Level} The converted level.
184 */
185function getLevel(nameOrValue) {
186 // DEBUG is not a predefined Closure log level, but maps to CONFIG. Since
187 // DEBUG is a predefined level for the WebDriver protocol, we prefer it over
188 // CONFIG.
189 if ('DEBUG' === nameOrValue || Logger.Level.DEBUG.value === nameOrValue) {
190 return Logger.Level.DEBUG;
191 } else if (goog.isString(nameOrValue)) {
192 return Logger.Level.getPredefinedLevel(/** @type {string} */(nameOrValue))
193 || Logger.Level.ALL;
194 } else {
195 return Logger.Level.getPredefinedLevelByValue(
196 /** @type {number} */(nameOrValue)) || Logger.Level.ALL;
197 }
198}
199exports.getLevel = getLevel;
200
201
202/**
203 * Normalizes a {@link Logger.Level} to one of the distinct values recognized
204 * by WebDriver's wire protocol.
205 * @param {!Logger.Level} level The input level.
206 * @return {!Logger.Level} The normalized level.
207 */
208function normalizeLevel(level) {
209 if (level.value <= Logger.Level.ALL.value) { // ALL is 0.
210 return Logger.Level.ALL;
211
212 } else if (level.value === Logger.Level.OFF.value) { // OFF is Infinity
213 return Logger.Level.OFF;
214
215 } else if (level.value < Logger.Level.INFO.value) {
216 return Logger.Level.DEBUG;
217
218 } else if (level.value < Logger.Level.WARNING.value) {
219 return Logger.Level.INFO;
220
221 } else if (level.value < Logger.Level.SEVERE.value) {
222 return Logger.Level.WARNING;
223
224 } else {
225 return Logger.Level.SEVERE;
226 }
227}
228
229
230/**
231 * Common log types.
232 * @enum {string}
233 */
234var Type = {
235 /** Logs originating from the browser. */
236 BROWSER: 'browser',
237 /** Logs from a WebDriver client. */
238 CLIENT: 'client',
239 /** Logs from a WebDriver implementation. */
240 DRIVER: 'driver',
241 /** Logs related to performance. */
242 PERFORMANCE: 'performance',
243 /** Logs from the remote server. */
244 SERVER: 'server'
245};
246exports.Type = Type;
247
248
249/**
250 * Describes the log preferences for a WebDriver session.
251 * @final
252 */
253var Preferences = goog.defineClass(null, {
254 /** @constructor */
255 constructor: function() {
256 /** @private {!Object.<string, Logger.Level>} */
257 this.prefs_ = {};
258 },
259
260 /**
261 * Sets the desired logging level for a particular log type.
262 * @param {(string|Type)} type The log type.
263 * @param {!Logger.Level} level The desired log level.
264 */
265 setLevel: function(type, level) {
266 this.prefs_[type] = normalizeLevel(level);
267 },
268
269 /**
270 * Converts this instance to its JSON representation.
271 * @return {!Object.<string, string>} The JSON representation of this set of
272 * preferences.
273 */
274 toJSON: function() {
275 var obj = {};
276 for (var type in this.prefs_) {
277 if (this.prefs_.hasOwnProperty(type)) {
278 obj[type] = this.prefs_[type].name;
279 }
280 }
281 return obj;
282 }
283});
284exports.Preferences = Preferences;
285
286
287/**
288 * A single log entry recorded by a WebDriver component, such as a remote
289 * WebDriver server.
290 * @final
291 */
292var Entry = goog.defineClass(null, {
293 /**
294 * @param {(!Logger.Level|string)} level The entry level.
295 * @param {string} message The log message.
296 * @param {number=} opt_timestamp The time this entry was generated, in
297 * milliseconds since 0:00:00, January 1, 1970 UTC. If omitted, the
298 * current time will be used.
299 * @param {string=} opt_type The log type, if known.
300 * @constructor
301 */
302 constructor: function(level, message, opt_timestamp, opt_type) {
303
304 /** @type {!Logger.Level} */
305 this.level = goog.isString(level) ? getLevel(level) : level;
306
307 /** @type {string} */
308 this.message = message;
309
310 /** @type {number} */
311 this.timestamp = goog.isNumber(opt_timestamp) ? opt_timestamp : goog.now();
312
313 /** @type {string} */
314 this.type = opt_type || '';
315 },
316
317 statics: {
318 /**
319 * Converts a {@link goog.debug.LogRecord} into a
320 * {@link webdriver.logging.Entry}.
321 * @param {!goog.debug.LogRecord} logRecord The record to convert.
322 * @param {string=} opt_type The log type.
323 * @return {!Entry} The converted entry.
324 */
325 fromClosureLogRecord: function(logRecord, opt_type) {
326 return new Entry(
327 normalizeLevel(/** @type {!Logger.Level} */(logRecord.getLevel())),
328 '[' + logRecord.getLoggerName() + '] ' + logRecord.getMessage(),
329 logRecord.getMillis(),
330 opt_type);
331 }
332 },
333
334 /**
335 * @return {{level: string, message: string, timestamp: number,
336 * type: string}} The JSON representation of this entry.
337 */
338 toJSON: function() {
339 return {
340 'level': this.level.name,
341 'message': this.message,
342 'timestamp': this.timestamp,
343 'type': this.type
344 };
345 }
346});
347exports.Entry = Entry;