npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

lognow

v0.4.0

Published

Quick and clean universal logging.

Downloads

181

Readme

lognow

NPM Package lognow License: MIT

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 lognow

Quick 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 Console object — it's a LogLayer ILogLayer instance, 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 entry
  • withContext() - 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 sent

Options

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.json will 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] hello

Note 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 output
  • FORCE_COLOR - Enables colorization (takes precedence over NO_COLOR if both are set)
  • DEBUG - Enables verbose logging (equivalent to verbose: 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.js

Examples

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

@kitschpatrol

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