Skip to content

Loglevel 2.0 #119

@kutuluk

Description

@kutuluk

Loglevel is good. It's time to make it fresh. More clearly. More modern.

  • Remove support for legacy browsers.
  • Remove support for persistance levels.
  • Remove noConflict()
  • Сustomizable levels
  • Flat flow of plugins (only one extra line in the stacktrace for any number of plugins)
  • log.getLogger('child') => log('child')
  • getter/setter log.level
  • maybe something else...

Implementation loglevel.js

const noop = () => {};
const loggers = {};
const configs = {};
const chain = [];
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'silent'];
let levelsInfo = levels.slice();

class Plugin {
  constructor(name) {
    this.name = name;
  }

  // eslint-disable-next-line class-methods-use-this
  factory() {
    return () => {};
  }
};

// Build the best logging method possible for this env
// Wherever possible we want to bind, not wrap, to preserve stack traces
function defaultFactory(methodValue) {
  /* eslint-disable no-console */
  if (typeof console === 'undefined') return noop;

  const methodName = console[levels[methodValue]] ? levels[methodValue] : 'log';
  if (console[methodName]) return console[methodName].bind(console);

  return noop;
  /* eslint-enable no-console */
}

function pluginsFactory(methodValue, logger) {
  const methods = [];

  chain.forEach((plugin) => {
    const rootConfig = configs[plugin.name][''];
    const loggerConfig = configs[plugin.name][logger.title];
    if (rootConfig || loggerConfig) {
      methods.push(
        plugin.factory(
          methodValue,
          logger,
          Object.assign({}, plugin.defaults, rootConfig, loggerConfig),
        ),
      );
    }
  });

  const native = defaultFactory(methodValue);

  return (...args) => {
    for (let i = 0; i < methods.length; i++) {
      methods[i](args);
    }
    native(...args);
  };
}

let factory = defaultFactory;

function rebuildMethods(logger) {
  for (let i = 0; i < levels.length - 1; i++) {
    logger[levels[i]] = i < logger.level ? noop : factory(i, logger);
  }
}

function removeMethods(logger) {
  for (let i = 0; i < levels.length - 1; i++) {
    delete logger[levels[i]];
  }
}

function Logger(logName, logLevel) {
  const logger = this || {};

  const defineProperty = Object.defineProperty;

  defineProperty(logger, 'title', {
    get() {
      return logName;
    },
  });

  defineProperty(logger, 'level', {
    get() {
      return logLevel;
    },
    set(lvl) {
      let newLevel = lvl;

      if (typeof newLevel === 'string') {
        newLevel = levels.indexOf(newLevel.toLowerCase());
      }

      if (typeof newLevel === 'number' && newLevel >= 0 && newLevel < levels.length) {
        logLevel = newLevel;
        rebuildMethods(logger);
      } else {
        throw new Error(`Invalid level: ${lvl}`);
      }
    },
  });

  defineProperty(logger, 'levels', {
    get() {
      return levelsInfo;
    },
  });

  logger.use = function (plugin, config) {
    // if (
    //  typeof plugin === 'object' &&
    //  typeof plugin.name === 'string' &&
    //  typeof plugin.factory === 'function'
    // ) {
    if (plugin instanceof Plugin) {
      const pluginName = plugin.name;
      if (!configs[pluginName]) {
        // lazy plugging
        configs[pluginName] = {};
        chain.push(plugin);
        factory = pluginsFactory;
      }

      plugin = pluginName;
    }

    if (typeof plugin !== 'string' || !configs[plugin]) {
      throw new Error(`Invalid plugin: ${plugin}`);
    }

    configs[plugin][logName] = config || {};
    rebuildMethods(logger);
  };

  logger.level = logLevel;
  loggers[logName] = logger;

  return logger;
}

function log(name, level) {
  name = name || '';
  if (typeof name !== 'string') {
    throw new TypeError(`Invalid name: ${name}`);
  }

  return loggers[name] || new Logger(name, level || log.level);
}

Logger.call(log, '', 3);

log.Plugin = Plugin;

log.config = (newLevels, newLevel) => {
  Object.keys(loggers).forEach(logger => removeMethods(loggers[logger]));

  levels = newLevels;
  levelsInfo = levels.slice();

  Object.keys(loggers).forEach((logger) => {
    loggers[logger].level = newLevel;
  });
};

export default log;

Example

const log = require('loglevel');

class Prefixer extends log.Plugin {
  constructor() {
    super('prefixer');

    this.defaults = {
      text: 'default prefix',
    };
    this.prevs = {};
  }

  factory(method, logger, config) {
    return (args) => {
      const timestamp = Date.now();
      const prev = this.prevs[logger.title];
      const delta = prev ? timestamp - prev : 0;
      this.prevs[logger.title] = timestamp;
      args.unshift(
        `[+${delta}ms] ${logger.levels[method].toUpperCase()} (${logger.title}) "${config.text}":`,
      );
    };
  }
}

log.level = 'trace';

(function StackTraceTest() {
  log.trace();
}());

const child = log('child');
child.info('child');

const prefixer = new Prefixer();

child.use(prefixer, { text: 'custom prefix:' });

log.info('root');

child.info('child');

let sum = 0;
for (let i = 0; i < 1000000; i++) {
  sum += Math.log(0.1);
}

log.use('prefixer');

log.info('root');

child.info(sum);

log.config(['verbose', 'trace', 'critical', 'silent'], 'trace');

log.critical('critical');

child.verbose('verbose1');

child.level = 'verbose';
child.verbose('verbose2');

(function StackTraceTest() {
  log.trace();
  child.trace();
}());

Output

C:\Users\u36\Dropbox\kutuluk\logler>node ./examples/example
Trace
    at StackTraceTest (C:\Users\u36\Dropbox\kutuluk\logler\examples\example.js:29:7)
    ...
child
root
[+0ms] INFO (child) "custom prefix:": child
[+0ms] INFO () "default prefix": root
[+27ms] INFO (child) "custom prefix:": -2302585.0930085
[+1ms] CRITICAL () "default prefix": critical
[+1ms] VERBOSE (child) "custom prefix:": verbose2
Trace: [+0ms] TRACE () "default prefix":
    at Function.trace (C:\Users\u36\Dropbox\kutuluk\logler\lib\logger.js:105:14)
    at StackTraceTest (C:\Users\u36\Dropbox\kutuluk\logler\examples\example.js:64:7)
    ...
Trace: [+2ms] TRACE (child) "custom prefix:":
    at Logger.trace (C:\Users\u36\Dropbox\kutuluk\logler\lib\logger.js:105:14)
    at StackTraceTest (C:\Users\u36\Dropbox\kutuluk\logler\examples\example.js:65:9)
    ...

If this fits into the development concept of loglevel, I will make a pull request

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions