Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

snooplogg

cb1kenobi10.5kMIT6.1.1TypeScript support: included

Your mind on your logs and your logs on your mind

log, logger, logging, logs, levels, debug, console, snoop, snooping

readme


SnoopLogg

SnoopLogg is a lightweight, zero dependency debug logging library for Node.js, Bun, and Deno. It is specifically designed for CLI programs, daemons, and libraries.

What separates SnoopLogg from other loggers is the ability to 'snoop' on other log instances. Instead of passing the logger around, create a single top-level logger, tell it to snoop, then create as many new loggers as you want and the top-level logger will relay them all.

Real talk. SnoopLogg is dope, but it's not the best solution for things such as logging HTTP server requests and web browser console is not supported (yet). For CLI apps and libaries, SnoopLogg is the shiz.

Features

  • Snoop on other SnoopLogg instances to aggregate log messages
  • Pipe log messages to one or more streams (such as a file or socket)
  • Namespaced and nested namespaced loggers
  • Filter messages by namespace or log level
  • Automatic namespace colorization
  • Custom log message formatting and styling
  • Pretty stack trace rendering
  • Support for object mode streams
  • Zero dependencies

Basic Logging

SnoopLogg provides 7 loggers. log() is the only one that doesn't print a label.

import { log, trace, debug, info, warn, error, panic } from 'snooplogg';

log('This is a log() message');
trace('This is a trace() message');
debug('This is a debug() message');
info('This is a info() message');
warn('This is a warn() message');
error('This is a error() message');
panic('This is a panic() message');

Basic Logging

By default, it prints the time from which the program has started, the log method, and the log message. The format is completely customizable.

Logging Values

SnoopLogg uses the built-in util.format() and util.inspect() to render values.

info('My name is %s and my favorite drink is %s', 'Snoop', 'juice');

debug({
  name: 'Snoop',
  occupation: 'Logger'
});

error(new Error('This is an error'));

Logging Values

Namespaces

The default export is a snooplogg instance that can be invoked as a function to create a namespaced child logger. You can have as many deeply nested namespaces as you'd like.

import snooplogg from 'snooplogg';

snooplogg.info('This is the default namespace');

const fooLogger = snooplogg('foo');
fooLogger.info('This is the foo namespace');

const barLogger = fooLogger('bar');
barLogger.info('This is the bar namespace');

const bazLogger = snooplogg('baz');
bazLogger.info('This is the baz namespace');

Namespaces

Namespace Filtering

By default, debug logging is suppressed. You must enable it by setting the SNOOPLOGG (or DEBUG) environment variable to the desired filter pattern.

$ SNOOPLOGG=* node myscript.js

Specifying * will display all log messages. The root log methods cannot be filtered, only enabled or disabled.

Multiple namespace filters can be specified as a comma separated list. Wildcards are supported. Prefix the filter with - (dash) to ignore the pattern.

const fooLogger = snooplogg('foo');
fooLogger.info('FOO!');

const barLogger = snooplogg('bar');
barLogger.info('BAR!');

const bazLogger = snooplogg('baz');
bazLogger.info('BAZ!');

Filtering

Note that SnoopLogg does not support "log levels". If you want to filter by log method, then you'll need to pipe SnoopLogg into an object mode transform stream that suppresses unwanted log messages, then pipe that into stderr, file, etc. See pipe() below.

Log Levels

The default log level is 'trace'. You can override it by setting the SNOOPLOGG_LEVEL environment variable to one of the following values:

'trace' | 'debug' | 'log' | 'info' | 'warn' | 'error' | 'panic'

When you run your program, you would do:

$ SNOOPLOGG=* SNOOPLOGG_LEVEL=info node myapp.js

You override the default log level when creating a new SnoopLogg instance:

import { SnoopLogg, LogLevels } from 'snooplogg';

const logger = new SnoopLogg({ logLevel: 'info' });

// then change it at runtime
logger.setLogLevel('warn');

// or change it using the LogLevel object
logger.setLogLevel(LogLevels.debug);

Note that the log level only affects the output, specifically of the log instance the level was set. It doesn't affect other instances that are snooping.

Snooping

SnoopLogg allows you to "snoop" or aggregrate log messages from other SnoopLogg instances.

For example, say you have an app and a library. The app writes all debug logs to disk. You wouldn't necessarily want the library writing debug logs to disk, so you can have the app debug logger "snoop" on the library's debug logger.

const app = new SnoopLogg().enable('*').pipe(process.stdout);
const lib = new SnoopLogg();

app('app').info('This is the app logger and it will snoop on all other loggers');
lib('lib').info('This is the lib logger, but nothing will be logged');
app.snoop();
lib('lib').info(`This is the lib logger and I\'m being snooped`);

