Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loglevel 2.0 #119

Open
kutuluk opened this issue Jan 31, 2018 · 21 comments
Open

Loglevel 2.0 #119

kutuluk opened this issue Jan 31, 2018 · 21 comments

Comments

@kutuluk
Copy link
Contributor

kutuluk commented Jan 31, 2018

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

@pimterry
Copy link
Owner

pimterry commented Feb 4, 2018

This is really great.

I've got some specific thoughts below, but generally I'm very keen on this, there's a lot of nice improvements here that we can make to iterate on feedback and improve the UX longterm.

There's a couple of places where I'm not totally sold, but most of this sounds superb. The main thing I'm trying to do is ensure that loglevel continues to work out of the box for as many people as possible, even in awkward environments, not just for people keeping up to date with the nicest modern stack.

On your list above:

  • Remove support for legacy browsers.
    • Open to this, but we should be explicit about what that means. I'd be ok with dropping support for browsers with < 1% web traffic right now though. That still leaves IE 11 (~3% of web traffic in Jan 2018), but rules out the rest of IE, and a lot of the other older browsers. I'm pulling stats from here: http://gs.statcounter.com/browser-version-market-share
  • Remove support for persistance levels.
    • I find persistence quite useful. Without it, you can't change the log level used at page startup without rewriting the source of the page itself. I'm open to disabling it by default or something though.
    • We could disable persistence by default, and create a .persist() method instead, which persists the current level.
    • In fact, that could even be a plugin actually (add .persist(), load current level on plugin registratoin), maybe that's the right way to go, and we just need to ensure that's practical...
  • Remove noConflict()
    • Why? There's definitely (sadly) still a meaningful proportion of web development that happens without proper module systems (42% of devs use browserify/webpack, 10% use RequireJS, and 32% don't do any bundling whatsoever, according to the https://www.sitepoint.com/front-end-tooling-trends-2017, this time last year), and if you're using a global then noConflict can be pretty important. Easy to implement, and doesn't hurt anybody else afaict.
    • Generally I'm quite keen to ensure loglevel is useful for as many JS developers as possible, so while there's still a contingent using bare JS & it's easy, it'd be nice to support them. It's useful to let you quickly add nice logging to tiny scripts in HTML pages or into sites like jsbin too, with zero setup required.
  • Сustomizable levels
    • Oooh, nice fix to a common debate, definitely!
    • Curious what the API for this looks like though - how do I define a new level, and map it to a console function?
  • Flat flow of plugins (only one extra line in the stacktrace for any number of plugins)
    • 100%, and we can pull in plugin API improvements en route too
  • log.getLogger('child') => log('child')
    • I don't have strong feelings about this. This is shorter, but getLogger feels clearer. Still, if you do then I'm happy to change it.
  • getter/setter log.level
    • Nice idea, definitely 👍
  • Maybe something else...
    • Built in Flow & TS type definitions would be nice (personally I'd probably just rewrite the source in TS, but up to you)
    • Support for some more console methods (like timing and group)
    • Don't any of that for 2.0.0, just as part of the v2 direction in future.

@kutuluk
Copy link
Contributor Author

kutuluk commented Feb 5, 2018

Speaking of version 2.0, I meant completely abandoning obsolete environments. Backwards compatibility could be saved with the help of the loglevel/legasy submodule. This is a common practice.

But I do not insist, because I understand that loglevel is widely used and breaking back compatibility is undesirable. Therefore, I realized my idea in the form of a package https://github.com/kutuluk/log-n-roll. This is a 900-byte logger with a built-in prefixer. I think this is ideal for modern applications such as SPA and PWA.

But I can say with confidence that all the same thing can be implemented in loglevel. The difference is only in size. So you just need to decide which changes will improve the loglevel and make them.

@karfau
Copy link

karfau commented Feb 7, 2018

Awesome work, and ideas I hope this won't grow into YAL (yet another logger).
I'm happy to contribute to any of this, and able to write TS.
One idea about compatibility:
I haven't totally wrapped my head around it, but maybe this is possible:
Have two kind of builds: one for "modern usage" as @kutuluk called it,
but available under loglevel/modern (since it is easy for newer methods to only require/import that part)
and the fully fledged legacy support still being available under loglevel.

I have't looked at the code base so far, but maybe the compatibility can also be added as a plugin?

@kutuluk
Copy link
Contributor Author

kutuluk commented Feb 8, 2018

Implementing the support of browsers from the Stone Age, you risk yourself stuck in the Stone Age. If this is your choice, then I'm not going to challenge it. But I want to live today and look into tomorrow, instead of hanging in yesterday.

My position is very strong - backward compatibility with browsers no lower than IE 11 (ES5). Ensuring compatibility below is not a library task - it's a task for https://github.com/es-shims/es5-shim.

@pimterry
Copy link
Owner

pimterry commented Feb 8, 2018

backward compatibility with browsers no lower than IE 11 (ES5)

Yep, that sounds good to me. As in my comment above, I think IE 11 is a good & low bar nowadays, and I'm fine with only publishing a 'modern' build that supports IE11+. That's still very inclusive (IE10 is now down to 0.1% of browser usage), and users who want support back even further than that can just stick with loglevel 1.x, which will keep working fine for all sorts of super old browsers, or look at shimming etc if they really need to. A legacy build would work, but I'd rather just point people back to 1.x and keep ongoing dev simple.

The main legacy bit that I would like to retain is UMD, for developers not using webpack and similar, since the stats seem to suggest that's still a significant percentage of developers. That's a cheap, easy and tiny addition though, and doesn't get involved in the core of the code - we just keep the UMD wrapper & noConflict. We could do that in a separate build if we really wanted to, but publishing etc is all quite a bit simpler if there's just a single copy that works everywhere.

@kutuluk
Copy link
Contributor Author

kutuluk commented Feb 8, 2018

I think IE 11 is a good & low bar nowadays, and I'm fine with only publishing a 'modern' build that supports IE11+.

It sounds delicious. My current implementation https://github.com/kutuluk/log-n-roll is fully operational in IE 11. With the help of https://github.com/developit/microbundle three ES5 builds are compiled: UMD (without noConflict), CommonJS and ESM.

My opinion about noConflict(): resolving name conflicts is not a library task, it's a programmer's task. The only thing that is needed from the library is to provide a permanent name, which is occupied in the global scope. One clause in the documentation. Nothing more.

I've already changed my original sketch and now it's almost finished. I propose to take it as a basis, thoroughly think through the API, implement some of the most popular plug-ins (in a simplified form I have already implemented something), test and merge into loglevel under version 2.0 without backwards compatibility. The current version of loglevel is published as loglevel@legacy and maintained separately.

@pimterry
Copy link
Owner

That all sounds great to me. I've opened a v2 branch on this repo, can you PR changes into there? That way we can do a series of PRs to reimplement the core, then update the docs etc, and make various other improvements I'd like around the project, and finally merge & release it all together once it's all ready.

I'm still on the fence about noConflict(). You're right that developers can work around it themselves, but it can be quite awkward, and it's a pretty strong convention. Anyway, we can get started regardless. PR whatever you're happy with to v2 when you're ready, and we can start with that and iterate from there.

@zmoshansky
Copy link

I'd second that it'd be nice to see persisting levels disabled by default (The current behaviour is confusing as we use default_level and if a log level is set, we can't override without explicitly enumerating loggers and calling setLevel.

Alternatively, provide a clear method~

 if (window.localStorage) {
      window.localStorage
      const isLogLevel = R.contains('loglevel');
      for (var key in window.localStorage){
        if (isLogLevel(key)) {
          window.localStorage.removeItem(key);
        }
      }
    }

@sellmic
Copy link

sellmic commented Apr 12, 2018

These ideas all sound great, from my end I don't think there's any must haves in the suggested v2 features. But I'm just evaluating the library at this point, so I'm probably missing a lot of context.

My interest is more on being able to add plugins to the log system in order to have flexibility on sending logs to a server, and it looks like the current version supports that with a few plugins already (or we could write our own).

What's the status of v2, doesn't look there's been much activity on that branch ...

@Mr0grog
Copy link
Contributor

Mr0grog commented Dec 5, 2024

@pimterry Do you still have any appetite for someone picking this work up? Last time this came up in January 2023 I did not have the time or energy for it (and it wasn’t clear whether you had time to really shepherd anybody’s work on it either). I might have some time for this later in December or January.

If you are interested, what are the important goals?

  • You’ve talked in a few places about rewriting in TypeScript, so I assume that’s a strong desire on your part. (It need not be a breaking v2-level change, though.)

  • I think it’s safe to drop IE (it’s been almost 7 years since the previous discussion on it here, and it’s effectively gone from the rankings), but what criteria do you want to apply for determining minimum support?

    I am deeply on-board with maintaining a broad and sometimes old support base (stability and long-term runtime support is one of the things I really appreciate about this library!), although v2 gives us a nice opportunity to use some newer JS features like class syntax, getters/setters, etc.

    I think Loglevel already works fine in Bun/Deno/QuickJS, but it would probably be nice to make it official. :)

  • ESM support has been around for long enough now that I think it is the best way to ship this library by default, but it might be useful to continue shipping a UMD build as well (I think this is fairly self-evident, but recent articles like Julia Evans’s “Importing a frontend Javascript library without a build system”, or the continuing issues around Node.js or Jest support for modules are also good indicators).

  • Rethinking plugin support seems like a big feature in this discussion already, and it certainly came up the cleanup work I did here a year ago. This would solve Improve the methodFactory API (stop exposing the field directly, and make it easier to use) #82, Make it easy to install plugins per-logger, not just globally #117, and remove the rebuild() function introduced to solve Why do I have to configure the log in every typescript file? #187.

  • Rethinking persistence also seems to have come up here and in other places. It seems clear that setLevel() vs. setDefaultLevel() can be a little confusing, too, and the latter mostly exists because of persistence (originally my fault, IIRC).

    The right way out of this is probably to make saving the current level its own method and never do it by default (as suggested earlier in this thread). We should probably also just get rid of setDefaultLevel() entirely — without persistence-by-default, it’s just sugar for if (!logger.getPersistedLevel()) logger.setLevel(x), which is really simple. It’s mainly valuable for library authors (as opposed to app authors), and library authors usually shouldn’t be setting their own level anyway.

  • Rethinking logger hierarchy/relationships.

    • Loggers take their level from the root logger at creation time, which leads to unpredictable defaults and messy config (I think this was my fault, too). Non-root loggers should either be totally independent of the default logger or maintain a live relationship (if they haven’t had their own level explicitly set, they have the level of their parent, including changing when the parent changes — we talked about this in Cleanup/modernization of developer test/build tooling #191 but decided it would be a breaking change).
    • We punted on hierarchy deeper than 1 level when first adding multiple logger support and never revisited it. This is mainly a question of inheriting levels and plugins (see above point). v2 is a good to time to implement this if we want it.
  • Map debug() to console.debug()? (log.debug should map to console debug in Chrome #137)

  • Configuration via environment variables (allow setting logLevel based on env variable #124) and maybe querystring? (Not really breaking changes that need a v2 release)

Are there important things not in that list? Things in that list you have specific ideas about?

@pimterry
Copy link
Owner

pimterry commented Dec 9, 2024

Do you still have any appetite for someone picking this work up?

I just had a baby a couple of months ago, so I am quite busy! That said, I can probably help with shepherding things slowly through and I do still think many improvements here would be worthwhile.

Of your list above:

  • Rewriting the codebase in TypeScript might be nice but isn't critical (although I do find it very useful) but we should certainly publish types.
  • I too am keen to maintain a very wide support base.
    • I'd be very happy to add wider runtime support (Bun/Deno/etc) to that.
    • I'd agree we can drop some older browsers now, but I'm not sure where would be best to set the line. Fancy syntax isn't really the issue (it'd be easy to compile to older JS formats, or even provide multiple bundles for different levels of compat), it was really the weird behaviours of old console globals that was the largest problem, which was mostly an IE6/7/8 issue.
    • The big question is whether we support IE11 or not - it's now very old, but caniuse suggests it's still ~0.5% of web traffic (highest of the IEs), Microsoft still supports it for long-term paid support customers (https://endoflife.date/internet-explorer), at a global scale 0.5% is not an inmaterial number of people, and jQuery v4 (currently in beta) will be IE11+. Depends how much effort it is really, so I'd suggest we aim for that but just keep an eye out for major problems.
    • Regardless, I'd be happy to say anything older than IE11 can definitely go for v2, which will simplify things significantly.
  • ESM support definitely, but yes I'd want to keep a UMD build to provide support for CJS & global usage too at least (I had assumed AMD was dead, but https://www.npmjs.com/package/requirejs still getting 1 million downloads a week suggests not - at least we get this for free with UMD anyway). Nowadays export maps should make it easier to do this cleanly (so modern ESM envs or bundlers don't need to worry about the UMD bundle).
  • Yes, improving the plugin API would be great. The key problem before was that it was designed as an afterthought - hopefully we can design it up front to do better this time around 😄.
  • Agreed on dropping default persistence. Seems like something that could plausibly become a plugin if we started firing a 'level-changed' event or something?
  • Hierarchy ideas are interesting! I don't immediately know what's best here, but I'm very open to improvements and I can certainly see good arguments for if they haven’t had their own level explicitly set, they have the level of their parent, including changing when the parent changes and multi-level hierarchies alongside that.
  • Yes, very very happy to finally resolve log.debug should map to console debug in Chrome #137 and use console.debug again now
  • For env vars/querystring, I think configuration from external sources is something we should think about as a plugin. Seems like a sensible thing to support, but doesn't necessarily seem like something everybody will want or need.

Other thoughts:

  • We should continue to aim to not break the log stacktraces/source line by default, and make it possible for plugins to do the same wherever possible (which means logic based on bind()ing console methods, and other such tricks). It's not always possible, but it's really useful to have most of the time and a key part of the tradeoff between loglevel vs other logging libraries.
  • Would be good to keep the library as small & fast as possible (i.e. minimizing the total runtime JS, and especially the cost of logging calls themselves) and keep functionality to just the essentials, moving what we can into plugins. Some things need to be built in (logging itself, levels definition & configuration, the plugin API, the logger hierarchy), but most other things (timestamps, formatting, remote log transports, persistence) could easily be optional additions.
  • That said, we should be able to include support for more console.* methods (time/timeEnd, group, etc) - this is mostly just implementing them as passthrough to the real API. I'm not sure what the compatibility issues are here, but including some basic fallbacks so you can just call anything everywhere and always get something basic at least (if there are indeed any methods not supported in all our target envs) without any risk of errors is important.
  • Where possible, I'd like to use tooling etc that's unlikely to go through constant churn in future. Bash scripts and small focused mature libraries instead of fancy task runners & frameworks (this may actually be an argument against typescript). With small long-term-stable libraries like this, tooling churn becomes a bigger overhead and hassle than core development if we're not careful.

@Mr0grog
Copy link
Contributor

Mr0grog commented Dec 10, 2024

We should continue to aim to not break the log stacktraces/source line by default… Would be good to keep the library as small & fast as possible

👍 My feel on the core ideas that make Loglevel what it is and that we don’t want to break are:

  • Very small and no dependencies — low cost and easy to include in your project.
  • Broad compatibility/Just Works™ in most environments.
  • Keeps the stack/callsite info intact by default.
  • Filters by level.
  • Multiple, named logger instances.

The big question is whether we support IE11 or not - it's now very old, but caniuse suggests it's still ~0.5% of web traffic (highest of the IEs), Microsoft still supports it for long-term paid support customers

Whoa, I hadn’t realized it was still supported in any form! 😅 FWIW, caniuse is showing 0.37%, but I think that’s out of date — their data comes from statcounter.io, which is showing 0.09% for November 2024 (CSV) (the last date that had 0.37% was January 2022, so my guess here is that caniuse stopped updating IE numbers when general support was discontinued in 2022, and 0.37% was the high mark for that year).

Anyway, I agree that’s not nothing, although it’s at < 0.1% and getting lower every couple months. Happy to go with whatever you decide here. (Interesting side note: I was hoping to dismiss it entirely by seeing all the usage come from US/Europe, where it probably represents old enterprise apps that aren’t going to upgrade any Loglevel version they might be using, but no, it is coming exclusively from Iran, Turkmenistan, and …Liechtenstein (!), which are not so easily ignorable).

We should certainly publish types. …I'd like to use tooling etc that's unlikely to go through constant churn in future. Bash scripts and small focused mature libraries instead of fancy task runners & frameworks (this may actually be an argument against typescript)

I definitely hear you on wanting to minimize churn from tooling. But I think this is always going to be an issue since the level of compatibility you are shooting for is just way beyond what the JS community as a whole is worried about. This development note on compatibility will probably always be necessary.

In particular, I don’t think we can escape:

  • Including TypeScript as long as we are publishing types. At the very least, we need some tests of the type definitions, and I don’t know a good way to do that without TypeScript.

  • A test framework of some sort. I just don’t think it’s worth rolling our own, for so many reasons.

It’s probably possible to replace a lot of the existing Grunt stuff with bash. Otherwise, I’m not sure it’s possible to get more minimal than we are now after #194 and #195.

For types in particular, I’ve maintained packages that build their types from JSDoc comments and ones where the actual source is TypeScript. If this is going to essentially be a rewrite, I would tend towards writing in in TypeScript, but that does tend to add to the dev tooling complexity. So JSDoc comments might be better in that regard. Either way, I would recommend against continuing to hand-roll the type definitions.

it'd be easy to compile to older JS formats, or even provide multiple bundles for different levels of compat

I think we probably want to try and avoid that (at least as much as is reasonably possible), since it drives us right back to this tooling churn issue.

[Persistence] Seems like something that could plausibly become a plugin… For env vars/querystring, I think configuration from external sources is something we should think about as a plugin.

I was thinking we definitely still want built-in support for loading dynamic configuration of some sort — I can understand how that’s not “core,” but it still seems like a pretty basic and common need. If I want to flip on more detailed debugging while investigating a shipping version of a website or app (or filing an issue/support ticket), it’s really nice to be able to just do that without having thought ahead to add an extra plugin or custom code of your own (assuming you are the app’s author; this isn’t an option for you otherwise).

And insofar as the places we’re loading from are writable (e.g. localstorage or cookies and not env vars or querystrings), it would make sense to build in the corresponding write support.

Alternatively, maybe built-in support only includes sources that are non-writable, e.g. env vars or URL querystring/hash? That sidesteps the saving/writing issues.

we should be able to include support for more console.* methods (time/timeEnd, group, etc)

I think there are a lot of open questions here! And there’s already a lot to think about in all the above stuff. Maybe best to leave this to plugins to patch in for now, and it’s something that could be addressed in a later 2.x release?

@pimterry
Copy link
Owner

My feel on the core ideas that make Loglevel what it is

I think we're on the same page here 👍

IE11

Let's try aiming to support IE11 then, but stay open to dropping it if something turns out to be particularly difficult/inconvenient. I'm hopefully it'll be OK.

It’s probably possible to replace a lot of the existing Grunt stuff with bash. Otherwise, I’m not sure it’s possible to get more minimal than we are now

That's ok! Yes, my main concern isn't the current state - in the message above I was really trying to set context for future decisions, as we consider tooling we might want to include.

There are some small changes we could make: I'd be happy to replace Grunt with small scripts, but it's not critical, just nice to tidy up. I think using TypeScript and other fairly mature tools is fine (unless we do very complex things, TS should keep working as-is with low churn for a long time).

I just want to make sure we don't integrate new tools to solve problems as part of v2 that then create extra maintenance going forwards.

it'd be easy to compile to older JS formats, or even provide multiple bundles for different levels of compat

I think we probably want to try and avoid that (at least as much as is reasonably possible), since it drives us right back to this tooling churn issue.

I think in fact it's not so bad! If we say we're happy with TypeScript, tsc can do this for us with no other tools involved. We just set the compile target, e.g. we could build with target: es5 and it'll compile modern code (e.g. classes, arrow functions) into code that'll run in IE11.

For multiple bundles, we would have a couple of different tsconfig.json files and run tsc -p <config> for each format, and that's it. Tsc can also do UMD vs ESM generation with separate config files too.

Dynamic config

Yeah, this is interesting and needs thought, you're right, and it needs thought before the first release, since it's pretty fundamental and could be breaking. My main concern is that everybody will want something different, and we don't want to support everything.

Accepting external changes from storage at runtime is significantly harder than external config at startup - we'd need to monitor localStorage for changes for example, but then exclude our own changes from that, and then you have to think about multiple tabs sharing this state... Gets messy.

Allowing env/URL-based config is easier, but then brings back setDefaultLevel problem. If you have an codebase including log.setLevel('warn') and you run it with LOGLEVEL=info, what loglevel does it have?

3rd option, we could allow runtime changes via an explicit a global API, like globalThis.loglevel so you can just manually change it from any REPL (which works for both browser console & backend debug sessions). Convenient but raises questions about global pollution.

All interesting, but needs thought.

More console methods

I think there are a lot of open questions here! And there’s already a lot to think about in all the above stuff. Maybe best to leave this to plugins to patch in for now, and it’s something that could be addressed in a later 2.x release?

Just an idea really. It is a bit easier than it sounds, since it seems modern runtimes have generally standardized the console API, but would need investigation.

Doing this in plugins would be a bit unusual, since most other existing plugins are all focused on internal processing, not changing the external API very much. Not a breaking change to add later though, so yes I'm happy to punt for now, just food for thought.


Where would you want to start? I think we could break this up into steps, and build the core library + drop old bits (old compat, persistence) + set up export/bundle formats (and test their compatibility) now, while thinking further about the plugin & dynamic config parts that need more design work separately.

@Ryuno-Ki
Copy link

@pimterry

I just had a baby a couple of months ago, so I am quite busy!

Congratulations!

Love this lil library.

@Mr0grog

It’s probably possible to replace a lot of the existing Grunt stuff with bash.

Be aware that zsh and fish are also popular with some people. Perhaps look into a Makefile (so that you have a single interface that then could adapt to different shells).

@pimterry
Copy link
Owner

Congratulations!

Thanks!

Be aware that zsh and fish are also popular with some people. Perhaps look into a Makefile (so that you have a single interface that then could adapt to different shells).

True - but these would be shell scripts, not complex commands you need to run, so they're independent of the default shell you use.

If we put a #!/usr/bin/env bash at the start then they'll always run with bash regardless, even if you launch them with zsh/fish/other, which means that as long as you have bash installed somewhere (extremely common imo) it'll work.

That said, makefiles are very common and extremely stable and fairly convenient for most people I think. Let's see how it goes, and even if even need to migrate from Grunt at all - if we decide we do, and then we end up with quite a complicated script setup, make is definitely a reasonable option to clean that up.

@Mr0grog
Copy link
Contributor

Mr0grog commented Dec 12, 2024

Sorry I was MIA for a few days…

Where would you want to start? I think we could break this up into steps, and build the core library + drop old bits (old compat, persistence) + set up export/bundle formats (and test their compatibility) now, while thinking further about the plugin & dynamic config parts that need more design work separately.

I definitely don’t have time until probably late next week at the earliest, but I think what you’re suggesting makes sense (excepting maybe dropping persistence, since I feel like the question is still open about what we do and don’t want, see below).

When you say “old compat,” do you have specific things in mind? Based on this discussion, I think the clearly droppable things are:

  • Lazy console definition, i.e. enableLoggingWhenConsoleArrives (still support funky environments with no console at all by just making all logs no-ops and never changing).
  • Anything we are currently checking the IE user agent for, e.g. traceForIE.
  • Old syntax? (Easy to use let and some basics IE 11 supports, but I think class syntax might be good to use, too)

With that and separate ESM/UMD builds, we could start releasing v2.0.0-alpha.N builds tagged unstable on NPM and make continuous changes from there (with no compatibility guarantees from release to release).

Do you want to recreate a fresh v2 branch from the current main branch to target that PR to? While we’re talking branches, would you mind renaming the default branch to main?


Dynamic config

Accepting external changes from storage at runtime is significantly harder than external config at startup

Obviously I was not very clear! I just meant loading at startup (I probably should have said “runtime config” or “user config”). I agree live updating is much more complicated and not really necessary (or at least can be a plug-in). This is not a path I meant to send you down.

Allowing env/URL-based config is easier, but then brings back setDefaultLevel problem

What I meant here is that I think it might be OK to ignore this problem. IIRC I “invented” the concern that method was trying to address when I added it, and I am concerned I may have overinflated its value vs. the cognitive complexity it invites. (Another way to support this but that might be more clear is an initialization option, e.g. getLogger(name, { level: 'INFO' }). But maybe I’m overthinking it.)

we could allow runtime changes via an explicit a global API

Personally, I don’t think this shortcut really adds anything valuable over what’s already available.

More console methods

I think there are a lot of open questions here!

modern runtimes have generally standardized the console API, but would need investigation.

This is true, but a lot of these are stateful, and I think impose questions with non-obvious answers when it comes to multiple loggers.


@Ryuno-Ki I think @pimterry already covered what I would have said re: zsh/fish/etc. 🙂

@pimterry
Copy link
Owner

When you say “old compat,” do you have specific things in mind? Based on this discussion, I think the clearly droppable things are: ...

Yes, I agree with all of those, the only other notable part to lose is bindMethod - we should be able to globally rely on Function.bind nowadays no problem. Given that we're moving to TS, we can actually use any modern syntax we like as it'll get automatically appropriately compiled away for the given target.

Do you want to recreate a fresh v2 branch from the current main branch to target that PR to? While we’re talking branches, would you mind renaming the default branch to main?

Both now done 👍

Console APIs

This is true, but a lot of these are stateful, and I think impose questions with non-obvious answers when it comes to multiple loggers.

This is a good point! There's interesting solutions we could explore but yes multiple loggers do make it quite a bit more complicated. If there's any easy wins then we can consider including them, but happy to totally ignore anything non-trivial for v2.0.0.

Persistence

I think the runtime model is still interesting (controlling logs from debuggers/dev consoles is genuinely useful) but wouldn't be a breaking change to add later, so lets park that.

Changing the initialization logic later would be a breaking change though, so we need a plan for that up front.

For default levels, I do think there's a question here worth addressing... The problem is if the application developer (we can ignore libraries here I hope/assume) calls myLogger.setLevel('error') in their code to log only errors by default, and then runs it with an env var set asLOGLEVEL_MYLOGGER=info (probably not the actual env var format, we can bikeshed later). That doesn't seem especially unusual to me, what do you think?

In that scenario, presumably the intent is to override that error value to info instead. Given that, this means if an external input is set, we need to somehow ignore the setLevel call for the affected logger(s).

One option is that we just ignore all setLevel calls for the affected logger (but not other loggers, assuming we're going to use support some kind of logger-targeting syntax like DEBUG etc do). In effect, if you provide an external input, that forces the level of the given logger(s) indefinitely. Does that work? Seems like a reasonable & simple solution if the UX is OK.

@Mr0grog
Copy link
Contributor

Mr0grog commented Dec 19, 2024

OK, I filed a first step PR that drops special IE handling we no longer need in #203. It doesn’t try and change syntax, build tooling, module handling, or anything else yet. I’ll probably try and tackle separate ESM & UMD builds (and maybe TypeScript) next in a separate PR, since there are a lot more decisions and things to be done there.

Both now done 👍

Thanks!

if the application developer… calls myLogger.setLevel('error') in their code to log only errors by default, and then runs it with an env var set asLOGLEVEL_MYLOGGER=info… One option is that we just ignore all setLevel calls for the affected logger

That works, but then I feel like it makes setLevel() a little messy to reason about, and prevents you from opening the console and calling setLevel, so it doesn’t feel great to me.

What I was thinking with my last idea about getLogger(name, { level: 'INFO' }) was that would be the replacement for setDefaultLevel(). So an application developer would normally never call setLevel() (unless they setting up a UI or CLI option for users to set the level). Instead they’d set the default level when getting the logger:

// my-script.js
import { getLogger } from 'loglevel';

// Provide the default level as an option rather than getting the logger and calling `setDefaultLevel()`.
// No changes from today about how `setLevel()` behaves.
const myLogger = getLogger('myLogger', { level: 'error' });

// Rest of app code...
myLogger.warn('Some warn message');

So you get no logs normally:

> node my-script.js

But can override at runtime as usual:

> LOGLEVEL_MYLOGGER=info node my-script.js
Some warn message

But, TBH, this whole conversation is making me feel like I’m overly worried about all this, and the current interplay between setLevel(), setDefaultLevel(), and reading persisted levels might be fine enough.

@wayfarer3130
Copy link

For the naming, could we switch to @loglevel/loglevel for the primary loglevel, but also publish a version as just "loglevel" for backwards compatibility?
Then, we can add @loglevel/log4js for example for a set of log4js bindings for node-js usage, or maybe @loglevel/lambda for lambda function bindings

@Ryuno-Ki
Copy link

Ryuno-Ki commented Mar 2, 2025

For the naming, could we switch to @loglevel/loglevel for the primary loglevel, but also publish a version as just "loglevel" for backwards compatibility?

If I recall correctly there is a policy on it.
I found in Unpublish Policy the recommendation to use npm deprecate.

As in, publish a final loglevel version and announce the migration to @loglevel/loglevel.

@pimterry
Copy link
Owner

pimterry commented Mar 3, 2025

@wayfarer3130 @Ryuno-Ki I'm open to publishing separate-but-related packages as @loglevel/* if we want to, but I'm not sure what reason there is to deprecate the existing loglevel package name. It's simple and easy to remember, and it will continue working just fine with both v1 & v2 versions published - nobody will be upgraded unexpectedly because of how npm's default semver version ranges work (in addition to npm lockfiles too).

I assume your concern with intentionally using v1 in new and existing apps after v2 is published? That will be relevant for codebases that don't want the churn of updates or who do need the very old IE version support we're dropping. That won't be a problem though - there will be no issue with using v1 if you want to after v2 is published, in either existing or new codebases, even if the package name is the same.

It would be possible to deprecate individual versions of the package (i.e. all v1 versions of loglevel but not v2) if we wanted to, but while v1 is working and has no known issues or anything I don't think that's necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants