From 844e4f0107b2d7ee2f392cfb9aaa0ae71a607b3e Mon Sep 17 00:00:00 2001 From: Pat O'Neill Date: Wed, 18 Jan 2017 00:35:42 -0500 Subject: [PATCH] feat: Log Levels (#3853) Add a log levels and history api: `videojs.log.level()` and `videojs.log.history()`. `.level()` will return the current level and you can also set it to be one of: `all`, `error`, `off`, or `warn`. `.history()` will return a list of all things logged since videojs loaded. It can be disabled via `videojs.log.history.disable()` (and re-enabled with `enable()`) as well as cleared with `videojs.log.history.clear()`. --- docs/faq.md | 13 ++++ docs/guides/debugging.md | 112 +++++++++++++++++++++++++++++++++ src/js/utils/log.js | 122 +++++++++++++++++++++++++++++++----- test/unit/utils/log.test.js | 84 +++++++++++++++++++++++-- 4 files changed, 312 insertions(+), 19 deletions(-) create mode 100644 docs/guides/debugging.md diff --git a/docs/faq.md b/docs/faq.md index 2e52a8933c..ddf6941a4e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -19,6 +19,7 @@ * [Q: How can I autoplay a video on a mobile device?](#q-how-can-i-autoplay-a-video-on-a-mobile-device) * [Q: How can I play RTMP video in video.js?](#q-how-can-i-play-rtmp-video-in-videojs) * [Q: How can I hide the links to my video/subtitles/audio/tracks?](#q-how-can-i-hide-the-links-to-my-videosubtitlesaudiotracks) +* [Q: Can I turn off video.js logging?](#q-can-i-turn-off-videojs-logging) * [Q: What is a plugin?](#q-what-is-a-plugin) * [Q: How do I make a plugin for video.js?](#q-how-do-i-make-a-plugin-for-videojs) * [Q: Where can I find a list of video.js plugins?](#q-where-can-i-find-a-list-of-videojs-plugins) @@ -169,6 +170,16 @@ help but are outside of the scope of video.js. For content that must be highly secure [videojs-contrib-eme][eme] adds DRM support. +## Q: Can I turn off video.js logging? + +Yes! This can be achieved by adding the following code _after_ including Video.js, but _before_ creating any player(s): + +```js +videojs.log.level('off'); +``` + +For more information, including which logging levels are available, check out the [debugging guide][debug-guide]. + ## Q: What is a plugin? A plugin is a group of reusable functionality that can be re-used by others. For instance a plugin could add @@ -307,3 +318,5 @@ Yes! Please [submit an issue or open a pull request][pr-issue-question] if this [semver]: http://semver.org/ [starter-example]: http://jsbin.com/axedog/edit?html,output + +[debug-guide]: ./guides/debug.md diff --git a/docs/guides/debugging.md b/docs/guides/debugging.md new file mode 100644 index 0000000000..d3206a0c8f --- /dev/null +++ b/docs/guides/debugging.md @@ -0,0 +1,112 @@ +# Debugging + +## Table of Contents + +* [Logging](#logging) + * [API Overview](#api-overview) + * [Log Safely](#log-safely) + * [Log Objects Usefully](#log-objects-usefully) + * [Log Levels](#log-levels) + * [Available Log Levels](#available-log-levels) + * [History](#history) + +## Logging + +Video.js includes a lightweight wrapper - `videojs.log` - around a subset of [the `console` API][console]. The available methods are `videojs.log`, `videojs.log.warn`, and `videojs.log.error`. + +### API Overview + +Most of these methods should be fairly self-explanatory, but for complete details, see [the API docs][api]. + +| Method | Alias Of | Matching Level(s) | +| ------------------------------- | --------------- | ----------------- | +| `videojs.log()` | `console.log` | all | +| `videojs.log.warn()` | `console.warn` | all, warn | +| `videojs.log.error()` | `console.error` | all, warn, error | +| `videojs.log.level()` | n/a | n/a | +| `videojs.log.history()` | n/a | n/a | +| `videojs.log.history.clear()` | n/a | n/a | +| `videojs.log.history.disable()` | n/a | n/a | +| `videojs.log.history.enable()` | n/a | n/a | + +For descriptions of these features, please refer to the sections below. + +### Log Safely + +Unlike the `console`, it's safe to leave `videojs.log` calls in your code. They won't throw errors when the `console` doesn't exist. + +### Log Objects Usefully + +Similar to the `console`, any number of mixed-type values can be passed to `videojs.log` methods: + +```js +videojs.log('this is a string', {butThis: 'is an object'}); +``` + +However, certain browser consoles (namely, IE10 and lower) do not support non-string values. Video.js improves on this situation by passing objects through `JSON.stringify` before logging them in IE10 and below. In other words, instead of the above producing this: + +```txt +VIDEOJS: this is a string [object Object] +``` + +it will produce this: + +```txt +VIDEOJS: this is a string {"butThis": "is an object"} +``` + +### Log Levels + +Unlike the `console`, `videojs.log` includes the concept of logging levels. These levels toggle logging methods on or off. + +Levels are exposed through the `videojs.log.level` method. This method acts as both a getter and setter for the current logging level. With no arguments, it returns the current logging level: + +```js +videojs.log.level(); // "all" +``` + +By passing a string, the logging level can be changed to one of the available logging levels: + +```js +videojs.log.level('error'); // show only error messages and suppress others +videojs.log('foo'); // does nothing +videojs.log.warn('foo'); // does nothing +videojs.log.error('foo'); // logs "foo" as an error +``` + +### Available Log Levels + +* **all** (default): enables all logging methods +* **error**: only show `log.error` messages +* **off**: disable all logging methods +* **warn**: only show `log.warn` _and_ `log.error` messages + +### History + +> **Note:** In Video.js 5, `videojs.log.history` was an array. As of Video.js 6, it is a function which returns an array. This change was made to provide a richer, safer logging history API. + +By default, the `videojs.log` module tracks a history of _everything_ passed to it regardless of logging level: + +```js +videojs.log.history(); // an array of everything that's been logged up to now +``` + +This will work even when logging is set to **off**. + +This can be useful, but it can also be a source of memory leaks. For example, logged objects will be retained in history even if references are removed everywhere else! + +To avoid this problem, history can be disabled or enabled via method calls (using the `disable` and `enable` methods respectively). Disabling history is as easy as: + +```js +videojs.log.history.disable(); +``` + +Finally, the history (if enabled) can be cleared at any time via: + +```js +videojs.log.history.clear(); +``` + +[api]: http://docs.videojs.com/docs/api/index.html + +[console]: https://developer.mozilla.org/en-US/docs/Web/API/Console diff --git a/src/js/utils/log.js b/src/js/utils/log.js index 4667439c20..4d4139880b 100644 --- a/src/js/utils/log.js +++ b/src/js/utils/log.js @@ -8,9 +8,16 @@ import {isObject} from './obj'; let log; +// This is the private tracking variable for logging level. +let level = 'all'; + +// This is the private tracking variable for the logging history. +let history = []; + /** * Log messages to the console and history based on the type of message * + * @private * @param {string} type * The name of the console method to use. * @@ -22,29 +29,34 @@ let log; * but this is exposed as a parameter to facilitate testing. */ export const logByType = (type, args, stringify = !!IE_VERSION && IE_VERSION < 11) => { + const lvl = log.levels[level]; + const lvlRegExp = new RegExp(`^(${lvl})$`); if (type !== 'log') { - // add the type to the front of the message when it's not "log" + // Add the type to the front of the message when it's not "log". args.unshift(type.toUpperCase() + ':'); } - // add to history - log.history.push(args); + // Add a clone of the args at this point to history. + if (history) { + history.push([].concat(args)); + } - // add console prefix after adding to history + // Add console prefix after adding to history. args.unshift('VIDEOJS:'); // If there's no console then don't try to output messages, but they will - // still be stored in `log.history`. + // still be stored in history. // // Was setting these once outside of this function, but containing them // in the function makes it easier to test cases where console doesn't exist // when the module is executed. const fn = window.console && window.console[type]; - // Bail out if there's no console. - if (!fn) { + // Bail out if there's no console or if this type is not allowed by the + // current logging level. + if (!fn || !lvl || !lvlRegExp.test(type)) { return; } @@ -76,24 +88,104 @@ export const logByType = (type, args, stringify = !!IE_VERSION && IE_VERSION < 1 }; /** - * Log plain debug messages + * Logs plain debug messages. Similar to `console.log`. * - * @param {Mixed[]} args - * One or more messages or objects that should be logged. + * @class + * @param {Mixed[]} args + * One or more messages or objects that should be logged. */ log = function(...args) { logByType('log', args); }; /** - * Keep a history of log messages + * Enumeration of available logging levels, where the keys are the level names + * and the values are `|`-separated strings containing logging methods allowed + * in that logging level. These strings are used to create a regular expression + * matching the function name being called. + * + * Levels provided by video.js are: + * + * - `off`: Matches no calls. Any value that can be cast to `false` will have + * this effect. The most restrictive. + * - `all` (default): Matches only Video.js-provided functions (`log`, + * `log.warn`, and `log.error`). + * - `warn`: Matches `log.warn` and `log.error` calls. + * - `error`: Matches only `log.error` calls. + * + * @type {Object} + */ +log.levels = { + all: 'log|warn|error', + error: 'error', + off: '', + warn: 'warn|error', + DEFAULT: level +}; + +/** + * Get or set the current logging level. If a string matching a key from + * {@link log.levels} is provided, acts as a setter. Regardless of argument, + * returns the current logging level. + * + * @param {string} [lvl] + * Pass to set a new logging level. + * + * @return {string} + * The current logging level. + */ +log.level = (lvl) => { + if (typeof lvl === 'string') { + if (!log.levels.hasOwnProperty(lvl)) { + throw new Error(`"${lvl}" in not a valid log level`); + } + level = lvl; + } + return level; +}; + +/** + * Returns an array containing everything that has been logged to the history. + * + * This array is a shallow clone of the internal history record. However, its + * contents are _not_ cloned; so, mutating objects inside this array will + * mutate them in history. * - * @type {Array} + * @return {Array} + */ +log.history = () => history ? [].concat(history) : []; + +/** + * Clears the internal history tracking, but does not prevent further history + * tracking. */ -log.history = []; +log.history.clear = () => { + if (history) { + history.length = 0; + } +}; + +/** + * Disable history tracking if it is currently enabled. + */ +log.history.disable = () => { + if (history !== null) { + history.length = 0; + history = null; + } +}; + +/** + * Enable history tracking if it is currently disabled. + */ +log.history.enable = () => { + if (history === null) { + history = []; + } +}; /** - * Log error messages + * Logs error messages. Similar to `console.error`. * * @param {Mixed[]} args * One or more messages or objects that should be logged as an error @@ -101,7 +193,7 @@ log.history = []; log.error = (...args) => logByType('error', args); /** - * Log warning messages + * Logs warning messages. Similar to `console.warn`. * * @param {Mixed[]} args * One or more messages or objects that should be logged as a warning. diff --git a/test/unit/utils/log.test.js b/test/unit/utils/log.test.js index 5ae4d1b3df..0848fa8581 100644 --- a/test/unit/utils/log.test.js +++ b/test/unit/utils/log.test.js @@ -5,7 +5,7 @@ import {logByType} from '../../../src/js/utils/log.js'; import window from 'global/window'; import sinon from 'sinon'; -QUnit.module('log', { +QUnit.module('utils/log', { beforeEach() { @@ -29,8 +29,11 @@ QUnit.module('log', { // Restore the native/original console. window.console = this.originalConsole; + // Restore the default logging level. + log.level(log.levels.DEFAULT); + // Empty the logger's history. - log.history.length = 0; + log.history.clear(); } }); @@ -41,7 +44,7 @@ QUnit.test('logging functions should work', function(assert) { // Need to reset history here because there are extra messages logged // when running via Karma. - log.history.length = 0; + log.history.clear(); log('log1', 'log2'); log.warn('warn1', 'warn2'); @@ -65,11 +68,20 @@ QUnit.test('logging functions should work', function(assert) { getConsoleArgs('VIDEOJS:', 'ERROR:', 'error1', 'error2') ); - assert.equal(log.history.length, 3, 'there should be three messages in the log history'); + const history = log.history(); + + assert.equal(history.length, 3, 'there should be three messages in the log history'); + assert.deepEqual(history[0], ['log1', 'log2'], 'history recorded the correct arguments'); + assert.deepEqual(history[1], ['WARN:', 'warn1', 'warn2'], 'history recorded the correct arguments'); + assert.deepEqual(history[2], ['ERROR:', 'error1', 'error2'], 'history recorded the correct arguments'); }); QUnit.test('in IE pre-11 (or when requested) objects and arrays are stringified', function(assert) { + // Need to reset history here because there are extra messages logged + // when running via Karma. + log.history.clear(); + // Run a custom log call, explicitly requesting object/array stringification. logByType('log', [ 'test', @@ -84,3 +96,67 @@ QUnit.test('in IE pre-11 (or when requested) objects and arrays are stringified' assert.deepEqual(window.console.log.firstCall.args, ['VIDEOJS: test {"foo":"bar"} [1,2,3] 0 false null']); }); + +QUnit.test('setting the log level changes what is actually logged', function(assert) { + + // Need to reset history here because there are extra messages logged + // when running via Karma. + log.history.clear(); + + log.level('error'); + + log('log1', 'log2'); + log.warn('warn1', 'warn2'); + log.error('error1', 'error2'); + + assert.notOk(window.console.log.called, 'console.log was not called'); + assert.notOk(window.console.warn.called, 'console.warn was not called'); + assert.ok(window.console.error.called, 'console.error was called'); + + const history = log.history(); + + assert.deepEqual(history[0], ['log1', 'log2'], 'history is maintained even when logging is not performed'); + assert.deepEqual(history[1], ['WARN:', 'warn1', 'warn2'], 'history is maintained even when logging is not performed'); + assert.deepEqual(history[2], ['ERROR:', 'error1', 'error2'], 'history is maintained even when logging is not performed'); + + log.level('off'); + + log('log1', 'log2'); + log.warn('warn1', 'warn2'); + log.error('error1', 'error2'); + + assert.notOk(window.console.log.called, 'console.log was not called'); + assert.notOk(window.console.warn.called, 'console.warn was not called'); + assert.strictEqual(window.console.error.callCount, 1, 'console.error was not called again'); + + assert.throws( + () => log.level('foobar'), + new Error('"foobar" in not a valid log level'), + 'log.level() only accepts valid log levels when used as a setter' + ); +}); + +QUnit.test('history can be enabled/disabled', function(assert) { + + // Need to reset history here because there are extra messages logged + // when running via Karma. + log.history.clear(); + + log.history.disable(); + log('log1'); + log.warn('warn1'); + log.error('error1'); + + let history = log.history(); + + assert.strictEqual(history.length, 0, 'no history was tracked'); + + log.history.enable(); + log('log1'); + log.warn('warn1'); + log.error('error1'); + + history = log.history(); + + assert.strictEqual(history.length, 3, 'history was tracked'); +});