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

@visulima/pail

v6.0.0

Published

Highly configurable Logger for Node.js, Edge and Browser.

Readme

typescript-image mit licence npm downloads Chat PRs Welcome



📋 Migration Guide

If you're upgrading from an earlier version of pail, check out our Migration Guide for breaking changes and upgrade instructions.


Why Pail?

  • Easy to use
  • Hackable to the core
  • Integrated timers
  • Custom pluggable processors and reporters
  • TypeScript support
  • Interactive and regular modes
  • Secrets & sensitive information filtering
  • Filename, date and timestamp support
  • Scoped loggers and timers
  • Scaled logging levels mechanism
  • String interpolation support
  • Object and error interpolation
  • Stack trace and pretty errors
  • Simple and minimal syntax
  • Spam prevention by throttling logs
  • Browser and Server support
  • Redirect console and stdout/stderr to pail and easily restore redirect.
  • Pretty or JSON output
  • ESM‑only with tree‑shaking support (Node.js ≥ 20.19)
  • Supports circular structures
  • Fast and powerful, see the benchmarks
  • Wide events — accumulate context across an operation and emit once
  • Framework middleware for Express, Fastify, Hono, Elysia, SvelteKit, and Next.js

Install

npm install @visulima/pail
yarn add @visulima/pail
pnpm add @visulima/pail

Concepts

Most importantly, pail adheres to the log levels defined in [RFC 5424][rfc-5424] extended with trace level. This means that you can use the log levels to filter out messages that are not important to you.

Log Levels

Pail supports the logging levels described by [RFC 5424][rfc-5424].

  • DEBUG: Detailed debug information.

  • INFO: Interesting events. Examples: User logs in, SQL logs.

  • NOTICE: Normal but significant events.

  • TRACE: Very detailed and fine-grained informational events.

  • WARNING: Exceptional occurrences that are not errors. Examples: Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong.

  • ERROR: Runtime errors that do not require immediate action but should typically be logged and monitored.

  • CRITICAL: Critical conditions. Example: Application component unavailable, unexpected exception.

  • ALERT: Action must be taken immediately. Example: Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up.

  • EMERGENCY: Emergency: system is unusable.

Bypass Log Level Filter with Force

Normally, the logger will only output messages at or above the configured level.

However, you can force a log to be emitted regardless of the current level:

import { createPail } from "@visulima/pail";

const logger = createPail({
    logLevel: "warn", // Only show warning and above
});

logger.info("This won't be logged"); // Filtered out

logger.force.error("Something went wrong!");
logger.force.info("This will show even if level is set to 'warn'");

⚠️ Use this with care—force is designed for exceptional situations where logs must be guaranteed to appear.

Reporters

Reporters are responsible for writing the log messages to the console or a file. pail comes with a few built-in reporters:

| Browser (console.{function}) | Server (stdout or stderr) | | ---------------------------- | ------------------------- | | JsonReporter | JsonReporter | | PrettyReporter | PrettyReporter | | HttpReporter | HttpReporter | | x | SimpleReporter | | x | FileReporter |

Processors

Processors are responsible for processing the log message (Meta Object) before it's written to the console or a file. This usually means that they add some metadata to the record's context property.

A processor can be added to a logger directly (and is subsequently applied to log records before they reach any handler).

pail comes with a few built-in processors:

  • CallerProcessor - adds the caller information to the log message
    • The Meta Object is extended with a file name, line number and column number
  • RedactProcessor - redacts sensitive information from the log message

    The redact processor needs the "@visulima/redact" package to work. Use npm install @visulima/redact, pnpm add @visulima/redact or yarn add @visulima/redact to install it.

  • MessageFormatterProcessor - formats the log message (Util.format-like unescaped string formatting utility) [@visulima/fmt][fmt]
  • ErrorProcessor - serializes the error with cause object to a std error object that can be serialized.
  • OpenTelemetryProcessor - adds OpenTelemetry trace context to log metadata

    The OpenTelemetry processor needs the "@opentelemetry/api" package to work. Use npm install @opentelemetry/api, pnpm add @opentelemetry/api or yarn add @opentelemetry/api to install it. Extracts trace ID, span ID, and trace flags from the active OpenTelemetry span and adds them to the log context for distributed tracing correlation.

Usage

import { pail } from "@visulima/pail";

pail.success("Operation successful");
pail.debug("Hello", "from", "L59");
pail.pending("Write release notes for %s", "1.2.0");
pail.fatal(new Error("Unable to acquire lock"));
pail.watch("Recursively watching build directory...");
pail.complete({
    prefix: "[task]",
    message: "Fix issue #59",
    suffix: "(@prisis)",
});

usage

Which build @visulima/pail resolves to

The . entry point picks a build from the export conditions your runtime or bundler sets:

| Runtime | Condition matched | Build | Default reporter | | ------------------------------------------- | ------------------------ | ----------------------- | ---------------------- | | Node.js, Bun (Node compat) | import | dist/index.server.js | Pretty (stdout/stderr) | | Browsers and bundlers targeting the browser | browser | dist/index.browser.js | JSON (console) | | Cloudflare Workers, Vercel Edge | workerd / edge-light | dist/index.browser.js | JSON (console) |

The server build writes to process.stdout / process.stderr and measures the terminal, so it needs Node's core modules. Edge runtimes get the console-based build instead, which keeps . importable on Cloudflare Workers without the nodejs_compat flag and on Vercel Edge.

Importing @visulima/pail/server always gives you the server build, on every runtime — use it only where Node core is available (on Cloudflare Workers that means enabling nodejs_compat).

Custom Loggers

To create a custom logger define an options object yielding a types field with the logger data and pass it as argument to the createPail function.

import { createPail } from "@visulima/pail";

const custom = createPail({
    types: {
        remind: {
            badge: "**",
            color: "yellow",
            label: "reminder",
            logLevel: "info",
        },
        santa: {
            badge: "🎅",
            color: "red",
            label: "santa",
            logLevel: "info",
        },
    },
});

custom.remind("Improve documentation.");
custom.santa("Hoho! You have an unused variable on L45.");

custom-types

Here is an example where we override the default error and success loggers.

import { pail, createPail } from "@visulima/pail";

pail.error("Default Error Log");
pail.success("Default Success Log");

const custom = createPail({
    scope: "custom",
    types: {
        error: {
            badge: "!!",
            label: "fatal error",
        },
        success: {
            badge: "++",
            label: "huge success",
        },
    },
});

custom.error("Custom Error Log");
custom.success("Custom Success Log");

override default types

Scoped Loggers

To create a scoped logger from scratch, define the scope field inside the options object and pass it as argument to the createPail function.

import { createPail } from "@visulima/pail";

const mayAppLogger = createPail({
    scope: "my-app",
});

mayAppLogger.info("Hello from my app");

simple scope

To create a scoped logger based on an already existing one, use the scope() function, which will return a new pail instance, inheriting all custom loggers, timers, secrets, streams, configuration, log level, interactive mode & disabled statuses from the initial one.

import { pail } from "@visulima/pail";

const global = pail.scope("global scope");

global.success("Hello from the global scope");

function foo() {
    const outer = global.scope("outer", "scope");
    outer.success("Hello from the outer scope");

    setTimeout(() => {
        const inner = outer.scope("inner", "scope");
        inner.success("Hello from the inner scope");
    }, 500);
}

foo();

extended scope

Child Loggers

Create child loggers that inherit settings from their parent while overriding only what you need. Child loggers are independent instances with their own state (timers, counters, etc.), but they inherit configuration like reporters, processors, types, log levels, and throttle settings.

import { createPail } from "@visulima/pail";
import { PrettyReporter } from "@visulima/pail/reporter/pretty";

const parent = createPail({
    logLevel: "informational",
    types: {
        http: {
            label: "HTTP",
            logLevel: "informational",
        },
    },
    reporters: [new PrettyReporter()],
});

// Child inherits all parent settings
const child = parent.child();
child.http("GET /api/users 200"); // Inherits http type from parent
child.info("Request processed"); // Inherits log level from parent

// Child can override specific settings
const debugChild = parent.child({ logLevel: "debug" });
debugChild.debug("Detailed debug info"); // Uses debug level

// Child can add new types
const dbChild = parent.child({
    types: {
        db: {
            label: "DB",
            logLevel: "informational",
        },
    },
});
dbChild.db("Query executed"); // New type available
dbChild.http("GET /api 200"); // Still has parent types

// Child scope extends parent scope
const scopedChild = parent.child({ scope: ["api"] });
// Logs will include both parent and child scope if parent had scope set

What Gets Inherited

  • Types: Child types are merged with parent types (child can add new or override existing)
  • Reporters: Child reporters are added to parent reporters (both are used)
  • Processors: Child processors are added to parent processors (both are applied)
  • Log Levels: Child log levels override parent log levels
  • Scope: Child scope extends parent scope (combined into array)
  • Throttle Settings: Child inherits parent throttle settings unless overridden
  • Timer Messages: Child inherits parent timer messages unless overridden

What's Independent

  • State: Each child logger has its own timers, counters, and message queue
  • Disabled/Paused: Child logger state is independent from parent

Interactive Loggers (Only on if stdout and stderr is a TTY)

To initialize an interactive logger, create a new pail instance with the interactive attribute set to true. While into the interactive mode, previously logged messages originating from an interactive logger, will be overridden only by new ones originating from the same or a different interactive logger. Note that regular messages originating from regular loggers are not overridden by the interactive ones.

import { createPail } from "@visulima/pail";

console.log("\n");

const pail = createPail();

const interactive = createPail({ interactive: true });

pail.info("This is a log message 1");

setTimeout(() => {
    interactive.await("[%d/4] - Process A", 1);
    setTimeout(() => {
        interactive.success("[%d/4] - Process A", 2);
        setTimeout(() => {
            interactive.await("[%d/4] - Process B", 3);
            setTimeout(() => {
                interactive.error("[%d/4] - Process B", 4);
            }, 1000);
        }, 1000);
    }, 1000);
});

pail.info("This is a log message 2");
pail.info("This is a log message 3");
pail.info("This is a log message 4");

For a more complex example, use can use the getInteractiveManager function, see the following code:

import { createPail } from "@visulima/pail";

const interactive = createPail({ interactive: true });

const TICKS = 60;
const TIMEOUT = 80;
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const messages = ["Swapping time and space...", "Have a good day.", "Don't panic...", "Updating Updater...", "42"];
let ticks = TICKS;
let i = 0;

const interactiveManager = interactive.getInteractiveManager();

interactiveManager.hook();

// eslint-disable-next-line no-console
console.log(" - log message");
// eslint-disable-next-line no-console
console.error(" - error message");
// eslint-disable-next-line no-console
console.warn(" - warn message");

const id = setInterval(() => {
    if (--ticks < 0) {
        clearInterval(id);

        interactiveManager.update("stdout", ["✔ Success", "", "Messages:", "this line will be deleted!!!"]);
        interactiveManager.erase("stdout", 1);
        interactiveManager.unhook(false);
    } else {
        const frame = frames[(i = ++i % frames.length)];
        const index = Math.round(ticks / 10) % messages.length;
        const message = messages[index];

        if (message) {
            interactiveManager.update("stdout", [`${frame} Some process...`, message]);
        }
    }
}, TIMEOUT);

Timers

Timer are managed by the time(), timeLog() and timeEnd() functions. A unique label can be used to identify a timer on initialization, though if none is provided the timer will be assigned one automatically. In addition, calling the timeEnd() function without a specified label will have as effect the termination of the most recently initialized timer, that was created without providing a label.

import { pail } from "@visulima/pail";

pail.time("test");
pail.time();
pail.timeLog("default", "Hello");

setTimeout(() => {
    pail.timeEnd();
    pail.timeEnd("test");
}, 500);

timers

Its also possible to change the text inside time() and timeEnd() by using the options object.

import { createPail } from "@visulima/pail";

const pail = createPail({
    messages: {
        timerStart: "Start Timer:",
        timerEnd: "End Timer:",
    },
});

pail.time("test");
pail.timeEnd("test");

Progress Bars (Server Only)

Pail includes a comprehensive progress bar system inspired by cli-progress, with support for single and multi-bar modes, various styles, and interactive terminal output.

Single Progress Bar

import { createPail } from "@visulima/pail";

const logger = createPail({ interactive: true });
const bar = logger.createProgressBar({
    total: 100,
    format: "Downloading [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}",
});

bar.start();
bar.update(50);
bar.increment(10);
bar.stop();

Styled Progress Bars

Pail supports various built-in progress bar styles:

import { createPail } from "@visulima/pail";

const logger = createPail({ interactive: true });

// Shades classic (default)
const bar1 = logger.createProgressBar({
    total: 100,
    style: "shades_classic",
    format: "Progress [{bar}] {percentage}%",
});

// Shades grey
const bar2 = logger.createProgressBar({
    total: 100,
    style: "shades_grey",
    format: "Progress [{bar}] {percentage}%",
});

// Rect style
const bar3 = logger.createProgressBar({
    total: 100,
    style: "rect",
    format: "Progress [{bar}] {percentage}%",
});

// ASCII style
const bar4 = logger.createProgressBar({
    total: 100,
    style: "ascii",
    format: "Progress [{bar}] {percentage}%",
});

// You can still override individual style settings
const bar5 = logger.createProgressBar({
    total: 100,
    style: "shades_classic",
    barCompleteChar: "🚀", // Override the complete character
    format: "Progress [{bar}] {percentage}%",
});

Available styles: shades_classic, shades_grey, rect, filled, solid, ascii, custom

Multi Progress Bars

Display multiple progress bars simultaneously:

import { createPail } from "@visulima/pail";

const logger = createPail({ interactive: true });
const multiBar = logger.createMultiProgressBar({
    style: "shades_classic", // Apply style to all bars
});

const bar1 = multiBar.create(100);
const bar2 = multiBar.create(200);
const bar3 = multiBar.create(150);

// Update bars as needed
bar1.update(50);
bar2.update(75);
bar3.update(25);

// Clean up when done
multiBar.stop();

Custom Progress Bar

Create fully customized progress bars with your own characters and formatting:

import { createPail } from "@visulima/pail";

const logger = createPail({ interactive: true });
const bar = logger.createProgressBar({
    total: 100,
    format: "🚀 Downloading {filename}: [{bar}] {percentage}% | Speed: {speed} MB/s | ETA: {eta}s",
    barCompleteChar: "🚀",
    barIncompleteChar: "⚪",
    width: 20,
});

bar.start(0, 0, {
    filename: "large-file.zip",
    speed: "0.0",
});

// Update with payload data
bar.update(50, { speed: "2.5" });
bar.stop();

Gradient Character Arrays

Create smooth gradient animations by providing arrays of characters that progressively fill:

import { createPail } from "@visulima/pail";

const logger = createPail({ interactive: true });

// Shade gradient (light to dark)
const bar = logger.createProgressBar({
    total: 100,
    barCompleteChar: ["░", "▒", "▓", "█"], // Gradient array
    barIncompleteChar: " ",
    format: "Downloading [{bar}] {percentage}%",
});

bar.start();
for (let i = 0; i <= 100; i++) {
    bar.update(i);
}
bar.stop();

Supported gradients:

  • Shades: ["░", "▒", "▓", "█"] (light to dark)
  • Blocks: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] (small to large)
  • Temperature: ["🔵", "🟢", "🟡", "🟠", "🔴"] (cold to hot)
  • Custom: Any array of characters for your own gradient effect

Composite Progress Bars

Track multiple related progress bars simultaneously with automatic color layering based on progress percentage. This is perfect for operations with multiple parallel sources or stages:

import { createPail } from "@visulima/pail";
import colorize from "@visulima/colorize";

const logger = createPail({ interactive: true });

// Create a composite multi-bar that displays all bars as a single layered composite
const multiBar = logger.createMultiProgressBar({
    composite: true, // Enable composite mode
    format: "[{bar}]  ① {r}%  ② {y}%  ③ {b}%",
});

const source1 = multiBar.create(100, 0, { r: "0", y: "0", b: "0" });
const source2 = multiBar.create(100, 0, { r: "0", y: "0", b: "0" });
const source3 = multiBar.create(100, 0, { r: "0", y: "0", b: "0" });

// Apply colors to each source
multiBar.setBarColor(source1, colorize.red);
multiBar.setBarColor(source2, colorize.yellow);
multiBar.setBarColor(source3, colorize.blue);

// Update sources with different speeds
for (let i = 0; i <= 100; i++) {
    source1.update(i); // Red: 100% progress
    source2.update(Math.floor(i * 0.7)); // Yellow: 70% progress
    source3.update(Math.floor(i * 0.4)); // Blue: 40% progress
    await new Promise((r) => setTimeout(r, 40));
}

multiBar.stop();

Output example:

[▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓██████████████████████]  ① 100%  ② 70%  ③ 40%

Color Layering & Character Shading

The composite bar uses character shading to represent overlapping progress bars:

| Character | Meaning | Example | | ---------------- | ------------------ | ------------------- | | █ (solid) | One bar visible | Red at 100% | | ▓ (medium) | Two bars overlap | Red + Yellow | | ▒ (light) | Three bars overlap | Red + Yellow + Blue | | ░ (lightest) | Four+ bars overlap | All bars present |

How It Works

The composite bar renders based on progress percentage at each position:

  1. At position 0-40%: Only Red (①) is filled → shows █ with red color
  2. At position 40-70%: Red + Yellow (①+②) filled → shows ▓ with yellow color (highest index shown)
  3. At position 70-100%: All three (①+②+③) filled → shows ▒ with blue color (highest index)

The highest-indexed bar's color is used at each position, creating a natural progression.

Dynamic Visibility Based on Progress:

The bar with the smallest progress percentage is shown on top for maximum visibility:

  • Red at 30%, Yellow at 60% → Red is visible first (30% < 60%)
  • When Red reaches 30% and Yellow is only at 20% → Yellow becomes visible (20% < 30%)
  • This ensures slower-progressing operations are always visible and not hidden beneath faster ones

Use Cases

  1. Multi-source Download: Show progress from multiple download sources
  2. Parallel Tasks: Monitor concurrent operations (build, test, lint)
  3. Pipeline Stages: Upload → Process → Finalize with different completion times
  4. Batch Operations: Parse → Validate → Compile at different speeds
  5. Resource Tracking: CPU, Memory, Disk usage in parallel

Each progress bar:

  • Updates independently at its own speed
  • Can have different total values and progress rates
  • Uses color to distinguish sources
  • Shows through character shading when overlapping

Spinners (Server Only)

Pail includes a comprehensive spinner system inspired by cli-progress, with support for single and multi-spinner modes, various styles, and interactive terminal output.

Single Spinner

import { createPail } from "@visulima/pail";

const logger = createPail({ interactive: true });
const spinner = logger.createSpinner({
    text: "Loading...",
    color: "blue",
});

spinner.start();
spinner.succeed("Loaded!");
spinner.fail("Failed!");
spinner.stop();

Multi Spinner

import { createPail } from "@visulima/pail";

const logger = createPail({ interactive: true });
const multiSpinner = logger.createMultiSpinner({
    style: "dots", // Apply style to all spinners
});

const spinner1 = multiSpinner.create("Loading A");
const spinner2 = multiSpinner.create("Loading B");
const spinner3 = multiSpinner.create("Loading C");

// Update spinners as needed
spinner1.succeed("Loaded A!");
spinner2.succeed("Loaded B!");
spinner3.succeed("Loaded C!");

// Clean up when done
multiSpinner.stop();

Custom Spinner

Create fully customized spinners with your own characters and formatting:

import { createPail } from "@visulima/pail";

const logger = createPail({ interactive: true });
const spinner = logger.createSpinner({
    text: "🚀 Downloading {filename}: [{bar}] {percentage}% | Speed: {speed} MB/s | ETA: {eta}s",
    barCompleteChar: "🚀",
    barIncompleteChar: "⚪",
    width: 20,
});

spinner.start(0, 0, {
    filename: "large-file.zip",
    speed: "0.0",
});

// Update with payload data
spinner.update(50, { speed: "2.5" });
spinner.succeed();

Object Tree

Render objects and data structures as formatted ASCII trees for better terminal visualization and debugging:

import { renderObjectTree } from "@visulima/pail/object-tree";

const data = {
    user: {
        name: "John Doe",
        email: "[email protected]",
        profile: {
            age: 30,
            location: "New York",
            skills: ["JavaScript", "TypeScript", "Node.js"],
        },
    },
    settings: {
        theme: "dark",
        notifications: true,
    },
};

console.log(renderObjectTree(data));

Output:

├─ user:
│  ├─ name: John Doe
│  ├─ email: [email protected]
│  └─ profile:
│     ├─ age: 30
│     ├─ location: New York
│     └─ skills:
├─ settings:
│  ├─ theme: dark
│  └─ notifications: true

Note: renderObjectTree is exported as a separate module (@visulima/pail/object-tree) to reduce main bundle size. It works in both Node.js and Browser environments and is a pure utility function with no platform-specific dependencies.

Custom Rendering

Customize how object trees are rendered:

import { renderObjectTree } from "@visulima/pail/object-tree";

const obj = {
    name: "John",
    age: 30,
    address: {
        street: "Main St",
        city: "New York",
    },
};

// Custom rendering with sorting and formatting
const tree = renderObjectTree(obj, {
    sortFn: (a, b) => a.localeCompare(b), // Sort keys alphabetically
    renderFn: (node) => {
        if (typeof node === "string") return node.toUpperCase();
        return ["boolean", "string", "number"].includes(typeof node) ? String(node) : undefined;
    },
    joined: true, // Return as single string (false for array of lines)
});

console.log(tree);

Configuration Options

| Option | Description | Default | | ------------------- | ----------------------------------------------------- | -------------------- | | joined | Return as single string or array of lines | true | | sortFn | Function to sort object keys (null for natural order) | null | | renderFn | Function to render node values | Renders primitives | | separator | Separator between key and value | ": " | | keyNeighbour | Connector for keys with siblings | "├─ " | | keyNoNeighbour | Connector for last keys | "└─ " | | spacerNeighbour | Spacer for branches with siblings | "│ " | | spacerNoNeighbour | Spacer for branches without siblings | " " | | breakCircularWith | Text for circular references | " (circular ref.)" |

Use Cases

  1. Debugging: Visualize complex object structures during development
  2. Logging: Pretty-print objects for better readability in logs
  3. API Responses: Display JSON API responses in tree format
  4. Configuration Display: Show configuration trees in terminal applications
  5. Data Inspection: Inspect deeply nested data structures

HTTP Reporter

The HTTP Reporter sends logs to HTTP endpoints, making it perfect for centralized logging services, log aggregation platforms, or custom logging APIs. It supports batching, compression, retries, rate limiting, and Edge Runtime compatibility.

Note: When used in Edge Runtime environments (Next.js Edge, Cloudflare Workers, etc.), the package automatically resolves to an Edge-compatible version that disables compression and uses Edge-compatible APIs. You don't need to import a separate class - just use HttpReporter and the package handles the routing.

Basic Usage

import { createPail } from "@visulima/pail";
import { HttpReporter } from "@visulima/pail/reporter/http";

const logger = createPail({
    reporters: [
        new HttpReporter({
            url: "https://api.example.com/logs",
            method: "POST",
            headers: {
                Authorization: "Bearer your-token",
            },
        }),
    ],
});

logger.info("Application started", { version: "1.0.0" });

Edge Runtime Compatibility

For Edge Runtime environments, enable Edge compatibility mode explicitly (or let the package auto-detect):

import { createPail } from "@visulima/pail";
import { HttpReporter } from "@visulima/pail/reporter/http";

const logger = createPail({
    reporters: [
        new HttpReporter({
            url: "https://api.example.com/logs",
            edgeCompat: true, // Enable Edge compatibility (auto-enabled in Edge environments)
            headers: {
                Authorization: "Bearer your-token",
            },
        }),
    ],
});

logger.info("Edge function executed");

When edgeCompat is enabled (or automatically enabled in Edge environments), compression is disabled and Edge-compatible APIs are used.

Batching

HTTP Reporter supports automatic batching to reduce network requests:

const logger = createPail({
    reporters: [
        new HttpReporter({
            url: "https://api.example.com/logs",
            enableBatchSend: true,
            batchSize: 100, // Send when 100 logs are queued
            batchSendTimeout: 5000, // Or send after 5 seconds
            batchMode: "delimiter", // Options: "delimiter", "array", "field"
            batchSendDelimiter: "\n", // Delimiter for batch entries
        }),
    ],
});

Batch Modes:

  • delimiter (default): Join entries with a delimiter (e.g., newline-delimited JSON)
  • array: Send entries as a JSON array
  • field: Wrap entries in an object with a field name (requires batchFieldName)
// Field mode example (e.g., for Logflare)
new HttpReporter({
    url: "https://api.logflare.app/logs",
    batchMode: "field",
    batchFieldName: "batch", // Wraps entries in { batch: [...] }
});

Compression

Enable gzip compression to reduce payload size:

const logger = createPail({
    reporters: [
        new HttpReporter({
            url: "https://api.example.com/logs",
            compression: true, // Enable gzip compression
        }),
    ],
});

Note: Compression is automatically disabled when Edge compatibility mode is enabled (either via edgeCompat: true or automatically in Edge Runtime environments).

Retry Logic

Configure retry behavior for failed requests:

const logger = createPail({
    reporters: [
        new HttpReporter({
            url: "https://api.example.com/logs",
            maxRetries: 3, // Retry up to 3 times
            retryDelay: 1000, // Base delay: 1 second
            respectRateLimit: true, // Wait on 429 responses
        }),
    ],
});

Error Handling

Handle errors with custom callbacks:

const logger = createPail({
    reporters: [
        new HttpReporter({
            url: "https://api.example.com/logs",
            onError: (error) => {
                console.error("Failed to send log:", error);
                // Send to fallback service, alert, etc.
            },
            onDebug: (entry) => {
                console.log("Sending log entry:", entry);
            },
            onDebugRequestResponse: ({ req, res }) => {
                console.log("Request:", req.method, req.url);
                console.log("Response:", res.status, res.statusText);
            },
        }),
    ],
});