Snoop

You can stop snooping by calling snooplogg.unsnoop().

Note: SnoopLogg defines a global snooplogg variable to pass messages between logger instances and the message format is a plain object. This means, in theory, SnoopLogg v6 or newer instances can snoop on any other SnoopLogg v6 or new instances. No need to worry about keeping the dependency versions in sync.

Programmatic Instantiation

You can create your own SnoopLogg instances instead of using the default one:

import { SnoopLogg } from 'snooplogg';

const myLogger = new SnoopLogg();
myLogger.enable('*');
myLogger.info('Yippee yo!');

const fooLogger = myLogger('foo');
fooLogger.log('Yippee yay!');

Should you need to, you can also check to see if a specific namespace is enabled by calling:

myLogger.isEnabled('foo');

Piping

You can pipe SnoopLogg into one or more writable-like streams such as a file:

const out = fs.createWriteStream('debug.log');
snooplogg.pipe(out, { /* snooplogg stream options */ });
snooplogg.info('This will be written to stderr and a file');

pipe() takes two parameters: the stream and options:

pipe(stream: WritableLike, options?: StreamOptions)

WritableLike is basically a minimum WritableStream that looks like this:

interface WritableLike {
  isTTY?: boolean;
  on: (...args: any[]) => any;
  removeListener: (...args: any[]) => any;
  writableObjectMode?: boolean;
  write: (...args: any[]) => any;
}

If the writable stream has objectMode property set to true, SnoopLogg will passthrough the raw message object.

The stream options allows you to override logging settings as well as force the stream to flush.

interface StreamOptions {
  colors?: boolean;
  elements?: LogElements;
  flush?: boolean;
  format?: LogFormatter;
}

If the history feature is enabled, then you can set the flush option to write all messages in the history to the new pipe:

snooplogg.pipe(out, { flush: true });

To stop piping to a stream:

snooplogg.unpipe(out);

Transform

You can pipe the messages to a transform stream to have complete control over each log message.

You can use Node.js' built-in Transform or you can roll your own.

The Custom Way

import { SnoopEmitter, type WritableLike } from 'snooplogg';

class MyTransform extends EventEmitter implements WritableLike {
  out: WritableLike; // or WritableStream

  constructor(out: WritableLike) {
    super();
    this.out = out;
  }

  write(msg) {
    this.out.write(`The message is ${msg}`);
  }
}

const myLogger = new SnoopLogg().enable('*').pipe(new MyTransform());
myLogger.info('Transform me!')

The Node.js Way

import { Transform } from 'node:stream';

class MyTransformer extends Transform {
  constructor(opts = {}) {
    opts.objectMode = true;
    super(opts);
  }

  _transform(msg, enc, cb) {
    if (msg && typeof msg === 'object' && !(msg instanceof Buffer)) {
      this.push(JSON.stringify(msg, null, 2));
    }
    cb();
  }
}

const out = new MyTransformer();
out.pipe(process.stdout);

const myLogger = new SnoopLogg().enable('*');
myLogger.pipe(out);
myLogger.info('Transform me!')

Transform

You can pipe the debug log to as many streams as you like, however each log message is formatted per stream. This could impact performance if you have a lot of log messages and several streams. Instead, consider piping SnoopLogg to a transform stream that in turn pipes to several streams:

import { Transform } from 'node:stream';

class Demuxer extends Transform {
  _transform(msg, enc, cb) {
    this.push(msg);
    cb();
  }
}

const demuxer = new Demuxer();
demuxer.pipe(process.stdout);
demuxer.pipe(myfile);

myLogger.pipe(demuxer);

History

SnoopLogg can buffer the previous log messages. By default, this is disabled. To enable it, set the history size to the desired value:

snooplogg.config({ historySize: 5 });

The code above will buffer the last 5 messages. To dump the history, you need to pipe SnoopLogg to a writable stream and set the flush: true flag.

for (let i = 1; i <= 10; i++) {
    snooplogg.info(`This is message ${i}`);
}

snooplogg.pipe(process.stdout, { flush: true });

History

This feature is specifically designed for daemons (e.g. servers) and takes inspiration from adb logcat.

For example, pretend you have a server running in the background. Now you want to see the debug log for when the server is initializes, but by the time you connect, those messages are in the past. Don't stress. Simply set the historySize to something reasonable, then implement a "logcat" route handler that pipes the SnoopLogg instance to the connection socket with flush: true set. SnoopLogg will automatically unpipe the stream when it is ended.

Note that setting a relative large history size may impact performance.

Config

SnoopLogg has a relatively simple configuration:

interface SnoopLoggConfig {
    colors?: boolean;
    elements?: LogElements;
    format?: LogFormatter | null;
    historySize?: number;
}

You can call the .config() function to change the default logger or pass the config into a new instance.

snooplogg.config({ historySize: 10 });
const myLogger = new SnoopLogg({ historySize: 10 });
myLogger.config({ historySize: 20 });

We'll discuss these settings in more detail below.

colors

Set this flag to false to to disable colors. Colors are enabled by default unless overwritten by the stream settings or the stream is not a TTY.

elements

You can pass in an object with overrides for any of the element specific renderers.

type FormatLogElements = {
    error: (err: Error, styles: StyleHelpers) => string;
    message: (msg: string, method: string, styles: StyleHelpers) => string;
    method: (name: string, styles: StyleHelpers) => string;
    namespace: (ns: string, styles: StyleHelpers) => string;
    timestamp: (ts: Date, styles: StyleHelpers) => string;
    uptime: (uptime: number, styles: StyleHelpers) => string;
};

Each element formatter is passed a styles object containing the contents of the ansi-styles package. It also includes the nsToRgb(string) function which deterministically finds a color based on the supplied string where the color is not too light or too dark.

snooplogg.config({
  elements: {
    namespace(ns, { color, nsToRgb, rgbToAnsi256 }) {
      const { r, g, b } = nsToRgb(ns);
      return `${color.ansi256(
        rgbToAnsi256(r, g, b)
      )}${ns}${color.close}`;
    }
  }
});

format()

A custom formatter that renders a log message.

snooplogg.info('This is the default format');

snooplogg.config({
  format(msg, styles) {
    const { args, colors, elements, method, ns, ts, uptime } = msg;
    return `${ts.toISOString()} [${method}] ${args.join(' ')}`;
  }
});

snooplogg.info('This is the custom format');

Format

historySize

The number of log messages to buffer. Defaults to 0. Pipe SnoopLogg to a writable stream with { flush: true } to see the buffered messages.

Demo

Pull the repo, pnpm i && pnpm build, then run:

SNOOPLOGG=* node demo/demo.js

# or

SNOOPLOGG=* bun demo/demo.js

License

MIT

changelog

v6.1.1 (October 22, 2025)

  • Fix typo... I can't spell

v6.1.0 (October 22, 2025)

  • Switch from Node.js EventEmitter to a lightweight browser-compatible version
  • Add support for log levels using SNOOPLOGG_LEVEL env var or logLevel option

v6.0.3 (October 19, 2025)

  • chore: Replace rollup with rolldown (tsdown)

v6.0.2 (October 14, 2025)

  • chore: Updated dependencies

v6.0.1 (June 24, 2025)

  • chore: Updated dependencies

v6.0.0 (July 16, 2024)

  • BREAKING CHANGE: Require Node.js 18.17.0 or newer
  • BREAKING CHANGE: Removed support for custom log methods
  • BREAKING CHANGE: Removed support for middleware
  • BREAKING CHANGE: Removed console creator
  • BREAKING CHANGE: Removed utility streams StripColors, Format, StdioStream, and StdioDispatcher
  • BREAKING CHANGE: Removed support for user-defined colors
  • BREAKING CHANGE: Removed support for inspect options
  • BREAKING CHANGE: Removed brightness settings
  • BREAKING CHANGE: Removed theme support; use new format()
  • BREAKING CHANGE: Removed style(); use new elements
  • BREAKING CHANGE: Removed the .stdio helper
  • BREAKING CHANGE: All logout is written to stderr
  • BREAKING CHANGE: Renamed maxBufferSize to historySize
  • BREAKING CHANGE: Removed createInstanceWithDefaults() helper
  • feat: Improved style and formatting system
  • feat: Reduced package size
  • feat: Support for stream specific formatting overrides
  • fix: Namespaces can no longer contain spaces, commas, or pipe characters

v5.1.0 (July 14, 2022)

  • fix: Re-export original chalk instance instead of instance with forced colors.
  • chore: Updated dependencies.

v5.0.0 (Mar 2, 2022)

  • BREAKING CHANGE: Require Node.js 14.15.0 LTS or newer.
  • fix: Always force colors even when not available. Use StripColors transform to remove color sequences when piping to files or non-TTY terminals.
  • chore: Updated dependencies.
  • chore: Replaced Travis with GitHub action.

v4.0.0 (Feb 24, 2021)

  • BREAKING CHANGE: Changed package to a ES module.
  • BREAKING CHANGE: Require Node.js 12 or newer.

3.0.2 (Jan 5, 2021)

  • chore: Updated dependencies.

3.0.1 (Nov 30, 2020)

  • chore: Updated dependencies.