Skip to content

Commit

Permalink
feat: createLogger for easier logging in individual modules (#5418)
Browse files Browse the repository at this point in the history
Create a new createLogger module for better debugging. Each logger has its own log level and its own createLogger that will nest logs underneath them. `player.log` is also included, which logs the player id as part of the log statement. The history API also got a filter method.

For example:
```js
var log = videojs.log.createLogger('foo');
log('hello');
// > VIDEOJS: foo: hello
```
  • Loading branch information
gkatsev committed Sep 28, 2018
1 parent ebf8d66 commit 966eb56
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 232 deletions.
68 changes: 56 additions & 12 deletions docs/guides/debugging.md
Expand Up @@ -6,9 +6,12 @@
* [API Overview](#api-overview)
* [Log Safely](#log-safely)
* [Log Objects Usefully](#log-objects-usefully)
* [Creating new Loggers](#creating-new-loggers)
* [Log Levels](#log-levels)
* [Available Log Levels](#available-log-levels)
* [Debug Logging](#debug-logging)
* [History](#history)
* [History filtering](#history-filtering)

## Logging

Expand All @@ -18,17 +21,19 @@ Video.js includes `videojs.log`, a lightweight wrapper around a subset of [the `

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, debug, info |
| `videojs.log.debug()` | `console.debug` | all, debug |
| `videojs.log.warn()` | `console.warn` | all, debug, info, warn |
| `videojs.log.error()` | `console.error` | all, debug, info, 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 |
| Method | Alias Of | Matching Level(s) |
| ------------------------------- | --------------- | ----------------------------- |
| `videojs.log()` | `console.log` | all, debug, info |
| `videojs.log.debug()` | `console.debug` | all, debug |
| `videojs.log.warn()` | `console.warn` | all, debug, info, warn |
| `videojs.log.error()` | `console.error` | all, debug, info, warn, error |
| `videojs.log.createLogger()` | n/a | n/a |
| `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 |
| `videojs.log.history.filter()` | n/a | n/a |

For descriptions of these features, please refer to the sections below.

Expand All @@ -44,6 +49,23 @@ Similar to the `console`, any number of mixed-type values can be passed to `vide
videojs.log('this is a string', {butThis: 'is an object'});
```

### Creating new Loggers

Sometimes, you want to make a new module or plugin and log messages with a label. Kind of how all these logs are prepended with `VIDEOJS:`. You can do that via the `createLogger` method. It takes a name and gives you back a log object like `videojs.log`. Here's an example:

```js
const mylogger = videojs.log.createLogger('mylogger');

mylogger('hello world!');
// > VIDEOJS: mylogger: hello world!

// We can even chain it further
const anotherlogger = mylogger.createLogger('anotherlogger');

anotherlogger('well, hello there');
// > VIDEOJS: mylogger: anotherlogger: well, hello there
```

### Log Levels

Unlike the `console`, `videojs.log` includes the concept of logging levels. These levels toggle logging methods on or off.
Expand Down Expand Up @@ -78,7 +100,7 @@ Although the log levels attempt to match their `window.console` counterparts, `w

### 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.
> **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. You can also filter the history based on the name of the logger.
By default, the `videojs.log` module tracks a history of _everything_ passed to it regardless of logging level:

Expand All @@ -102,6 +124,28 @@ Finally, the history (if enabled) can be cleared at any time via:
videojs.log.history.clear();
```

#### History filtering

If you want to find all the history that was created by a particular logger, you can do so via `history.filter()`.
Given a specific logger with name `foo`, you can pass `foo` to `history.filter()` and get all items logger by foo.
Let me show you an example:

```js
const mylogger = videojs.log.createLogger('mylogger');
const anotherlogger = mylogger.createLogger('anotherlogger');

videojs.log('hello');
mylogger('how are you');
anotherlogger('today');

videojs.log.history.filter('VIDEOJS');
// > [['VIDEOJS:', 'hello'], ['VIDEOJS: mylogger:', 'how are you'], ['VIDEOJS: mylogger: anotherlogger:', 'today']]
videojs.log.history.filter('mylogger');
// > [['VIDEOJS: mylogger:', 'how are you'], ['VIDEOJS: mylogger: anotherlogger:', 'today']]
videojs.log.history.filter('anotherlogger');
// > [['VIDEOJS: mylogger: anotherlogger:', 'today']]
```

[api]: https://docs.videojs.com/

[console]: https://developer.mozilla.org/en-US/docs/Web/API/Console
5 changes: 4 additions & 1 deletion src/js/player.js
Expand Up @@ -14,7 +14,7 @@ import * as Dom from './utils/dom.js';
import * as Fn from './utils/fn.js';
import * as Guid from './utils/guid.js';
import * as browser from './utils/browser.js';
import log from './utils/log.js';
import log, { createLogger } from './utils/log.js';
import toTitleCase, { titleCaseEquals } from './utils/to-title-case.js';
import { createTimeRange } from './utils/time-ranges.js';
import { bufferedPercent } from './utils/buffer.js';
Expand Down Expand Up @@ -313,6 +313,9 @@ class Player extends Component {
// Run base component initializing with new options
super(null, options, ready);

// create logger
this.log = createLogger(this.id_);

// Tracks when a tech changes the poster
this.isPosterFromTech_ = false;

Expand Down
247 changes: 247 additions & 0 deletions src/js/utils/create-logger.js
@@ -0,0 +1,247 @@
/**
* @file create-logger.js
* @module create-logger
*/
import window from 'global/window';

// 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.
*
* @param {Array} args
* The arguments to be passed to the matching console method.
*/
const LogByTypeFactory = (name, log) => (type, level, args) => {
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".
args.unshift(type.toUpperCase() + ':');
}

// Add console prefix after adding to history.
args.unshift(name + ':');

// Add a clone of the args at this point to history.
if (history) {
history.push([].concat(args));
}

// If there's no console then don't try to output messages, but they will
// still be stored in history.
if (!window.console) {
return;
}

// 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.
let fn = window.console[type];

if (!fn && type === 'debug') {
// Certain browsers don't have support for console.debug. For those, we
// should default to the closest comparable log.
fn = window.console.info || window.console.log;
}

// 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;
}

fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args);
};

export default function createLogger(name) {
// This is the private tracking variable for logging level.
let level = 'info';

// the curried logByType bound to the specific log and history
let logByType;

/**
* Logs plain debug messages. Similar to `console.log`.
*
* Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
* of our JSDoc template, we cannot properly document this as both a function
* and a namespace, so its function signature is documented here.
*
* #### Arguments
* ##### *args
* Mixed[]
*
* Any combination of values that could be passed to `console.log()`.
*
* #### Return Value
*
* `undefined`
*
* @namespace
* @param {Mixed[]} args
* One or more messages or objects that should be logged.
*/
const log = function(...args) {
logByType('log', level, args);
};

// This is the logByType helper that the logging methods below use
logByType = LogByTypeFactory(name, log);

/**
* Create a new sublogger which chains the old name to the new name.
*
* For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following:
* ```js
* mylogger('foo');
* // > VIDEOJS: player: foo
* ```
*
* @param {string} name
* The name to add call the new logger
* @return {Object}
*/
log.createLogger = (subname) => createLogger(name + ': ' + subname);

/**
* 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`: Matches only Video.js-provided functions (`debug`, `log`,
* `log.warn`, and `log.error`).
* - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
* - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
* - `warn`: Matches `log.warn` and `log.error` calls.
* - `error`: Matches only `log.error` calls.
*
* @type {Object}
*/
log.levels = {
all: 'debug|log|warn|error',
off: '',
debug: 'debug|log|warn|error',
info: 'log|warn|error',
warn: 'warn|error',
error: 'error',
DEFAULT: level
};

/**
* Get or set the current logging level.
*
* If a string matching a key from {@link module:log.levels} is provided, acts
* as a setter.
*
* @param {string} [lvl]
* Pass a valid level 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.
*
* @return {Array}
*/
log.history = () => history ? [].concat(history) : [];

/**
* Allows you to filter the history by the given logger name
*
* @param {string} fname
* The name to filter by
*
* @return {Array}
* The filtered list to return
*/
log.history.filter = (fname) => {
return (history || []).filter((historyItem) => {
// if the first item in each historyItem includes `fname`, then it's a match
return new RegExp(`.*${fname}.*`).test(historyItem[0]);
});
};

/**
* Clears the internal history tracking, but does not prevent further history
* tracking.
*/
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 = [];
}
};

/**
* Logs error messages. Similar to `console.error`.
*
* @param {Mixed[]} args
* One or more messages or objects that should be logged as an error
*/
log.error = (...args) => logByType('error', level, args);

/**
* Logs warning messages. Similar to `console.warn`.
*
* @param {Mixed[]} args
* One or more messages or objects that should be logged as a warning.
*/
log.warn = (...args) => logByType('warn', level, args);

/**
* Logs debug messages. Similar to `console.debug`, but may also act as a comparable
* log if `console.debug` is not available
*
* @param {Mixed[]} args
* One or more messages or objects that should be logged as debug.
*/
log.debug = (...args) => logByType('debug', level, args);

return log;
}

0 comments on commit 966eb56

Please sign in to comment.