{"id":22763618,"url":"https://github.com/mearns/loglow","last_synced_at":"2025-03-30T09:42:19.368Z","repository":{"id":33702023,"uuid":"233714275","full_name":"mearns/loglow","owner":"mearns","description":"Logging and More for JavaScript","archived":false,"fork":false,"pushed_at":"2023-01-05T12:45:57.000Z","size":958,"stargazers_count":0,"open_issues_count":42,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-05T11:45:05.210Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mearns.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-01-13T23:23:37.000Z","updated_at":"2020-10-31T03:12:57.000Z","dependencies_parsed_at":"2023-01-15T02:07:42.864Z","dependency_job_id":null,"html_url":"https://github.com/mearns/loglow","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":"mearns/node-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mearns%2Floglow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mearns%2Floglow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mearns%2Floglow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mearns%2Floglow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mearns","download_url":"https://codeload.github.com/mearns/loglow/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246301955,"owners_count":20755512,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-11T11:09:22.312Z","updated_at":"2025-03-30T09:42:19.336Z","avatar_url":"https://github.com/mearns.png","language":"JavaScript","readme":"## Overview\n\nLoggers have names, which are actually heirarchical paths, typically beginning with the package name or the package\nscope and package name, and then usually refined by module and function names. Path heirarchies are delimited by\nslashes, like \"@loglow/core/config/loadConfig\".\n\nLogging configuration is specified by path and applies to all loggers which include that path as a prefix. For instance,\nconfiguration set for \"@loglow/core\" will apply to a logger named \"@loglow/core\" and also to all loggers whose names start\nwith \"@loglow/core/\". Note that it only applies to full path prefixes, e.g., it would not apply to a logger named\n\"@loglow/coretastic\". Additionally, a root configuration applies to all loggers.\n\nConfiguration is defined by configurators: functions that take a configuration object and return a configuration object.\nAll configurators that apply to a given logger are applied sequentially from shortest path to longest, starting with a\ndefault configuration object to which the root configurator is applied. The resulting configuration is passed to the next\nconfigurator, etc.\n\nThe resulting configuration object is called an _implementation_. It's created when a logger is needed and cached until\na configurator that applies to the implementation is changed.\n\nA logger itself, the thing that is used for adding log entries, is mostly just a façade around some functions that load\nthe required _implementation_ and use it to generate the log-entry. Loggers are stateless; you can get one by name when you need it,\nreuse or throw it away and get it back when you want it again. Because of the separation of loggers and implementations,\ngetting a logger is cheap.\n\n## Config\n\nConfiguration for a given logger has four components:\n\n-   Enabled / Disabled\n-   Decorators\n-   Middleware\n-   Receivers\n\n### Enabled / Disabled\n\nThis is the master switch for a logger. Loggers are disabled by default and need to be enabled for them to do anything. If\na logger is disabled, all calls to it are simply no-ops.\n\n### Decorators\n\nA **decorator** is function that returns a set of metadata that gets added to all log entries for a logger.\n\n### Middleware\n\n**Middleware** is a log-entry transformer, as described below. Middleware is very powerful and very general. In fact, most\nof the other aspects of configuration are actually implemented as middleware. Middleware can not only transform the data in a single\nlog entry, but also filter out log entries, split one log entry into multiple, and perform side effects.\n\nIt is important to keep in mind that middleware is called synchronously on every log (assuming the logger is enabled). Further, middleware\nis configured as a chain, so one middleware function may be one of several that get applied. Adding milliseconds to every log call is\nprobably not a good idea.\n\n#### Receivers\n\nThe **receiver** is the ultimate destination for every log entry, at least as far as the logging library is concerned. The receiver will\ndo things like write the entry to console or file system, ship it to a log aggregation system, etc.\n\nEvery enabled logger has to have exactly one receiver configured. If you have multiple destinations you want a log entry to go to, you\ncan use one receiver function that delegates to multiple other functions, but it is up to you to implement this.\n\nIf your receiver is asynchronous, as will often be the case, serialization must be handled external to the logging. In other words, the logging\nfunction will not wait for your receiver to finish asynchronous work: as soon as the receiver function returns, the logger can proceed\nwith the next log entry.\n\nTo handle serialization of asynchronous receivers, make your receiver stateful, tracking a reassignable promise. On each call, create a new\npromise chained from the previous, settling when the receiver is completed, and set the tracked promise to the new promise. You'll also want to\nmake sure you don't quit your application before this promise is settled.\n\nTODO: We should provide a helper function to do this for any given receiver function.\n\n## Log Entries, Decorators, Receivers, and Middleware\n\nThis section describes the core concepts associated with writing logs. The basic flow for writing a log is as follows:\n\n1. Call your logger to intiate a log entry. You specify a message and optional metadata; a timestamp is added automatically.\n2. Decorators are applied to add additional metadata to the log entry.\n3. Middleware is invoked as a chain to transform the log entry.\n4. The configured Receiver for the logger is invoked with the finalized log entry.\n\nA call to initiate a log entry looks like this:\n\n```typescript\nfunction logger(message: string, ...meta: Array\u003cunknown\u003e): void;\n```\n\nNote that the `meta` parameter is variadic: you can pass in any number of arguments of any type\nfollowing the initial `message` parameter.\n\nThis produces a log entry with the following format:\n\n```typescript\ninterface LogEntry {\n    date: Date;\n    message: string;\n    metadata: unknown;\n}\n```\n\nUpon creation, the initial log entry has its `metadata` property set to the _array_ of variadic\n`meta` arguments arguments passed into the logger.\n\nNote that if the logger is not enabled, the log entry is not created and the call to logger returns (almost) immediately without doing anything.\n\nAfter the initial log entry is created, any decorators configured for the logger are invoked\nand their return values are appended to the end of the `metadata` array. Note that decorators must\nreturn synchronously.\n\nThe next step is to apply any configured middleware for the logger. A middleware is simply a function with the following signature:\n\n```typescript\ntype middleware(loggerName: string, logEntry: LogEntry, next: (loggerName: string, logEntry: LogEntry) =\u003e void): void;\n```\n\nThe first middleware function is invoked with the name of the logger and the initial log entry after decorators are applied.\nThe third argument is a function that encapsulates invoking the next middleware in the chain,\nif any. The middleware is responsible for passing the desired log entry to this function in order\nto get it processed. If the middleware doesn't invoke the given `next` function with a log entry,\n_the log entry will be dropped_. This is how middleware can be used to filter out log entries based\non their contents (if you want to drop everything for a logger, it's usually preferable to just\ndisable it). Alternatively, the middleware can invoke `next` multiple times to generate multiple log\nentries from one. The most common use case for middleware is to transform the log entry before passing it on.\n\nNote that the `next` function is only valid until the middleware function returns. If you call it\nagain after that, it will throw an error. This is to prevent middleware from attempting to do\nasynchronous transformations which could cause logs to be processed out of order.\n\nAlso note that it is not specified whether `next` will actually cause the next middleware to be\ninvoked as part of the same callstack or if it will queue up the call(s) to be made after the\ncurrent middleware returns. Middleware should not depend on either of these possibilities (e.g.,\ndo not assume your middleware can catch errors thrown by subsequent middleware).\n\nThe final middleware function is invoked with a `next` argument which encapsulates passing the log\nentry to the configured `receiver`. This is entirely transparent to the middleware, it doesn't know (nor should it care)\nif the it is the last middleware function or not, whether `next` is going to be for another middleware\nfunction, or for the receiver.\n\nMiddleware has the potential for changing the loggerName, but not changing the logger itself and doing so is\n**not recommended**. In other words, you can pass a different loggerName to `next` than what your middleware was\ninvoked with, but this does not change the logger implementation: it will get processed by the remaining middleware,\nand end up at the same receiver. Again, this is not usually recommended as it can make it difficult to understand\nwhere a log entry came from.\n\nIf that's _not_ what you want, you can simply get the logger you want to use and invoke it as you normally would.\n\n## Errors\n\nErrors in middleware will be caught and cause the log entry to be aborted, replaced by a log entry for a special\nlogger named \"@loglow/external/errors\". Note that this entry is still passed to the original logger for the entry\nso that it will show up where expected, but as a different logger name. This is the same anytime a middleware\nchanges the logger name (although it's not usually recommended to do so).\n\nErrors in receivers will not be caught, they will be thrown from within the logger invocation. If that's not what you want,\nyou'll need to handle the errors inside of it.\n\n## Usage Patterns\n\nWe divide logging users into two categories: leaders and contributors. Contributors can get loggers, use them, and even decorate\nthem, but cannot enable or disable them, configure middleware, or configure receivers. Leaders can configure any loggers\nin any way.\n\nLibraries are almost always contributors, where as a top-level application would be the leader. Initially, there is no leader;\nanyone can configure any logger however they please. To become the leader, a module just needs to ask for the lock. As long as something\nelse hasn't already taken the lock, that module will receive it and become the leader. Once the lock is acquired, that module becomes\nthe leader, with exclusive access to full configuration.\n\nLibraries will generally get, decorate, and use loggers, but not do any additional logging configuration, where as the top-level\napplication performs configuration of all loggers to control what logs get written out to the final destination and how.\n\nThe top-level code contains the entry point for the application and is therefore uniquely positioned to be the first to get the lock,\njust in case an included module trys to do that same. If anything but the lock holder tries to get a lock or configure logging, its\nsimply a no-op.\n\nAfter acquiring the lock, the leader will configure logging at startup for whatever loggers they care to configure. This is typically\ndone at load time for the top-level entrypoint of the application, before any other modules are loaded so that the configuration is\nin place before any loggers are used.\n\nSince contributors don't really know what situation they're being used in, they don't have a reason to configure most aspects of logging.\nOne aspect of configuration that is useful for contributors is decorating loggers. Adding a package version, for instance, could be\nuseful. Single threaded code might also decorate its logger with a unique identifier so that logs can be correlated together into a\ncoherent story.\n\nFor re-entrant and asynchronous code, decorating one logger for the entire module may not be appropriate since the logs entries from\ndifferent stories would be interleaved. One strategy to deal with this involves generating a unique identifier for each such story\n(if there isn't already an intrinsic identifier for the story), and passing it through to all subsequent functions. This isn't ideal,\nbut something like https://nodejs.org/api/async_hooks.html#async_hooks_class_asynclocalstorage should help hen it's available.\nThe unique story identifier is sufficient for correlating logs together. If you want to decorate those logs with common data, you\ncan use a logger whose name includes the story identifier, and decorate that.\n\n### Decorating Pro Patterns\n\nCorrelating logs together in a coherent story is a good thing. Adding the same metadata to every log in a story is redundant.\nIn many cases, it's all you have. A better pattern, where possible, is to have a log collector that ties together your log entries\nbased on a defined story identifier as described above. Once assembled into a story, you only need the redundant metadata in a single\nlog entry.\n\n## Log Levels\n\nSurprise! There are none. We've come to the conclusion that log levels aren't a great pattern. It's usually better to have static\nlog messages which are clarified with dynamic metadata, and add a middleware filter to include only those specific log messages\nyou actually care about. Trying to decide in advance on where a given log entry fits on a linear scale of utility tends to be\nfraught with errors, leading to either overly noisy logging or ineffectively sparse logging.\n\nBut old habbits die hard, and we can't pretend that log levels are never going to be useful. The easiest approach is to simply\nadd a pre-defined metadata field to every entry defining its level. A middleware function can then filter out entries based on\nthis field.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmearns%2Floglow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmearns%2Floglow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmearns%2Floglow/lists"}