lognow
v0.4.0
Published
Quick and clean universal logging.
Downloads
181
Maintainers
Readme
lognow
Quick and clean universal logging.
Overview
[!WARNING]
Lognow is under development. It should not be considered suitable for general use until a 1.0 release.
Somehow, no single logging library out there quite worked for my purposes... so it's come to this.
Lognow provides a handful of helpers and turn-key configurations for (my) typical logging needs. It's a thin wrapper over the LogLayer project, allowing its use in a consistent, pre-configured, and unobtrusive way.
It provides:
- Pretty console logs by default with a reasonable amount of metadata, colorization, object formatting, error handling, etc.
- Rotating JSONL file logs enabled via a single option flag.
- A plausible strategy for log dependency injection in library projects and integration with other logging systems.
- A universal / isomorphic implementation with support for Node.js, web browsers, and Electron.
It's probably most similar aesthetically to tslog, but with the extra interoperability abstractions provided by LogLayer, and integrated Electron support a la electron-log.
This library is not designed to be infinitely configurable or extensible. Instead, it's designed to cover 95% of my use-cases with a single import, and gets to 99% with a few lines of configuration. For the remaining 1% of my logging needs, it makes more sense to just work with LogLayer directly.
Getting started
Dependencies
Node 20.19.0+, or any recent web browser.
Installation
npm install lognowQuick Start
import { log } from 'lognow'
// Simple logging
log.info('Hello, world!')
// With metadata
log.withMetadata({ action: 'login', userId: '123' }).info('User logged in')
// With persistent context
log.withContext({ requestId: 'req-456' })
log.info('Processing request')
log.info('Request completed')
// Both logs include the requestId context
// Different log levels
log.debug('Debug message')
log.warn('Warning message')
log.error('Error message')Usage
Basic
Most of the time, it makes sense to just import the default log instance:
import { log } from 'lognow'
log.info('What hath God wrought?')By default, you'll get a timestamped pretty log in the console or terminal:
12:47:56.394 INFO | What hath God wrought?[!IMPORTANT]
The log instance's interface is a bit different than a typical
Consoleobject — it's a LogLayerILogLayerinstance, so instead of passing objects and metadata directly to the logging method, additional methods are chained together to separate message strings from metadata or context objects.
A quick example:
import { log } from 'lognow'
log.withMetadata({ energy: 2, mood: 3 }).info('vibe check')15:09:21.011 INFO | vibe check
{ mood: 3, energy: 2 }This is a bit of extra work, but it disambiguates your logging intent in such a way that future interoperability with different logging targets is assured.
See the LogLayer docs for a full description of the interface.
Log Levels
Lognow supports six log levels, from least to most severe:
import { log } from 'lognow'
log.trace('Detailed debugging information')
log.debug('General debugging information')
log.info('Informational messages')
log.warn('Warning messages')
log.error('Error messages')
log.fatal('Fatal error messages')By default, only info and above are displayed. Use the verbose option to show all levels (see Configuration below).
Logging Errors
Errors are automatically formatted with stack traces:
import { log } from 'lognow'
try {
throw new Error('Something went wrong!')
} catch (error) {
log.error('Failed to process request', error)
}15:09:21.011 ERROR | Failed to process request
Error: Something went wrong!
at Object.<anonymous> (/path/to/file.ts:4:9)
...Context vs Metadata
The underlying LogLayer library provides two basic ways to attach data to your logs:
withMetadata()- Attaches data to a single log entrywithContext()- Adds context to the logger instance itself, which persists across all subsequent log calls on that instance
import { log } from 'lognow'
// Metadata: attached to one log only
log.withMetadata({ requestId: '123' }).info('Processing request')
log.info('This log has no metadata')
// Context: modifies the logger instance
log.withContext({ requestId: '123', userId: '456' })
log.info('Starting request')
log.info('Request completed')
// Both logs will include the context (requestId and userId)Multiple Messages
You can pass multiple messages to a single log call, but they must be a primitive type (objects need to be passed as metadata or context):
import { log } from 'lognow'
log.info('User logged in', 'Session started', 'Welcome email sent')15:09:21.011 INFO | User logged in Session started Welcome email sentOptions
Lognow has just five options:
name
Set the name of the logger, which is used as a prefix in messages logged to a console, and as extra metadata in logs written to files.
The default behavior depends on your runtime context:
- Browser projects default to undefined.
- Node projects with a
package.jsonwill default to the package name. - Electron projects default to
"Main"for the main process and"Renderer"for the renderer process.
logToConsole
Set to true or false to enable or disable pretty console logging.
Defaults to true.
Alternately, set to any Console- or Stream-like log target, or a partial PrettyBasicTransportConfig object to override the default configuration. (If you don't define a log target explicitly, process.stdout is used in Node.js and console is used in the browser.)
logJsonToFile
Set to true or false to enable or disable file logging. When enabled, logs are written as newline-delimited JSON (JSONL) to disk.
Defaults to false.
Logs are stored in the platform-standard location. The files are gzipped and rotated daily, and are never removed.
Complex metadata or context objects not natively representable within the JSON specification are serialized on a best-effort basis, with emphasis on being human-readable rather than being perfectly reconstructible as JavaScript.
Alternately, pass a path to a directory to log to the location of your choosing, or a partial JsonFileTransportConfig object to override the default configuration.
logJsonToConsole
Set to true or false to enable or disable JSON console logging. When enabled, logs are written as JSON strings to the console.
Defaults to false.
Alternately, set to any Console- or Stream-like log target, or a partial JsonBasicTransportConfig object to override the default configuration.
verbose
This is just a shortcut for setting the log level.
If true, all logs are shown regardless of level. If false, only info and higher logs are shown.
Defaults to false.
| Level | Priority | Verbose: False | Verbose: True |
| ------- | -------- | -------------- | ------------- |
| trace | 10 | ❌ | ✅ |
| debug | 20 | ❌ | ✅ |
| info | 30 | ✅ | ✅ |
| warn | 40 | ✅ | ✅ |
| error | 50 | ✅ | ✅ |
| fatal | 60 | ✅ | ✅ |
Configuration
Configuring the default log instance
Use setDefaultLogOptions() to set options on the default log instance.
import { log, setDefaultLogOptions } from 'lognow'
setDefaultLogOptions({
logJsonToConsole: true,
logToConsole: false,
name: 'example',
})
log.withMetadata({ energy: 2, mood: 3 }).info('vibe check')Will log:
{"level":"info","messages":["vibe check"],"metadata":{"energy":2,"mood":3},"name":"example","timestamp":"2025-10-25T19:34:14.754Z"}Configuring new instances
Use createLogger() to set options on a new custom logger instance. Defaults are assumed for any undefined options.
import { createLogger } from 'lognow'
const customLog = createLogger({
name: 'custom',
})
customLog.info('hello')Will log:
15:38:50.138 INFO [custom] helloNote that only the default log instance's configuration is mutable (via setDefaultLogOptions()). Custom logs should be recreated with createLogger() if you need to change configuration.
Environment configuration
In Node.js-like environments, Lognow respects several environment variables:
NO_COLOR- Disables colorization in console outputFORCE_COLOR- Enables colorization (takes precedence overNO_COLORif both are set)DEBUG- Enables verbose logging (equivalent toverbose: true)
These environment variables override any code-level configuration.
Example:
# Disable colors
NO_COLOR=1 node your-app.js
# Enable verbose logging
DEBUG=1 node your-app.js
# Force colors and verbose logging
FORCE_COLOR=1 DEBUG=1 node your-app.jsExamples
Log to a File
By default, log files are stored in the typical logging directory for your platform:
import { log, setDefaultLogOptions } from 'lognow'
setDefaultLogOptions({
logJsonToFile: true,
name: 'My Application',
})
log.info('File this away')On macOS, this will write to ~/Library/Logs/My Application/.
On Linux, logs are written to ~/.local/state/My Application/logs/.
On Windows, logs are written to %LOCALAPPDATA%\My Application\logs\.
Custom Log Directory
Specify a custom directory for log files:
import { log, setDefaultLogOptions } from 'lognow'
setDefaultLogOptions({
logJsonToFile: '/logs',
name: 'My Application',
})
log.info('This will be logged to /logs')Dual Output: Console and File
Log to both console and file simultaneously:
import { log, setDefaultLogOptions } from 'lognow'
setDefaultLogOptions({
logJsonToFile: true, // Structured logs in file
logToConsole: true, // Pretty logs in console
name: 'My Application',
})
log.info('Logged to both console and file')
// Console: "12:47:56.394 INFO [My Application] Logged to both console and file"
// File: {"level":"info","messages":[...], "name":"My Application",...}Electron
Lognow automatically manages inter-process communication in Electron applications to merge any logs from the renderer process into your main process' log stream.
In your main process, e.g. main.js, grab the default log instance like you would in any other context — the only difference is that you explicitly import from the lognow/electron export instead:
import { log } from 'lognow/electron'
log.info('Hello from main!')
// The rest of your Electron main process code...When you want to log from the renderer, add the following to your preload script, e.g. preload.js. This sets up an inter-process-communication (IPC) channel to ship messages from the renderer to the main process:
import 'lognow/electron/preload'
// The rest of your Electron preload code...Then, in your renderer / browser code, use the default lognow/electron log export as usual:
import { log } from 'lognow/electron'
log.info('Hello from renderer!')
// The rest of your Electron renderer code...That's it. When you run the project, logs from both processes will appear in your main process' console, prefixed with their origin:
12:47:56.394 INFO [Main] Hello from main!
12:47:56.633 INFO [Renderer] Hello from renderer![!WARNING]
Timestamps reflect the time of the log entry in the originating process, not the time of the log entry in the receiving process, so timestamps in the main process' console might appear out of order.
Electron support is designed primarily for use with the default log instance — for now, every log instance in the renderer process automatically sends logs to every log instance in the main process, so you can quickly end up with duplicate logs if you have multiple log instances in the main process.
Libraries
If you're building a library that others will use, you can use Lognow with dependency injection to allow consumers to integrate with their own logging systems.
Step 1: Create a logger in your library
In your library project, create a simple log.ts utility file which creates a logger instance used throughout the library:
the-library/log.ts:
import type { ILogBasic, ILogLayer } from 'lognow'
import { createLogger, injectionHelper } from 'lognow'
/**
* The default logger instance for the module.
* Configure log settings here.
* Exported for use throughout the library.
*/
export let log = createLogger({ name: 'YourLibrary' })
/**
* Set the logger instance for the module.
* Export this for library consumers to inject their own logger.
* @param logger - Accepts either a LogLayer instance or a Console- or Stream-like log target
*/
export function setLogger(logger?: ILogBasic | ILogLayer) {
log = injectionHelper(logger)
}Then use this logger throughout your library:
the-library/index.ts:
import { log } from './log.js'
/**
* A function that uses the library's logger
*/
export function greet(name: string) {
log.info('Greeting user', name)
return `Hello, ${name}!`
}
// Export the setLogger function so consumers can inject their own logger
export { setLogger } from './log.js'Step 2: Inject a logger from the consuming application
In a different project that uses the library, you can inject a logger instance:
the-application.ts:
import { getChildLogger, log } from 'lognow'
import { greet, setLogger } from 'the-library'
// The library we've imported has its own lognow instance:
greet()
// In our application, we can use the default logger:
log.info('Hello from application!')
// We can create and attach a child logger to the default logger,
// and then inject it into the library to override its internal transports.
setLogger(getChildLogger(log, 'child'))
// Now the library logs run through the application's logger,
// with the chain of inheritance in the context object.
greet()If the library consumer doesn't want to use lognow, they can still inject a Console- or WritableStream-like logger instance into the library to receive basic messages without additional dependencies:
the-application.ts:
import { greet, setLogger } from 'the-library'
setLogger(console)
// Now the library's logs go straight to the passed `console` instance:
greet()Or, since LogLayer provides many additional transport adapters, it's easy for library consumers to integrate with their existing logging infrastructure of choice by defining a LogLayer instance to their liking:
the-application.ts:
import { PinoTransport } from '@loglayer/transport-pino'
import { LogLayer } from 'loglayer'
import { pino } from 'pino'
import { greet, setLogger } from 'the-library'
const pinoLogger = pino({
level: 'trace',
})
const log = new LogLayer({
transport: new PinoTransport({
logger: pinoLogger,
}),
})
setLogger(log)
// Now the library's logs are passed to the pino logger:
greet()
// Logs: "Hello from library!"Background
Implementation notes
Why free functions
Why do we have to use free functions to manipulate the default log instance instead of calling log.options() or something?
Lognow exposes a standard LogLayer instance so you can trivially ditch lognow without modifying any of your logging call sites.
This brings a slight compromise in discoverability: Additional configuration management and convenience functions must be provided via procedural-style free functions instead of extensions of the LogLayer class itself.
Pretty printing objects
In the browser, you can pass objects directly to the console, but logging to a stream requires object serialization, formatting, and colorization.
Many strategies were evaluated for pretty printing: node:util inspect, node-inspect-extracted, json-stringify-pretty-compact, pretty-format, stringify-object, loupe, object-inspect, and tslog.
I found that it's hard to improve on node's native inspect implementation for handling unusual object types. A universal port of inspect is used for serialization to stream targets in the browser environment.
Serializing complex objects for file logs
Many packages exist for serializing complex JavaScript objects into valid JSON, but few are equipped to handle esoteric data types and fewer still emphasize human readability of the output over being evaluable as JavaScript.
Human readability seems more important than perfect round-trip evaluation for the kind of context and metadata objects we're likely to want to log. (Of course the logs remain parsable, but complex objects like functions and circular references are rendered as strings.)
I found that safe-stable-stringify does a nice job of this in combination with the serialize-error package for Error object serialization.
Serializing complex objects for Electron IPC
Log objects must be serialized for transport between processes in Electron. Here, we do care about parsability, since the log object must be deserialized before it's passed into the main process' log instance. SuperJson, devalue, and next-json were evaluated.
Only next-json successfully round-tripped the nightmare object used in testing.
Maintainers
Acknowledgments
Thanks to Theo Gravity for developing LogLayer, and for responding to my questions so quickly and helpfully.
Contributing
Issues and pull requests are welcome.
License
MIT © Eric Mika
