# 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 |
67 | goog.module('webdriver.logging');
68 | goog.module.declareLegacyNamespace();
69 |
70 | var LogManager = goog.require('goog.debug.LogManager');
71 | var LogRecord = goog.require('goog.debug.LogRecord');
72 | var Logger = goog.require('goog.debug.Logger');
73 | var Objects = goog.require('goog.object');
74 | var padNumber = goog.require('goog.string').padNumber;
75 |
76 |
77 | /** @const */
78 | exports.LogRecord = LogRecord;
79 |
80 |
81 | /** @const */
82 | exports.Logger = Logger;
83 |
84 |
85 | /** @const */
86 | exports.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 | */
94 | Logger.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 | */
104 | function getLogger(opt_name) {
105 | return LogManager.getLogger(opt_name || Logger.ROOT_LOGGER_NAME);
106 | }
107 | exports.getLogger = getLogger;
108 |
109 |
110 | /**
111 | * Logs all messages to the Console API.
112 | */
113 | function consoleHandler(record) {
114 | if (typeof console === 'undefined' || !console) {
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 | */
149 | exports.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 | */
159 | exports.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 | */
171 | exports.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 | */
185 | function 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 | }
199 | exports.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 | */
208 | function 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 | */
234 | var 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 | };
246 | exports.Type = Type;
247 |
248 |
249 | /**
250 | * Describes the log preferences for a WebDriver session.
251 | * @final
252 | */
253 | var 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 | });
284 | exports.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 | */
292 | var 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 | });
347 | exports.Entry = Entry;