Examples

Real-world examples of using Pail

Last updated:

Examples

Real-world examples demonstrating how to use Pail in various scenarios.

Express.js API Server

import express from "express";
import { createPail } from "@visulima/pail";
import { PrettyReporter } from "@visulima/pail/reporter/pretty";
import CallerProcessor from "@visulima/pail/processor/caller";

const logger = createPail({
    scope: "api",
    types: {
        http: {
            badge: "🌐",
            color: "blue",
            label: "HTTP",
            logLevel: "info",
        },
    },
    reporters: [new PrettyReporter()],
    processors: [new CallerProcessor()],
});

const app = express();

// Request logging middleware
app.use((req, res, next) => {
    const requestId = req.headers["x-request-id"] || `req-${Date.now()}`;
    const requestLogger = logger.scope("request", requestId);
    req.logger = requestLogger;

    requestLogger.http(`${req.method} ${req.path}`);

    const start = Date.now();
    res.on("finish", () => {
        const duration = Date.now() - start;
        requestLogger.http(`${req.method} ${req.path} ${res.statusCode}`, {
            duration: `${duration}ms`,
        });
    });

    next();
});

app.get("/users", async (req, res) => {
    req.logger.time("query");
    const users = await db.query("SELECT * FROM users");
    req.logger.timeEnd("query");

    res.json(users);
});

app.listen(3000, () => {
    logger.info("Server started on port 3000");
});

Next.js Application

// lib/logger.ts
import { createPail } from "@visulima/pail";

export const logger = createPail({
    scope: "nextjs",
    logLevel: process.env.NODE_ENV === "production" ? "info" : "debug",
});

// app/api/users/route.ts
import { logger } from "@/lib/logger";

export async function GET() {
    const apiLogger = logger.scope("api", "users");

    apiLogger.info("Fetching users");

    try {
        const users = await fetchUsers();
        apiLogger.success(`Fetched ${users.length} users`);
        return Response.json(users);
    } catch (error) {
        apiLogger.error("Failed to fetch users", error);
        return Response.json({ error: "Failed to fetch users" }, { status: 500 });
    }
}

CLI Tool with Progress Bars

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

const logger = createPail({
    interactive: true,
    scope: "cli",
});

const processFiles = async (files: string[]) => {
    const multiBar = logger.createMultiProgressBar();

    const uploadBar = multiBar.create(files.length, 0, {
        format: "Uploading [{bar}] {percentage}% | {value}/{total} files",
    });

    const processBar = multiBar.create(files.length, 0, {
        format: "Processing [{bar}] {percentage}% | {value}/{total} files",
    });

    uploadBar.start();
    processBar.start();

    for (let i = 0; i < files.length; i++) {
        await uploadFile(files[i]);
        uploadBar.update(i + 1);

        await processFile(files[i]);
        processBar.update(i + 1);
    }

    multiBar.stop();
    logger.success(`Processed ${files.length} files`);
};

Background Job Processor

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

const logger = createPail({
    types: {
        job: {
            badge: "⚙️",
            color: "cyan",
            label: "JOB",
            logLevel: "info",
        },
        jobStart: {
            badge: "▶️",
            color: "green",
            label: "JOB_START",
            logLevel: "info",
        },
        jobComplete: {
            badge: "✅",
            color: "greenBright",
            label: "JOB_COMPLETE",
            logLevel: "info",
        },
        jobError: {
            badge: "❌",
            color: "red",
            label: "JOB_ERROR",
            logLevel: "error",
        },
    },
});

const processJob = async (jobId: string) => {
    const jobLogger = logger.scope("job", jobId);

    jobLogger.jobStart("Starting job", { jobId });

    try {
        jobLogger.job("Processing step 1");
        await step1();

        jobLogger.job("Processing step 2");
        await step2();

        jobLogger.jobComplete("Job completed", { jobId });
    } catch (error) {
        jobLogger.jobError("Job failed", error);
        throw error;
    }
};

Database Logger

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

const logger = createPail({
    types: {
        db: {
            badge: "💾",
            color: "cyan",
            label: "DATABASE",
            logLevel: "debug",
        },
        dbQuery: {
            badge: "🔍",
            color: "blueBright",
            label: "QUERY",
            logLevel: "debug",
        },
        dbError: {
            badge: "❌",
            color: "red",
            label: "DB_ERROR",
            logLevel: "error",
        },
    },
});

// Database wrapper
const db = {
    async query(sql: string, params?: any[]) {
        logger.dbQuery(sql, { params });
        logger.time("query");

        try {
            const result = await dbClient.query(sql, params);
            logger.timeEnd("query");
            return result;
        } catch (error) {
            logger.dbError("Query failed", error);
            logger.timeEnd("query");
            throw error;
        }
    },
};

Production Logging Setup

import { createPail } from "@visulima/pail";
import { JsonReporter } from "@visulima/pail/reporter/json";
import { JsonFileReporter } from "@visulima/pail/reporter/file";
import RedactProcessor from "@visulima/pail/processor/redact";
import CallerProcessor from "@visulima/pail/processor/caller";
import SamplingProcessor from "@visulima/pail/processor/sampling";
import EnvironmentProcessor from "@visulima/pail/processor/environment";

const logger = createPail({
    logLevel: process.env.PAIL_LOG_LEVEL || "info",
    reporters: [
        new JsonReporter(), // Console output (for log aggregation)
        new JsonFileReporter({
            filePath: "/var/log/app.log",
            interval: "1d",
            size: "100M",
            compress: "gzip",
        }),
    ],
    processors: [
        new CallerProcessor(),
        new RedactProcessor(
            [
                // Redact credit cards
                { key: "card", pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, replacement: "[CARD]" },
                // Redact emails
                { key: "email", pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replacement: "[EMAIL]" },
                // Redact SSNs
                { key: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: "[SSN]" },
            ],
            { exclude: [] }, // Optional options
        ),
        // Sample debug logs at 10%, keep all errors
        new SamplingProcessor({
            debug: 10,
            trace: 5,
        }),
        // Auto-detect runtime environment
        new EnvironmentProcessor({
            includePid: true,
            includeHostname: true,
        }),
    ],
});

Self-Documenting Errors

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

const logger = createPail();

// Throw errors with actionable context
try {
    const config = loadConfig();
} catch (error) {
    throw new PailError({
        message: "Failed to load configuration",
        why: "The config file is missing or malformed",
        fix: "Create a config.json file in the project root, or set the CONFIG_PATH environment variable",
        link: "https://docs.example.com/configuration",
        status: 500,
        cause: error,
    });
}

// Use the factory function for convenience
const error = createPailError({
    message: "API rate limit exceeded",
    why: "Too many requests in the current time window",
    fix: "Implement exponential backoff or reduce request frequency",
    status: 429,
});

// PailError serializes cleanly to JSON
logger.error(error);
// JSON output includes why, fix, link fields for easy debugging

React Component Logger

// hooks/useLogger.ts
import { useEffect, useRef } from "react";
import { pail } from "@visulima/pail";

export const useLogger = (componentName: string) => {
  const loggerRef = useRef(pail.scope("component", componentName));

  useEffect(() => {
    loggerRef.current.info("Component mounted");

    return () => {
      loggerRef.current.info("Component unmounted");
    };
  }, []);

  return loggerRef.current;
};

// components/UserProfile.tsx
import { useLogger } from "@/hooks/useLogger";

export const UserProfile = ({ userId }: { userId: string }) => {
  const logger = useLogger("UserProfile");

  useEffect(() => {
    logger.debug("Loading user", { userId });
    // ... load user
  }, [userId]);

  return <div>...</div>;
};

Error Tracking Integration

import { createPail } from "@visulima/pail";
import { JsonReporter } from "@visulima/pail/reporter/json";

class ErrorTrackingReporter implements Reporter {
    public log(meta: ReadonlyMeta) {
        if (meta.type.level === "error" || meta.type.level === "critical") {
            // Send to error tracking service
            errorTrackingService.captureError(meta.error || meta.message, {
                level: meta.type.level,
                context: meta.context,
                scope: meta.scope,
            });
        }
    }
}

const logger = createPail({
    reporters: [new JsonReporter(), new ErrorTrackingReporter()],
});

Testing Setup

// test/setup.ts
import { createPail } from "@visulima/pail";

// Disable logging in tests by default
export const testLogger = createPail({
    disabled: process.env.DEBUG !== "1",
    logLevel: "warning", // Only show warnings and errors
});

// Enable for specific tests
export const enableTestLogging = () => {
    testLogger.enable();
    // Note: logLevel cannot be changed after creation.
    // Create a new logger if you need a different log level.
};
Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues