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 🙏

© 2026 – Pkg Stats / Ryan Hefner

pipit

v2.0.1

Published

The universal logger with the pluggable architecture.

Readme

The universal logger with the pluggable architecture.

npm install --save-prod pipit

🔰 Usage

🧩 Built-in processors

🍪 Cookbook

Usage

You can start using Pipit as a replacement for console logging, no additional configuration is required:

import logger from 'pipit';

logger.log('Oh, snap!');

Logger uses channels to deliver messages. Each channel is a sequence of processors that may filter or enrich messages, print them to console or stdout, send them to a remote service, write them to a file, and do whatever you want, even send a push notification.

Let's create a new logger and configure it:

import { Logger } from 'pipit';
import writeToConsole from 'pipit/processor/writeToConsole';

const myLogger = new Logger();

// Open a channel that writes a message to the console
myLogger.addChannel(writeToConsole());

myLogger.log('Oh, snap!');

Logging levels

Messages can be logged with different severity levels:

myLogger.fatal('A very severe error events that will presumably lead the application to abort');

myLogger.error('An error event that might still allow the application to continue running');

myLogger.warn('Potentially harmful situation');

myLogger.info('Highlight the progress of the application at coarse-grained level');
// or myLogger.log(…)

myLogger.debug('Useful to debug an application');

myLogger.trace('A finer-grained informational message than debug, usually with a stack trace');

By default, Logger sends all messages to channels, but you can set a minimum required level of the message severity:

import { Logger, Level } from 'pipit';

// Log messages that have an error severity level or higher
const myLogger = new Logger(Level.ERROR);

// This message is ignored
myLogger.debug('Hello there');

// This message is logged
myLogger.fatal('Damn!');

Channels

You can open as many channels on a single logger as you need:

import { Logger, Level } from 'pipit';
import sendToSentry from 'src/main/plugin/sentry';
import levelCutoff from 'pipit/processor/levelCutoff';
import writeToConsole from 'pipit/processor/writeToConsole';

const myLogger = new Logger();

myLogger.addChannel(writeToConsole());

myLogger.addChannel(levelCutoff(Level.ERROR), sendToSentry());

myLogger.log('Good job!');

myLogger.fatal('A severe error has occurred!');

The first message in the example above would be printed to the console, while the second one is printed to console and sent to Sentry as well.

You can remove all channels using reset. This is especially useful if you want to re-configure the default global logger.

import logger from 'pipit';
import sendToSentry from 'pipit/processor/sendToSentry';

// Send all messages to Sentry
logger.reset().addChannel(sendToSentry());

You can also reset the logging level:

logger.reset(Level.WARN);

Processors

Logger channels are sequences of processors. Processor is a callback that receives an array of messages and performs arbitrary operations on those messages. When processor has completed its job, it can pass messages to the next processor in the channel.

To showcase how processors work, let's create a basic processor that prepends a timestamp to each logged message:

import { type LogProcessor } from 'pipit';

const myLogProcessor: LogProcessor = logger => (messages, next) => {
  for (const message of messages) {
    message.args.unshift(new Date(message.timestamp).toISOString());
  }

  next(messages);
};

Now let's use this processor to write message with timestamp to console:

import { Logger } from 'pipit';
import writeToConsole from 'pipit/processor/writeToConsole';

const myLogger = new Logger();

myLogger.addChannel(myLogProcessor, writeToConsole());

myLogger.log('Okay, cowboy');
// ⮕ '2022-11-25T16:59:44.286Z Okay, cowboy'

Context

Provide a context to a logger:

const myLogger = new Logger(Level.INFO, { hello: 'world' });

Each message contains a structured clone of the context that was captured the moment the message is logged.

Processors may use on the context to change their behavior or enhance logging. Here's a processor that does structured JSON logging:

import { type LogProcessor } from 'pipit';

const jsonLogProcessor: LogProcessor = logger => (messages, next) => {
  for (const message of messages) {
    message.args = [JSON.stringify({ ...message.context, text: message.args[0] })];
  }

  next(messages);
};

This processor would convert messages to JSON and pass to the next processor. Let's now use it to write JSON messages to console:

import { Logger } from 'pipit';
import writeToConsole from 'pipit/processor/writeToConsole';

const myLogger = new Logger({ hello: 'world' });

myLogger.addChannel(jsonLogProcessor, writeToConsole());

myLogger.debug('Okay, cowboy');
// ⮕ '{"hello":"world","text":"Okay, cowboy"}'

Events

Logger may publish events to which subscribers can react.

const myLogger = new Logger();

myLogger.subscribe(event => {
  // Handle an event here
});

myLogger.publish({ type: 'okay' });

Usually you subscribe to a logger events inside your processor:

import { Logger, type LogProcessor } from 'pipit';

const myLogProcessor: LogProcessor = logger => {
  logger.subscribe(event => {
    // Handle an event here
  });

  return (messages, next) => next(messages);
};

const myLogger = new Logger();

myLogger.addChannel(myLogProcessor);

Built-in processors

batchMessages

Batches messages using a timeout and/or limit strategy.

import batchMessages from 'pipit/processor/batchMessages';
import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(batchMessages({ timeout: 1_000, limit: 2 }), writeToConsole());

myLogger.log('No way');
// Does nothing, since not enough messages to dispatch

myLogger.log('Yay');
// ⮕ 'No way'
// ⮕ 'Yay'

By default, at most 50 messages are batched in the 100 msec timeframe. You can provide both the timeout and limit options at the same time and when any constraint is hit, then batched messages are sent to the next processor.

levelCutoff

Excludes messages that have an insufficient severity level.

import { Level } from 'pipit';
import levelCutoff from 'pipit/processor/levelCutoff';
import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(levelCutoff(Level.WARN), writeToConsole());

myLogger.info('Something happened');
// Does nothing, since level of this message is INFO

myLogger.fatal('The base is under attack');
// Prints the message, since its level is FATAL

This processor comes handy if you have multiple channels in your logger and want some of them to be used only if message is severe enough.

prependArgs

Prepends a set args to each message.

import prependArgs from 'pipit/processor/prependArgs';
import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(prependArgs('Hello,'), writeToConsole());

myLogger.log('Boss');
// ⮕ 'Hello, Boss'

prependLevel

Prepends severity level label to each message.

import prependLevel from 'pipit/processor/prependLevel';
import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(prependLevel(), writeToConsole());

myLogger.fatal('No way!');
// ⮕ 'FATAL No way

Colorize the level label:

myLogger.addChannel(prependLevel({ isColorized: true }), writeToConsole());

myLogger.fatal('No way!');
// ⮕ '\x1b[7m FATAL \x1b[27m No way

prependTimestamp

Prepends date and time in ISO format to each message.

import prependTimestamp from 'pipit/processor/prependTimestamp';
import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(prependTimestamp(), writeToConsole());

myLogger.log('Okay, cowboy');
// ⮕ '2022-11-25 16:59:44.286 Okay, cowboy'

Omit date and/or milliseconds for better readability:

myLogger.addChannel(prependTimestamp({ noDate: true, noMilliseconds: true }), writeToConsole());

myLogger.log('Okay, cowboy');
// ⮕ '16:59:44 Okay, cowboy'

sendToSentry

Sends a message to Sentry.

import sendToSentry from 'pipit/processor/sendToSentry';
import * as Sentry from '@sentry/browser';

myLogger.addChannel(sendToSentry(Sentry));

myLogger.log('To the moon!');
// Sends message to Sentry, no outher output

Logger context is sent to Sentry:

const myLogger = new Logger(Level.ERROR, { origin: 'Earth' });

myLogger.addChannel(sendToSentry(Sentry));

myLogger.log('To the moon!');
// Sends message to Sentry with context {"origin":"Earth"}

stringifyAsJSON

Replaces message arguments with a JSON-stringified value of the first argument.

import stringifyAsJSON from 'pipit/processor/stringifyAsJSON';
import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(stringifyAsJSON(), writeToConsole());

myLogger.log({ hello: 'world' });
// ⮕ {"hello":"world"}

structurizeArgs

Squashes message arguments into an object.

import structurizeArgs from 'pipit/processor/structurizeArgs';
import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(structurizeArgs(), writeToConsole());

myLogger.log('To the moon!');
// ⮕ { timestamp: 1767277876893, level: 'info', message: 'To the moon!'}

For structured logging, use structurizeArgs in conjunction with stringifyAsJSON:

import structurizeArgs from 'pipit/processor/structurizeArgs';
import stringifyAsJSON from 'pipit/processor/stringifyAsJSON';
import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(structurizeArgs(), stringifyAsJSON(), writeToConsole());

myLogger.log('To the moon!');
// ⮕ '{"timestamp":"2026-01-01T14:31:16.893Z","level":"info","message":"To the moon!"}'

Logger context is squashed with the message payload:

const myLogger = new Logger(Level.ERROR, { origin: 'Earth' });

myLogger.addChannel(structurizeArgs(), writeToConsole());

myLogger.log('To the moon!');
// ⮕ { timestamp: 1767277876893, level: 'info', origin: 'Earth', message: 'To the moon!'}

transformArgs

Transforms message arguments before passing it to the next processor.

import transformArgs from 'pipit/processor/transformArgs';
import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(
  transformArgs(message => ['Hello, ', ...message.args]),
  writeToConsole()
);

myLogger.log('Bob');
// ⮕ 'Hello, Bob'

writeToConsole

Prints messages to the console.

import writeToConsole from 'pipit/processor/writeToConsole';

myLogger.addChannel(writeToConsole());

myLogger.log('Okay');
// ⮕ 'Okay'

Cookbook

Multi-channel structured logging

Write structured JSON messages to the console and send errors to Sentry as well.

import * as Sentry from '@sentry/browser';
import logger, { Level } from 'pipit';
import structurizeArgs from 'pipit/processor/structurizeArgs';
import stringifyAsJSON from 'pipit/processor/stringifyAsJSON';
import writeToConsole from 'pipit/processor/writeToConsole';
import levelCutoff from 'pipit/processor/levelCutoff';
import sendToSentry from 'pipit/processor/sendToSentry';

// Discard the default console logging config
logger.reset();

// Structured console logging
logger.addChannel(structurizeArgs(), stringifyAsJSON(), writeToConsole());

// Send errors to Sentry
logger.addChannel(levelCutoff(Level.ERROR), sendToSentry(Sentry));

Now errors are written to console and sent to Sentry:

logger.error('Ooops!');

And messages with lower severity are written to console only:

logger.info('Good job!');

Logging computation-intensive values

If logging a value requires significant processing, it is recommended to first check whether the logger is set to the appropriate logging level:

import logger from 'pipit';

if (logger.isInfoEnabled) {
  // Obtain a heavily computed value
  const myHeavyValue = 'Hello';

  logger.info(myHeavyValue);
}