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
feat(message): Add new message interface #2399
Conversation
This makes a lot of sense to me. Qt is pretty well battle-tested and this messaging framework seems to work well for them. My concerns are as follows:
@kt3k thoughts? |
WDYT, @kt3k? |
Instead of a second framework, I think we should rework the current log mod (there were attempts in the past. I tried for a class based logger approach here which seems similar in concept) and issues now (#2645) while making it more general and expandable to be suitable for more use cases. I have some concerns of this specific PR:
|
@timreichen Some other responses:
I'll try changing it to use template strings and Console log name over the weekend and see if that helps. I have nothing against extending std logging, I just want library developers to be able to supply more information without changing the control flow. |
I think the two are compatible with the right structure.
Changing a global state with functions without reference to the state itself. enableCategory("driver.usb") in js you would expect either a property set, a method call or a passing a reference of the state: enableCategory(categories, "driver.usb") or better categories.enable("driver.usb") or even better categories["driver.usb"].enable = true The problem with a global state approach is that it introduces anti-patterns (the current log implementation also does this with
I think if we took a scoped state approach instead, we could achieve basically the same with more clarity and a more native js feel. For example, if log mod introduced class based loggers (as proposed before), we simply can add before: // Create a new category.
LOGGING_CATEGORY("driver.usb");
warning("This is a test warning"); // Should print "This is a test warning"
warning("driver.usb", "No USB devices found"); // Should print "driver.usb: No USB devices found"
const logger = new MessageLogger("driver.gpu", MessageType.Debug);
logger.debug("GPU Vendor = Intel"); // Should print "driver.gpu: GPU Vendor = Intel"
// Shouldn't print anything (debug messages disabled by default).
debug("driver.usb", "PCI device 1 found");
enableCategory("driver.usb", MessageType.Debug);
// Should print "driver.usb: PCI device 2 found"
debug("driver.usb", "PCI device 2 found");
disableCategory("driver.gpu"); // Disable all messages from `driver.gpu`
warning("driver.gpu", "Old driver found"); // Shouldn't print anything after: const logger = new ConsoleLogger()
const usbLogger = new MessageLogger(MessageLogger.logLevels.trace, { formatter: () => … })
logger.warn("This is a test warning"); // Should print "This is a test warning"
usbLogger.warn("No USB devices found"); // Should print "driver.usb: No USB devices found"
const gpuLogger = new MessageLogger(MessageLogger.logLevels.debug, { formatter: () => … });
gpuLogger.debug("GPU Vendor = Intel"); // Should print "driver.gpu: GPU Vendor = Intel"
usbLogger.quiet = true
// Shouldn't print anything (debug messages disabled by default).
usbLogger.debug("driver.usb", "PCI device 1 found");
usbLogger.quiet = false
// Should print "driver.usb: PCI device 2 found"
usbLogger.debug("PCI device 2 found");
gpuLogger.quiet = true
gpuLogger.warn("Old driver found"); // Shouldn't print anything
export { usbLogger, gpuLogger } some other file: import { usbLogger, gpuLogger } from "…"
usbLogger.formatter = (logLevel, data) => `${new Date().toISOString()}: ${logLevel} -> ${data.join(" "}`
gpuLogger.quiet = true |
The problem with this is that it means each MessageLogger must have its own formatter. That almost certainly means that library developers will add their own formatter function. I just don't think that would scale to hundreds of modules.
This I think is possible to get rid of. The only global would be a function: the message handler. Example: const usbLogger = new MessageCategory("driver.usb");
usbLogger.warn("No USB devices found");
const gpuLogger = new MessageCategory("driver.gpu");
gpuLogger.debug("GPU Vendor = Intel");
usbLogger.quiet = true
usbLogger.debug("driver.usb", "PCI device 1 found");
usbLogger.quiet = false
usbLogger.debug("PCI device 2 found");
gpuLogger.quiet = true
gpuLogger.warn("Old driver found"); By default, the output would be:
If the user/application wanted to customize this, they could have this: main.ts: import { setMessageLogger, MessageCategory } from "https://deno.land/std/";
setMessageLogger((category: MessageCategory) => {
// print what/however you want.
}); Or import * as log from "https://deno.land/std/log/mod.ts";
await log.setup({
logger: (msg: string, context: MessageContext, category: MessageCategory) => {
// print what/however you want.
}
}) Would this be better? The former feels like the native JS API for global |
I think the first approach is better. export const usbMessageLogger = new MessageLogger()
usbMessageLogger.dispatchEvent("message", new MessageEvent({ data: { logLevel: logLevels.debug details: "information" }} )) and import { usbMessageLogger } from "…"
usbMessageLogger.addEventListener("message", ({ data }) => {
console.log("from usb message logger:", data.logLevel, data.details)
}) |
I made some major changes to make it more JS-like. It's much smaller and hopefully less opinionated. By default, nothing is printed. If the point of messages is for modules to supply information to applications, then applications should be in control of all output. As far as EventTarget goes, I considered something like that, but there are two problems: |
How so?
I disagree. How is the user to know what loggers there are if they are not exported? I think if something is meant to be used by external code, it should be exported as a gerneal pattern in js. It also solves the issue of category name overlaps. |
I'm open to EventTarget, but my concern is that if it's being called in an async function or a callback, it could lead to some unexpected results for users. If dispatchEvent is synchronous though, that makes me feel better. My concern (as always) is scalability. Imagine if every module in the Denoland registry used this. Applications would have to import hundreds of loggers. It would also create a coupling by making the application depend on a module dependency. For naming overlap, it does put it on the module developer to pick category names (which is why the docs recommend |
/** Log a debug message. */ | ||
log(arg: string, ...args: unknown[]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In deno, log()
and info()
log messages in the same level (1), and debug()
logs messages in lower level (0). ref. https://github.com/denoland/deno/blob/f729576b2db2aa6ce000a598ad2e45533f686213/ext/console/02_console.js#L1977-L2005
I think these methods should be align with it.
/** A class for categorized messages. */ | ||
export class MessageCategory { | ||
/** Silence all output from this category if true. */ | ||
quiet = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this property necessary? What is the supposed usage?
...args: unknown[] | ||
) { | ||
// Do not print empty messages or when in quiet mode. | ||
if (!msg || this.quiet) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignoring empty msg
feels confusing to me as console.log()
prints a new line in JS
A logging library meant to scale to thousands of modules has to prepare for thousands of copies of the library itself present within the same application. Below is a hypothetical example with 2 copies of // https://adder.example/add@0.1.0
import { MessageCategory } from "https://deno.land/std@0.171.0/message/mod.ts";
const logger = new MessageCategory("add.trace");
export function add(a: number, b: number): number {
logger.info(`${a} + ${b}`);
return a + b;
}
// main.ts
import { setMessageHandler } from "https://deno.land/std@0.172.0/message/mod.ts";
import { add } from "https://adder.example/add@0.1.0";
setMessageHandler((message) => console.log(message));
add(1, 2); // should print `1 + 2`, but would not with the current design |
If application used hunderts of mods they would have an import statement of each anyway and only needed to add a specifier for each required logger. This is transparent and has no leeway for unwanted behavior. It also solves the issue of mismatching versions @0f-0b described without any global variable. I see two ways to solve the issue of scalability:
|
I agree with @Symbitic on the below
If logs are produced in transitive dependencies (ie. grandchildren, grandgradnchildren deps), it's difficult/messy to import those loggers/categories from them. |
if the Deno mantra is to use the platform then why not just use console.* ? |
Closing because of inactivity. If anyone feel strongly about this proposal, please open a new one. |
This is a port of the Qt Message logging framework. In contrast to the application-oriented
log
standard library module, this is specifically designed to scale to hundreds or thousands of modules while giving applications full control over the output.It does this by separating messages from their presentation. Modules emit messages, and the application takes care of if/how to present them.
This isn't a replacement for the
log
framework. Handler functions can use the logging module to print messages. This is intended for modules to emit categorized information that can be handled by the application.What it offers
[%{time yyyy/mm/dd}] %{message}
) and all messages from every module will be printed in this format.Use cases
Consider two opposing use cases:
This accommodates both use cases. Because the application is responsible for the presentation, modules do not need to worry about printing too much information. Debug messages are disabled by default and must be enabled by the application.
Related issue: #2398
See also: