UNPKG

17.8 kBJavaScriptView Raw
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'use strict';
19
20/**
21 * @fileoverview Defines WebDriver's logging system. The logging system is
22 * broken into major components: local and remote logging.
23 *
24 * The local logging API, which is anchored by the {@linkplain Logger} class is
25 * similar to Java's logging API. Loggers, retrieved by
26 * {@linkplain #getLogger getLogger(name)}, use hierarchical, dot-delimited
27 * namespaces (e.g. "" > "webdriver" > "webdriver.logging"). Recorded log
28 * messages are represented by the {@linkplain Entry} class. You can capture log
29 * records by {@linkplain Logger#addHandler attaching} a handler function to the
30 * desired logger. For convenience, you can quickly enable logging to the
31 * console by simply calling {@linkplain #installConsoleHandler
32 * 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 creating
37 * a WebDriver session:
38 *
39 * var prefs = new logging.Preferences();
40 * prefs.setLevel(logging.Type.BROWSER, logging.Level.DEBUG);
41 *
42 * var caps = Capabilities.chrome();
43 * caps.setLoggingPrefs(prefs);
44 * // ...
45 *
46 * Remote log entries, also represented by the {@link Entry} class, may be
47 * retrieved via {@link webdriver.WebDriver.Logs}:
48 *
49 * driver.manage().logs().get(logging.Type.BROWSER)
50 * .then(function(entries) {
51 * entries.forEach(function(entry) {
52 * console.log('[%s] %s', entry.level.name, entry.message);
53 * });
54 * });
55 *
56 * **NOTE:** Only a few browsers support the remote logging API (notably
57 * Firefox and Chrome). Firefox supports basic logging functionality, while
58 * Chrome exposes robust
59 * [performance logging](https://sites.google.com/a/chromium.org/chromedriver/logging)
60 * options. Remote logging is still considered a non-standard feature, and the
61 * APIs exposed by this module for it are non-frozen. This module will be
62 * updated, possibly breaking backwards-compatibility, once logging is
63 * officially defined by the
64 * [W3C WebDriver spec](http://www.w3.org/TR/webdriver/).
65 */
66
67/**
68 * Defines a message level that may be used to control logging output.
69 *
70 * @final
71 */
72class Level {
73 /**
74 * @param {string} name the level's name.
75 * @param {number} level the level's numeric value.
76 */
77 constructor(name, level) {
78 if (level < 0) {
79 throw new TypeError('Level must be >= 0');
80 }
81
82 /** @private {string} */
83 this.name_ = name;
84
85 /** @private {number} */
86 this.value_ = level;
87 }
88
89 /** This logger's name. */
90 get name() {
91 return this.name_;
92 }
93
94 /** The numeric log level. */
95 get value() {
96 return this.value_;
97 }
98
99 /** @override */
100 toString() {
101 return this.name;
102 }
103}
104
105/**
106 * Indicates no log messages should be recorded.
107 * @const
108 */
109Level.OFF = new Level('OFF', Infinity);
110
111
112/**
113 * Log messages with a level of `1000` or higher.
114 * @const
115 */
116Level.SEVERE = new Level('SEVERE', 1000);
117
118
119/**
120 * Log messages with a level of `900` or higher.
121 * @const
122 */
123Level.WARNING = new Level('WARNING', 900);
124
125
126/**
127 * Log messages with a level of `800` or higher.
128 * @const
129 */
130Level.INFO = new Level('INFO', 800);
131
132
133/**
134 * Log messages with a level of `700` or higher.
135 * @const
136 */
137Level.DEBUG = new Level('DEBUG', 700);
138
139
140/**
141 * Log messages with a level of `500` or higher.
142 * @const
143 */
144Level.FINE = new Level('FINE', 500);
145
146
147/**
148 * Log messages with a level of `400` or higher.
149 * @const
150 */
151Level.FINER = new Level('FINER', 400);
152
153
154/**
155 * Log messages with a level of `300` or higher.
156 * @const
157 */
158Level.FINEST = new Level('FINEST', 300);
159
160
161/**
162 * Indicates all log messages should be recorded.
163 * @const
164 */
165Level.ALL = new Level('ALL', 0);
166
167
168const ALL_LEVELS = /** !Set<Level> */new Set([
169 Level.OFF,
170 Level.SEVERE,
171 Level.WARNING,
172 Level.INFO,
173 Level.DEBUG,
174 Level.FINE,
175 Level.FINER,
176 Level.FINEST,
177 Level.ALL
178]);
179
180
181const LEVELS_BY_NAME = /** !Map<string, !Level> */ new Map([
182 [Level.OFF.name, Level.OFF],
183 [Level.SEVERE.name, Level.SEVERE],
184 [Level.WARNING.name, Level.WARNING],
185 [Level.INFO.name, Level.INFO],
186 [Level.DEBUG.name, Level.DEBUG],
187 [Level.FINE.name, Level.FINE],
188 [Level.FINER.name, Level.FINER],
189 [Level.FINEST.name, Level.FINEST],
190 [Level.ALL.name, Level.ALL]
191]);
192
193
194/**
195 * Converts a level name or value to a {@link Level} value. If the name/value
196 * is not recognized, {@link Level.ALL} will be returned.
197 *
198 * @param {(number|string)} nameOrValue The log level name, or value, to
199 * convert.
200 * @return {!Level} The converted level.
201 */
202function getLevel(nameOrValue) {
203 if (typeof nameOrValue === 'string') {
204 return LEVELS_BY_NAME.get(nameOrValue) || Level.ALL;
205 }
206 if (typeof nameOrValue !== 'number') {
207 throw new TypeError('not a string or number');
208 }
209 for (let level of ALL_LEVELS) {
210 if (nameOrValue >= level.value) {
211 return level;
212 }
213 }
214 return Level.ALL;
215}
216
217
218/**
219 * Describes a single log entry.
220 *
221 * @final
222 */
223class Entry {
224 /**
225 * @param {(!Level|string|number)} level The entry level.
226 * @param {string} message The log message.
227 * @param {number=} opt_timestamp The time this entry was generated, in
228 * milliseconds since 0:00:00, January 1, 1970 UTC. If omitted, the
229 * current time will be used.
230 * @param {string=} opt_type The log type, if known.
231 */
232 constructor(level, message, opt_timestamp, opt_type) {
233 this.level = level instanceof Level ? level : getLevel(level);
234 this.message = message;
235 this.timestamp =
236 typeof opt_timestamp === 'number' ? opt_timestamp : Date.now();
237 this.type = opt_type || '';
238 }
239
240 /**
241 * @return {{level: string, message: string, timestamp: number,
242 * type: string}} The JSON representation of this entry.
243 */
244 toJSON() {
245 return {
246 'level': this.level.name,
247 'message': this.message,
248 'timestamp': this.timestamp,
249 'type': this.type
250 };
251 }
252}
253
254
255/** @typedef {(string|function(): string)} */
256let Loggable;
257
258
259/**
260 * An object used to log debugging messages. Loggers use a hierarchical,
261 * dot-separated naming scheme. For instance, "foo" is considered the parent of
262 * the "foo.bar" and an ancestor of "foo.bar.baz".
263 *
264 * Each logger may be assigned a {@linkplain #setLevel log level}, which
265 * controls which level of messages will be reported to the
266 * {@linkplain #addHandler handlers} attached to this instance. If a log level
267 * is not explicitly set on a logger, it will inherit its parent.
268 *
269 * This class should never be directly instantiated. Instead, users should
270 * obtain logger references using the {@linkplain ./logging.getLogger()
271 * getLogger()} function.
272 *
273 * @final
274 */
275class Logger {
276 /**
277 * @param {string} name the name of this logger.
278 * @param {Level=} opt_level the initial level for this logger.
279 */
280 constructor(name, opt_level) {
281 /** @private {string} */
282 this.name_ = name;
283
284 /** @private {Level} */
285 this.level_ = opt_level || null;
286
287 /** @private {Logger} */
288 this.parent_ = null;
289
290 /** @private {Set<function(!Entry)>} */
291 this.handlers_ = null;
292 }
293
294 /** @return {string} the name of this logger. */
295 getName() {
296 return this.name_;
297 }
298
299 /**
300 * @param {Level} level the new level for this logger, or `null` if the logger
301 * should inherit its level from its parent logger.
302 */
303 setLevel(level) {
304 this.level_ = level;
305 }
306
307 /** @return {Level} the log level for this logger. */
308 getLevel() {
309 return this.level_;
310 }
311
312 /**
313 * @return {!Level} the effective level for this logger.
314 */
315 getEffectiveLevel() {
316 let logger = this;
317 let level;
318 do {
319 level = logger.level_;
320 logger = logger.parent_;
321 } while (logger && !level);
322 return level || Level.OFF;
323 }
324
325 /**
326 * @param {!Level} level the level to check.
327 * @return {boolean} whether messages recorded at the given level are loggable
328 * by this instance.
329 */
330 isLoggable(level) {
331 return level.value !== Level.OFF.value
332 && level.value >= this.getEffectiveLevel().value;
333 }
334
335 /**
336 * Adds a handler to this logger. The handler will be invoked for each message
337 * logged with this instance, or any of its descendants.
338 *
339 * @param {function(!Entry)} handler the handler to add.
340 */
341 addHandler(handler) {
342 if (!this.handlers_) {
343 this.handlers_ = new Set;
344 }
345 this.handlers_.add(handler);
346 }
347
348 /**
349 * Removes a handler from this logger.
350 *
351 * @param {function(!Entry)} handler the handler to remove.
352 * @return {boolean} whether a handler was successfully removed.
353 */
354 removeHandler(handler) {
355 if (!this.handlers_) {
356 return false;
357 }
358 return this.handlers_.delete(handler);
359 }
360
361 /**
362 * Logs a message at the given level. The message may be defined as a string
363 * or as a function that will return the message. If a function is provided,
364 * it will only be invoked if this logger's
365 * {@linkplain #getEffectiveLevel() effective log level} includes the given
366 * `level`.
367 *
368 * @param {!Level} level the level at which to log the message.
369 * @param {(string|function(): string)} loggable the message to log, or a
370 * function that will return the message.
371 */
372 log(level, loggable) {
373 if (!this.isLoggable(level)) {
374 return;
375 }
376 let message = '[' + this.name_ + '] '
377 + (typeof loggable === 'function' ? loggable() : loggable);
378 let entry = new Entry(level, message, Date.now());
379 for (let logger = this; !!logger; logger = logger.parent_) {
380 if (logger.handlers_) {
381 for (let handler of logger.handlers_) {
382 handler(entry);
383 }
384 }
385 }
386 }
387
388 /**
389 * Logs a message at the {@link Level.SEVERE} log level.
390 * @param {(string|function(): string)} loggable the message to log, or a
391 * function that will return the message.
392 */
393 severe(loggable) {
394 this.log(Level.SEVERE, loggable);
395 }
396
397 /**
398 * Logs a message at the {@link Level.WARNING} log level.
399 * @param {(string|function(): string)} loggable the message to log, or a
400 * function that will return the message.
401 */
402 warning(loggable) {
403 this.log(Level.WARNING, loggable);
404 }
405
406 /**
407 * Logs a message at the {@link Level.INFO} log level.
408 * @param {(string|function(): string)} loggable the message to log, or a
409 * function that will return the message.
410 */
411 info(loggable) {
412 this.log(Level.INFO, loggable);
413 }
414
415 /**
416 * Logs a message at the {@link Level.DEBUG} log level.
417 * @param {(string|function(): string)} loggable the message to log, or a
418 * function that will return the message.
419 */
420 debug(loggable) {
421 this.log(Level.DEBUG, loggable);
422 }
423
424 /**
425 * Logs a message at the {@link Level.FINE} log level.
426 * @param {(string|function(): string)} loggable the message to log, or a
427 * function that will return the message.
428 */
429 fine(loggable) {
430 this.log(Level.FINE, loggable);
431 }
432
433 /**
434 * Logs a message at the {@link Level.FINER} log level.
435 * @param {(string|function(): string)} loggable the message to log, or a
436 * function that will return the message.
437 */
438 finer(loggable) {
439 this.log(Level.FINER, loggable);
440 }
441
442 /**
443 * Logs a message at the {@link Level.FINEST} log level.
444 * @param {(string|function(): string)} loggable the message to log, or a
445 * function that will return the message.
446 */
447 finest(loggable) {
448 this.log(Level.FINEST, loggable);
449 }
450}
451
452
453/**
454 * Maintains a collection of loggers.
455 *
456 * @final
457 */
458class LogManager {
459 constructor() {
460 /** @private {!Map<string, !Logger>} */
461 this.loggers_ = new Map;
462 this.root_ = new Logger('', Level.OFF);
463 }
464
465 /**
466 * Retrieves a named logger, creating it in the process. This function will
467 * implicitly create the requested logger, and any of its parents, if they
468 * do not yet exist.
469 *
470 * @param {string} name the logger's name.
471 * @return {!Logger} the requested logger.
472 */
473 getLogger(name) {
474 if (!name) {
475 return this.root_;
476 }
477 let parent = this.root_;
478 for (let i = name.indexOf('.'); i != -1; i = name.indexOf('.', i + 1)) {
479 let parentName = name.substr(0, i);
480 parent = this.createLogger_(parentName, parent);
481 }
482 return this.createLogger_(name, parent);
483 }
484
485 /**
486 * Creates a new logger.
487 *
488 * @param {string} name the logger's name.
489 * @param {!Logger} parent the logger's parent.
490 * @return {!Logger} the new logger.
491 * @private
492 */
493 createLogger_(name, parent) {
494 if (this.loggers_.has(name)) {
495 return /** @type {!Logger} */(this.loggers_.get(name));
496 }
497 let logger = new Logger(name, null);
498 logger.parent_ = parent;
499 this.loggers_.set(name, logger);
500 return logger;
501 }
502}
503
504
505const logManager = new LogManager;
506
507
508/**
509 * Retrieves a named logger, creating it in the process. This function will
510 * implicitly create the requested logger, and any of its parents, if they
511 * do not yet exist.
512 *
513 * The log level will be unspecified for newly created loggers. Use
514 * {@link Logger#setLevel(level)} to explicitly set a level.
515 *
516 * @param {string} name the logger's name.
517 * @return {!Logger} the requested logger.
518 */
519function getLogger(name) {
520 return logManager.getLogger(name);
521}
522
523
524/**
525 * Pads a number to ensure it has a minimum of two digits.
526 *
527 * @param {number} n the number to be padded.
528 * @return {string} the padded number.
529 */
530function pad(n) {
531 if (n >= 10) {
532 return '' + n;
533 } else {
534 return '0' + n;
535 }
536}
537
538
539/**
540 * Logs all messages to the Console API.
541 * @param {!Entry} entry the entry to log.
542 */
543function consoleHandler(entry) {
544 if (typeof console === 'undefined' || !console) {
545 return;
546 }
547
548 var timestamp = new Date(entry.timestamp);
549 var msg =
550 '[' + timestamp.getUTCFullYear() + '-' +
551 pad(timestamp.getUTCMonth() + 1) + '-' +
552 pad(timestamp.getUTCDate()) + 'T' +
553 pad(timestamp.getUTCHours()) + ':' +
554 pad(timestamp.getUTCMinutes()) + ':' +
555 pad(timestamp.getUTCSeconds()) + 'Z] ' +
556 '[' + entry.level.name + '] ' +
557 entry.message;
558
559 var level = entry.level.value;
560 if (level >= Level.SEVERE.value) {
561 console.error(msg);
562 } else if (level >= Level.WARNING.value) {
563 console.warn(msg);
564 } else {
565 console.log(msg);
566 }
567}
568
569
570/**
571 * Adds the console handler to the given logger. The console handler will log
572 * all messages using the JavaScript Console API.
573 *
574 * @param {Logger=} opt_logger The logger to add the handler to; defaults
575 * to the root logger.
576 */
577function addConsoleHandler(opt_logger) {
578 let logger = opt_logger || logManager.root_;
579 logger.addHandler(consoleHandler);
580}
581
582
583/**
584 * Removes the console log handler from the given logger.
585 *
586 * @param {Logger=} opt_logger The logger to remove the handler from; defaults
587 * to the root logger.
588 * @see exports.addConsoleHandler
589 */
590function removeConsoleHandler(opt_logger) {
591 let logger = opt_logger || logManager.root_;
592 logger.removeHandler(consoleHandler);
593}
594
595
596/**
597 * Installs the console log handler on the root logger.
598 */
599function installConsoleHandler() {
600 addConsoleHandler(logManager.root_);
601}
602
603
604/**
605 * Common log types.
606 * @enum {string}
607 */
608const Type = {
609 /** Logs originating from the browser. */
610 BROWSER: 'browser',
611 /** Logs from a WebDriver client. */
612 CLIENT: 'client',
613 /** Logs from a WebDriver implementation. */
614 DRIVER: 'driver',
615 /** Logs related to performance. */
616 PERFORMANCE: 'performance',
617 /** Logs from the remote server. */
618 SERVER: 'server'
619};
620
621
622/**
623 * Describes the log preferences for a WebDriver session.
624 *
625 * @final
626 */
627class Preferences {
628 constructor() {
629 /** @private {!Map<string, !Level>} */
630 this.prefs_ = new Map;
631 }
632
633 /**
634 * Sets the desired logging level for a particular log type.
635 * @param {(string|Type)} type The log type.
636 * @param {(!Level|string|number)} level The desired log level.
637 * @throws {TypeError} if `type` is not a `string`.
638 */
639 setLevel(type, level) {
640 if (typeof type !== 'string') {
641 throw TypeError('specified log type is not a string: ' + typeof type);
642 }
643 this.prefs_.set(type, level instanceof Level ? level : getLevel(level));
644 }
645
646 /**
647 * Converts this instance to its JSON representation.
648 * @return {!Object<string, string>} The JSON representation of this set of
649 * preferences.
650 */
651 toJSON() {
652 let json = {};
653 for (let key of this.prefs_.keys()) {
654 json[key] = this.prefs_.get(key).name;
655 }
656 return json;
657 }
658}
659
660
661// PUBLIC API
662
663
664module.exports = {
665 Entry: Entry,
666 Level: Level,
667 LogManager: LogManager,
668 Logger: Logger,
669 Preferences: Preferences,
670 Type: Type,
671 addConsoleHandler: addConsoleHandler,
672 getLevel: getLevel,
673 getLogger: getLogger,
674 installConsoleHandler: installConsoleHandler,
675 removeConsoleHandler: removeConsoleHandler
676};