Payload Customization

Customize the payload format with a template function:

const logger = createPail({
    reporters: [
        new HttpReporter({
            url: "https://api.example.com/logs",
            payloadTemplate: ({ data, logLevel, message }) => {
                return JSON.stringify({
                    timestamp: new Date().toISOString(),
                    level: logLevel,
                    message,
                    metadata: data,
                });
            },
        }),
    ],
});

Size Limits

Configure maximum sizes to prevent oversized payloads:

const logger = createPail({
    reporters: [
        new HttpReporter({
            url: "https://api.example.com/logs",
            maxLogSize: 1_048_576, // 1MB per log entry
            maxPayloadSize: 5_242_880, // 5MB per batch payload
        }),
    ],
});

When a log entry exceeds maxLogSize, a LogSizeError is thrown. When a batch exceeds maxPayloadSize, it's automatically split into smaller batches.

Configuration Options

| Option | Type | Default | Description | | ------------------------ | ------------------------------------------ | -------------------- | ---------------------------------------------- | | url | string | required | HTTP endpoint URL | | method | string | "POST" | HTTP method | | headers | Record<string, string> \| (() => ...) | {} | Request headers (object or function) | | contentType | string | "application/json" | Content type for single requests | | batchContentType | string | "application/json" | Content type for batch requests | | enableBatchSend | boolean | true | Enable automatic batching | | batchSize | number | 100 | Number of logs to batch before sending | | batchSendTimeout | number | 5000 | Timeout (ms) to send batch regardless of size | | batchMode | "delimiter" \| "array" \| "field" | "delimiter" | Batch format mode | | batchSendDelimiter | string | "\n" | Delimiter for batch entries | | batchFieldName | string | undefined | Field name for "field" batch mode | | compression | boolean | false | Enable gzip compression | | maxRetries | number | 3 | Maximum retry attempts | | retryDelay | number | 1000 | Base delay between retries (ms) | | respectRateLimit | boolean | true | Wait on 429 rate limit responses | | maxLogSize | number | 1048576 | Maximum size per log entry (bytes) | | maxPayloadSize | number | 5242880 | Maximum size per payload (bytes) | | maxQueueSize | number | 10000 | Max queued entries before drop-oldest kicks in | | edgeCompat | boolean | false | Enable Edge Runtime compatibility | | onError | (error: Error) => void | undefined | Error callback | | onDrop | (droppedCount: number) => void | undefined | Called when queued entries are dropped | | onDebug | (entry: Record<string, unknown>) => void | undefined | Debug callback for log entries | | onDebugRequestResponse | (reqRes: {...}) => void | undefined | Debug callback for HTTP requests/responses | | payloadTemplate | (data: {...}) => string | undefined | Custom payload formatter |

Flushing & Shutdown

HttpReporter batches entries (up to batchSize, or every batchSendTimeout ms). Before a short-lived or serverless process exits, call flush() (or close() / dispose()) so the tail of the batch is shipped rather than lost:

const reporter = new HttpReporter({ url: "https://logs.example.com" });

// ... log ...

// Drain the queue before exit
await reporter.flush();

If the endpoint is unavailable, the queue is capped at maxQueueSize entries using a drop-oldest policy, so a sustained outage cannot grow memory without bound. Provide onDrop to be notified when entries are dropped.

JsonFileReporter likewise exposes close() / dispose() to end its rotating-file stream:

process.on("beforeExit", () => {
    fileReporter.close();
});

Wide Events

Wide events let you accumulate structured context throughout an operation and emit a single, comprehensive log event at the end — instead of scattering multiple log calls.

Inspired by Charity Majors' wide events pattern.

Basic Usage

import { createPail } from "@visulima/pail";
import { createWideEvent } from "@visulima/pail/wide-event";

const logger = createPail();

const ev = createWideEvent({ pail: logger, name: "api.checkout" });

ev.set({ user: { id: 1, plan: "pro" } });
ev.info("Validated cart", { itemCount: 3 });
ev.set({ cart: { id: 42, items: 3, total: 9999 } });
ev.info("Payment processed");
ev.finish({ status: 200 });

// Emits a single structured log:
// {
//   event: "api.checkout",
//   timestamp: "2026-03-14T10:23:45.612Z",
//   duration: "127ms",
//   duration_ms: 127,
//   status: 200,
//   user: { id: 1, plan: "pro" },
//   cart: { id: 42, items: 3, total: 9999 },
//   requestLogs: [
//     { level: "info", message: "Validated cart", context: { itemCount: 3 }, timestamp: "..." },
//     { level: "info", message: "Payment processed", timestamp: "..." }
//   ]
// }

Auto-Emit with Explicit Resource Management

Wide events implement Disposable for use with TC39 Explicit Resource Management (using):

{
    using ev = createWideEvent({ pail: logger, name: "api.checkout" });
    ev.set({ user: { id: 1 } });
    // auto-emits when scope exits
}

Typed Data Shape

interface CheckoutData {
    user: { id: number; plan: string };
    cart: { items: number; total: number };
}

const ev = createWideEvent<CheckoutData>({ pail: logger, name: "api.checkout" });
ev.set({ user: { id: 1, plan: "pro" } }); // fully typed

Level Escalation

The event level auto-escalates based on lifecycle log entries. If you call warn() or error(), the final emission uses the highest severity:

const ev = createWideEvent({ pail: logger, name: "api.checkout" });

ev.info("Cart validated"); // level stays "info"
ev.warn("Rate limit approaching"); // level escalates to "warn"
ev.info("Payment processed"); // level stays "warn" (no de-escalation)
ev.finish({ status: 200 }); // emits at "warn" level

Error Handling

const ev = createWideEvent({ pail: logger, name: "api.checkout" });

try {
    await processPayment();
} catch (error) {
    ev.error("Payment failed", error);
    ev.finish({ status: 500, error });
    // Emits with serialized error (name, message, stack, status, cause chain)
}

Options

| Option | Type | Default | Description | | ---------- | ------------- | -------- | --------------------------------- | | name | string | required | Event name, e.g. "api.checkout" | | pail | Pail instance | required | The pail logger to emit through | | service | string | — | Service name override | | type | string | "info" | Base log type (may be escalated) | | autoEmit | boolean | true | Auto-emit on Symbol.dispose |

Methods

| Method | Description | | ------------------------ | --------------------------------------------- | | set(data) | Deep-merge partial data into the event | | info(msg, ctx?) | Record an info-level lifecycle entry | | warn(msg, ctx?) | Record a warn-level entry (escalates level) | | error(msg, err?, ctx?) | Record an error-level entry (escalates level) | | debug(msg, ctx?) | Record a debug-level entry | | setError(error) | Attach an error (escalates to "error") | | setStatus(code) | Set HTTP status code | | finish(opts?) | Set status/error and emit | | emit(typeOverride?) | Emit the event (only fires once) | | getData() | Get read-only snapshot of accumulated data | | getLevel() | Get current severity level | | getRequestLogs() | Get read-only lifecycle log entries |

Framework Middleware

Pail provides first-class middleware/plugin adapters for popular web frameworks. Each adapter creates a request-scoped Wide Event that:

  • Accumulates context throughout the request lifecycle
  • Auto-emits a single structured log when the response completes or an error occurs
  • Is accessible via req.log / request.log / context.log and useLogger()
  • Supports route inclusion/exclusion via glob patterns
  • Supports per-route service name overrides

Shared Options

All middleware adapters accept these options:

| Option | Type | Description | | --------- | ------------------------------ | ----------------------------------- | | pail | Pail instance | The pail logger (required) | | service | string | Default service name | | include | string[] | Glob patterns for paths to include | | exclude | string[] | Glob patterns for paths to exclude | | routes | Record<string, { service? }> | Per-route config (first match wins) |

Express

import express from "express";
import { createPail } from "@visulima/pail";
import { pailMiddleware, useLogger } from "@visulima/pail/middleware/express";

const app = express();
const logger = createPail();

app.use(pailMiddleware({ pail: logger }));

app.get("/api/users", (req, res) => {
    req.log.set({ user: { id: 1 } });
    req.log.info("Fetched user list");
    // or from anywhere in the async call stack:
    // useLogger().set({ user: { id: 1 } });
    res.json({ ok: true });
});

Exports: pailMiddleware, useLogger, PailRequest, PailResponse, PailNextFunction, PailExpressMiddleware, WideEvent

Fastify

import Fastify from "fastify";
import { createPail } from "@visulima/pail";
import { pailPlugin, useLogger } from "@visulima/pail/middleware/fastify";

const app = Fastify();
const logger = createPail();

pailPlugin(app, { pail: logger });

app.get("/api/users", async (request, reply) => {
    request.log.set({ user: { id: 1 } });
    return { ok: true };
});

Exports: pailPlugin, useLogger, PailFastifyRequest, PailFastifyReply, PailFastifyInstance, WideEvent

Hono

import { Hono } from "hono";
import { createPail } from "@visulima/pail";
import { pailMiddleware, useLogger } from "@visulima/pail/middleware/hono";

const app = new Hono();
const logger = createPail();

app.use("*", pailMiddleware({ pail: logger }));

app.get("/api/users", (c) => {
    const log = useLogger(c);
    log.set({ user: { id: 1 } });
    return c.json({ ok: true });
});

Note: Hono's useLogger() takes the context c as an argument (the logger is stored on the context, not in AsyncLocalStorage).

Exports: pailMiddleware, useLogger, PailHonoContext, PailHonoNext, PailHonoMiddleware, WideEvent

Elysia

import { Elysia } from "elysia";
import { createPail } from "@visulima/pail";
import { pailPlugin, useLogger } from "@visulima/pail/middleware/elysia";

const logger = createPail();
const app = new Elysia();

pailPlugin(app, { pail: logger });

app.get("/api/users", ({ log }) => {
    log.set({ user: { id: 1 } });
    return { ok: true };
});

Exports: pailPlugin, useLogger, PailElysiaInstance, WideEvent

SvelteKit

// src/hooks.server.ts
import { createPail } from "@visulima/pail";
import { createPailHooks, useLogger } from "@visulima/pail/middleware/sveltekit";

const logger = createPail();
const { handle, handleError } = createPailHooks({ pail: logger });

export { handle, handleError };
// In a load function or action:
import { useLogger } from "@visulima/pail/middleware/sveltekit";

export const load = async (event) => {
    event.locals.log.set({ user: { id: 1 } });
    // or from anywhere in the async call stack:
    // useLogger().set({ user: { id: 1 } });
};

Exports: pailHandle, pailHandleError, createPailHooks, useLogger, PailSvelteKitEvent, PailSvelteKitHandle, PailSvelteKitHandleError, WideEvent

Next.js

Next.js integration uses two parts: an edge middleware for request ID injection and a handler wrapper for wide event logging.

// middleware.ts
import { NextResponse } from "next/server";
import { pailMiddleware } from "@visulima/pail/middleware/next";

export default pailMiddleware(NextResponse, {
    exclude: ["/_next/**", "/favicon.ico"],
});
// lib/pail.ts
import { createPail } from "@visulima/pail";
import { createWithPail } from "@visulima/pail/middleware/next";

const logger = createPail();
export const withPail = createWithPail({ pail: logger });
// app/api/users/route.ts
import { withPail } from "@/lib/pail";
import { useLogger } from "@visulima/pail/middleware/next";

export const GET = withPail(async (request: Request) => {
    const log = useLogger();
    log.set({ user: { id: 1 } });
    return Response.json({ ok: true });
});

Exports: pailMiddleware, createWithPail, useLogger, WideEvent

Route Configuration

All adapters support glob-based route configuration:

pailMiddleware({
    pail: logger,
    service: "api-gateway",
    exclude: ["/health", "/metrics", "/_next/**"],
    include: ["/api/**"],
    routes: {
        "/api/auth/**": { service: "auth-service" },
        "/api/payments/**": { service: "payments-service" },
    },
});
  • Exclusions take precedence over inclusions
  • Glob patterns support * (any non-/), ** (any including /), and ? (single char)
  • Route service overrides apply to the first matching pattern

Sensitive Header Filtering

All adapters automatically filter sensitive headers before logging:

| Filtered Headers | | --------------------- | | authorization | | cookie | | set-cookie | | x-api-key | | x-auth-token | | proxy-authorization |

Integrations

Use with @visulima/boxen

Create nicely framed messages with @visulima/boxen and log them with Pail:

import { pail } from "@visulima/pail";
import { boxen } from "@visulima/boxen";

const output = boxen("Build completed successfully", {
    headerText: "Success",
    padding: 1,
    borderStyle: "round",
});

pail.success(output);

You can also include multi‑line content; Pail preserves newlines:

import { pail } from "@visulima/pail";
import { boxen } from "@visulima/boxen";

const details = ["Service: api", "Env: production", "Commit: a1b2c3"].join("\n");

pail.info(
    boxen(details, {
        headerText: "Deploy Info",
        padding: { top: 0, right: 2, bottom: 0, left: 2 },
        borderStyle: "classic",
    }),
);

Use with @visulima/tabular

Render tables with @visulima/tabular and send them through Pail:

import { pail } from "@visulima/pail";
import { createTable } from "@visulima/tabular";

const table = createTable({
    showHeader: true,
    style: {
        // optional: customize colors/border via @visulima/colorize
    },
});

table.setHeaders(["Task", "Status", "Duration"]).addRow(["build", "ok", "1.2s"]).addRow(["test", "ok", "3.4s"]).addRow(["lint", "warn", "0.8s"]);

pail.info("\n" + table.toString());

For dynamic dashboards, combine tabular with Pail's interactive mode and update the rendered table as your process runs.

Disable and Enable Loggers

To disable a logger, use the disable() function, which will prevent any log messages from being written to the console or a file.

import { pail } from "@visulima/pail";

pail.disable();
pail.success("This message will not be logged");

To enable a logger, use the enable() function, which will allow log messages to be written to the console or a file.

import { pail } from "@visulima/pail";

pail.disable();
pail.success("This message will not be logged");
pail.enable();
pail.success("This message will be logged");

Pause and Resume Loggers

The pause() and resume() functions allow you to temporarily queue log messages and then flush them all at once. This is similar to how consola works and is useful when you need to buffer output during critical operations.

When paused, all log calls will be queued instead of being output immediately. When you call resume(), all queued messages will be processed in the order they were called.

import { pail } from "@visulima/pail";

pail.pause();

// These messages will be queued
pail.info("First message");
pail.warn("Second message");
pail.success("Third message");

// Resume logging - all queued messages are now output in order
pail.resume();

// This message is output immediately
pail.info("Fourth message");

Note: Unlike disable(), which discards log messages, pause() queues them for later output. This makes it ideal for scenarios where you want to control when messages appear without losing them.

Api

Supported Node.js Versions

Libraries in this ecosystem make the best effort to track Node.js’ release schedule. Here’s a post on why we think this is important.

Contributing

If you would like to help take a look at the list of issues and check our Contributing guild.

Note: please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

Credits

About

Related Projects

  • pino - 🌲 super fast, all natural json logger
  • winston - A logger for just about everything.
  • signale - Highly configurable logging utility
  • consola - 🐨 Elegant Console Logger for Node.js and Browser

Made with ❤️ at Anolilab

This is an open source project and will always remain free to use. If you think it's cool, please star it 🌟. Anolilab is a Development and AI Studio. Contact us at [email protected] if you need any help with these technologies or just want to say hi!

License

The visulima pail is open-sourced software licensed under the MIT