-
-
Notifications
You must be signed in to change notification settings - Fork 157
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
Make it easy to install plugins per-logger, not just globally #117
Comments
To repeat the current problem from the previous thread:
|
This totally makes sense, and that is a use case I'd like to support properly. I'll take a proper look at it this weekend, but I definitely think there's a route through there. As a first impression, I think the right approach is to make it easy to scope plugins to a specific logger instance. We've already got const log = require('loglevel');
// In the webpack-dev-middleware:
const middlewareLogger = log.getLogger('middleware');
// In webpack-dev-server:
const serverLogger = log.getLogger('dev-server');
// [etc]
applyPlugin(middlewareLogger);
// ^^ exact setup here is plugin specific
middlewareLogger.warn(...); // plugin should have wrapped this method
serverLogger.warn(...); // plugin should not have wrapped this method Would that work for you? I think almost all the plumbing is here to make this approach work, and it might even already work out of the box (though I need to find some time to thoroughly test that), and the real problem is that loglevel-plugin-prefix specifically checks to block this: https://github.com/kutuluk/loglevel-plugin-prefix/blob/master/lib/loglevel-plugin-prefix.js#L44. @kutuluk is there a specific reason you need to check this, and enforce that the plugin is applied to the root logger? |
The code in the comment preceding this one is what we had in mind, yes, thank you. In looking at the prefix plugin, it appears to be proxying the method factory, which doesn't seem to be doable on anything but the root logger. |
@pimterry yeah in taking another look at the code, |
@pimterry This leads to the fact that if the plugin is assigned to both the root logger and the child logger, then the plugin is executed twice. Therefore, the plugin can be applied either only to the root logger, or only to the child loggers. I chose the root logger. |
@shellscape I've done some investigation. That explanation above isn't correct - @kutuluk That's explanation is very useful, thanks. I see what's going on here I think - you're saying that it would work (i.e. you can apply plugins independently to different loggers), but that doing so breaks the prefix plugin's code because it would have to be run repeatedly, and that will break things. Is that about right? Looking at that code, you've got a static This is your underlying issue @shellscape. You don't need changes in loglevel at all to fix this, as far as I can tell - the tricky global state is on the plugin side. If the above's all correct, we can fix this by disabling the root logger check in the plugin and making the state linked to the logger somehow, rather than being global (e.g. you could use The semantics of |
In the current implementation, loglevel, originalFactory and pluginFactory are static because I allow the plugin to be applied exclusively to the root logger. I can easily rewrite the code so that these values are stored in the apply scope and the plugin can be applied to any number of child loggers. But then I will be forced to prohibit the use of a plugin with a root logger. The key problem is that you assign a plugin to the root logger, and then to the child logger, then when you call the method, the plugin with the settings of the child logger will be executed first, and then the plugin with the settings of the root logger will be executed. The code does not break, the plugin will work fine in both cases, but the user will get a behavior that he obviously does not expect. The current loglevel api does not provide tools that could fix this problem, so I'm forced to implement the plugin as it is now. |
Sorry, I've been off for Christmas - I'm now back looking at this! There is indeed some tricky plugin UX there. It's not super clear to me how to have both namespaced plugins and default 'global' plugin configuration, without loglevel providing a much more complex api to manage the currently applied plugins, which is both more complex and less flexible than the current 'just provide a function' approach. Alternatively, perhaps child loggers could be linked to the root logger, rather than copying it on setup? I.e. if you haven't added a plugin to a child, it should behave like the root logger (even if the root has had plugins added after the child was created), but when/if you do add a plugin to a child, then it should use that plugin and ignore any of the root logger's plugins entirely. Does that make sense? It'd be fairly easy to implement, but it would need a breaking change in behaviour, and maybe the plugin api itself (though I think that's optional), and I'm not sure if it is the most intuitive or useful model. Thoughts? Alternatively, for now, I think you can handle this in the plugin itself, by detecting this specific case. You can allow people to set plugins for either child loggers or the root logger, but record whether they've done so (e.g. That's not perfect, since there are niche cases where you could want to separate manage root and child plugins, but it's fairly simple to do I think, and it does give users a lot more flexibility than they have now. |
I was able to solve this in my fork without pain. In my implementation, |
Ah, interesting. Yeah, that already works here - you should be able to proxy The problem, as you've discovered, is that that only works if all your loggers are totally independent. That's a bit limiting, and changing it would certainly be a breaking change. Right now, using the shared ready-out-of-the-box root logger is how loglevel is primarily used afaik (child loggers are relatively new), and it's a simpler model for the standard easy use case, where you don't really need separate independent loggers. My suggestion above I think lets you bridge this, a bit: you'd have a root logger, plus child loggers that share plugins by default, but which can become totally independent if you try to add a plugin to any of them individually. That lets you do global or local logging totally however you like, in a composable way (separate libraries can decide to do either, within one codebase) My main concern is whether that's nice & intuitive, or really just confusing... |
We haven't found it remotely limiting. In fact, we've found the exact opposite. The root logger is still present in our implementation - it's simply an instance, like any "child" logger (we don't call them that), extended with sugar for ease-of-use. Insisting on a "master logger to rule them all" and implementing a master-child relationship is actually limiting, and I believe this thread/topic exposes the reasons why. I wasn't suggesting that you implement our paradigm - the differences of opinion on that are clear - |
I'm talking about the limitation you described above: 'that relies upon each logger being a wholly unique instance'. I'm not necessarily against going in that direction, especially if this necessitates a breaking change regardless, but it is a new constraint.
Ok, but to be clear: I am quite open to implementing that same paradigm. There are short-term workarounds, and other options to discuss too, but this in general seems like somewhere where there's clear value in improvements, and those are likely to be breaking regardless. Keeping plugin compatibility with your fork would be a nice free benefit to this approach, so you could benefit from the existing plugin ecosystem too, once they upgrade. |
kutuluk/loglevel-plugin-prefix@c7c9749 Solved =) |
@kutuluk neat, ok. Just so I understand, your solution there is:
Is that right? Implementation is a little complicated, but pretty neat, and it works very nicely & fairly intuitively from an end user POV imo. Obviously it'd be better if we could make it simpler for plugin authors to do this, and I think I'd like the get the built-in approach closer to this model out of the box (and in a perfect world, support some kind of |
Methods are wrapped if the plugin is applied to the logger for the first time. Each time only the configuration changes. |
Simplified plugin: const defaults = {
prefix: 'prefix',
};
const configs = {};
const apply = (logger, config) => {
if (!logger || !logger.setLevel) {
throw new TypeError('Argument is not a logger');
}
const originalFactory = logger.methodFactory;
const name = logger.name || '';
function methodFactory(methodName, logLevel, loggerName) {
const originalMethod = originalFactory(methodName, logLevel, loggerName);
const options = configs[loggerName] || configs[''];
return function (...args) {
// skip the root method for child loggers to prevent duplicate logic
if (name || !configs[loggerName]) {
args.unshift(options.prefix);
}
originalMethod(...args);
};
}
if (!configs[name]) {
logger.methodFactory = methodFactory;
}
const parent = configs[name] || configs[''] || defaults;
configs[name] = Object.assign({}, parent, config);
logger.setLevel(logger.getLevel());
return logger;
};
export default { apply }; |
cc @shellscape - pulling out the conversation from #82 (comment) as separate issue.
The text was updated successfully, but these errors were encountered: