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

halua

v4.1.0

Published

Tiny zero-dependency logger with pluggable dispatchers, child contexts, minor levels (e.g. INFO+5) and safe formatting

Readme

Halua

A powerful, extensible logging library for Node.js, browsers, and edge runtimes.

Halua gives you full control over log output through pluggable dispatchers (text, JSON, console), hierarchical child loggers, fine-grained level filtering (including minor levels like INFO+3), and zero-config defaults that just work.

npm version License: MIT

Features

  • Zero-config default logger (writes to console using appropriate methods)
  • Four built-in dispatchers: NewTextDispatcher, NewJSONDispatcher, NewConsoleDispatcher, NewConsoleColoredDispatcher
  • Compose any number of dispatchers per logger instance
  • Child loggers that automatically append context (logger.child("user", 42))
  • Powerful level system: TRACE < DEBUG < INFO < NOTICE < WARN < ERROR < FATAL + minor levels (INFO+5)
  • Per-dispatcher level overrides and exact-match mode
  • Zero-cost disabled levels: calling a level with no active dispatchers (e.g. debug in prod) is a true no-op with zero overhead
  • Beautiful structured formatting for objects, arrays, Maps, Sets, Errors, etc.
  • Safe by design — dispatcher errors never crash your application
  • .stamp(label, id?) + .stampEnd(id) (or returned ender) for performance.now-based timing with automatic pretty took X.XXms logging
  • Tiny, fast, tree-shakeable ESM + CJS + TypeScript

Installation

npm install halua
# or
pnpm add halua

Quick Start

import { halua } from "halua"

halua.info("Application started")
halua.warn("Disk space low", { available: "12%" })
halua.error("timeout") // strings accepted too (unknownToError normalizes to Error)

Default output (console):

22/05/2026 21:55:50 INFO Application started
22/05/2026 21:55:50 WARN Disk space low { available: '12%' }
22/05/2026 21:55:50 ERROR Error: timeout
    at ...

Dedicated Loggers & Dispatchers

Use the built-in dispatcher factories to create purpose-specific loggers:

import { halua, NewTextDispatcher, NewJSONDispatcher, Level } from "halua"

// Text logger (human readable)
let textLogger = halua.create(NewTextDispatcher((line) => sendToLogServer(line)))

// JSON logger (for structured ingestion)
let jsonLogger = halua.create(NewJSONDispatcher((json) => writeToArchive(json)))

// Console logger (explicit)
let consoleLogger = halua.create(NewConsoleDispatcher(console))

// Colored console (ANSI in Node, %c CSS in browsers; colors: trace/debug=purple, info=blue, notice=orange, warn/error/fatal=red)
let colorLogger = halua.create(NewConsoleColoredDispatcher(console))

textLogger.info("user action", { id: 123, type: "click" })
// -> 22/05/2026 21:55:50 INFO user action { id: 123, type: "click" }

jsonLogger.info("structured", { success: true })
// -> {"timestamp":"2026-05-22T18:55:50.430Z","level":"INFO","args":["structured",{"success": true}]}

You can pass an array to use multiple dispatchers at once:

let prodLogger = halua.create([NewTextDispatcher(sendToFile), NewJSONDispatcher(sendToElastic)], { level: Level.Info })

Child Loggers (Context)

let requestLogger = halua.child("requestId", "abc-123", "user", 42)

requestLogger.info("processing started")
// -> ... INFO processing started requestId abc-123 user 42

let stepLogger = requestLogger.child("step", "validate")
stepLogger.warn("slow validation")
// -> ... WARN slow validation requestId abc-123 user 42 step validate

Call .create({ withArgs: [] }) to clear context on a child.

Level Control

import { Level } from "halua"

// Instance level (affects all dispatchers that don't override)
let logger = halua.create({ level: Level.Warn })

logger.debug("hidden")
logger.info("hidden")
logger.warn("visible")
logger.error("visible")

Per-Dispatcher Levels

let logger = halua.create([
    NewTextDispatcher(sendToFile, { level: Level.Info }),
    NewJSONDispatcher(sendToMetrics, { level: Level.Error }),
])

Minor / Custom Levels

Use the LEVEL+N syntax for fine-grained control (e.g. sampling, feature flags):

let logger = halua.create(NewTextDispatcher(out), { level: `${Level.Info}+2` })

logger.logTo("INFO+1", "sampled out")
logger.logTo("INFO+2", "important info") // logged
logger.logTo("INFO+3", "very important") // logged
logger.logTo("WARN", "always higher major level") // logged

You can also pass string levels directly: { level: "ERROR+7" } or logTo("DEBUG+10", ...).

Sensitive Data Redaction

Pass redactDataRegExp (a RegExp) to halua.create(options) for the logger instance (applies to all its dispatchers) or to individual New*Dispatcher(..., { redactDataRegExp }) (overrides the logger default).

  • In strings (and strings inside arrays): all matches of the regexp are replaced by "^_^"
  • In objects and Maps: if a key matches the regexp, its value (any type) is replaced entirely by "^_^"
  • Works for both text and structured (JSON) output, and for errorMeta
  • Use the exported DefaultRedactRegExp for common PII (passwords, tokens, api keys, emails, SSNs, JWTs, credit cards, etc.) or provide your own.
import { halua, NewJSONDispatcher, DefaultRedactRegExp } from "halua"

// logger-level default (affects dispatchers without their own setting)
let prodLogger = halua.create(
    [
        NewJSONDispatcher(sendToStore),
        NewTextDispatcher(sendToFile, { level: Level.Warn }), // this one can override if needed
    ],
    { redactDataRegExp: DefaultRedactRegExp },
)

prodLogger.info("login", { user: "alice", password: "hunter2", apiKey: "sk_xxx" })
// args become: ["login", { user: "alice", password: "^_^", apiKey: "^_^" }]

prodLogger.info("token eyJhbGciOi...abc.123", "email: [email protected]")
// the string arg will have secrets replaced by ^_^

The redact helper is also exported for custom dispatchers or preprocessing.

Dispatcher Options

All New*Dispatcher factories accept a second options argument:

| Option | Type | Default | Description | | ------------------ | ------------------------ | ----------- | ---------------------------------------------------------------------------------------- | | level | LogLevel | undefined | Minimum level this dispatcher accepts | | exact | LogLevel \| LogLevel[] | null | Only log these exact levels (ignores normal hierarchy) | | printTimestamp | boolean | true | Include timestamp in output | | printLevel | boolean | true | Include level name in output | | spacing | boolean | true | Pretty-print objects/arrays with tabs & newlines (Text & JSON only) | | redactDataRegExp | RegExp | undefined | Redact sensitive data in strings/arrays and by key in objects/maps (see feature section) |

NewConsoleDispatcher and NewConsoleColoredDispatcher do not support spacing (they pass values directly to console methods).

API Reference

Main Export

import {
    halua,
    Level,
    NewTextDispatcher,
    NewJSONDispatcher,
    NewConsoleDispatcher,
    NewConsoleColoredDispatcher,
} from "halua"
  • halua — default logger instance (preconfigured with NewConsoleDispatcher)
  • Level — enum: Trace | Debug | Info | Notice | Warn | Error | Fatal
  • NewTextDispatcher(send: (line: string, errorMeta?: Record<string, any>) => void, options?) → factory
  • NewJSONDispatcher(send: (json: string, errorMeta?: Record<string, any>) => void, options?) → factory
  • NewConsoleDispatcher(console: {debug,info,warn,error}, options?) → factory
  • NewConsoleColoredDispatcher(console: {debug,info,warn,error}, options?) → factory (colors levels: TRACE/DEBUG=purple, INFO=blue, NOTICE=orange, WARN/ERROR/FATAL=red; uses ANSI in Node, %c in browsers)

Advanced Exports (for custom dispatcher authors)

import { DispatcherBase, format, getType, toJSONValue, Dispatcher, HaluaLogger, ConsoleLike } from "halua"
  • DispatcherBase — extendable base class implementing dispatch(meta, args) + timestamp/level prefixing; override via formatArg
  • format(spec: {type, value, ...}) — the text pretty-printer (handles circulars, Errors, Maps, etc.)
  • getType(value) — returns ArgumentType discriminant for any JS value
  • toJSONValue(value) — converts any value to a JSON-legal tree (Errors → {name,message,stack[]}, etc.)
  • redact(value, regexp?) — recursively redacts strings by content match and object/map values by key match (used internally by the redact feature)
  • DefaultRedactRegExp — built-in regexp matching common sensitive keys and value patterns (password, token, email, ssn, jwt, cc, etc.)
  • Dispatcher — interface for raw custom dispatchers (dispatch(meta, args): void)
  • HaluaLogger — the logger instance interface
  • ConsoleLike — minimal { debug, info, warn, error } shape accepted by NewConsoleDispatcher / NewConsoleColoredDispatcher

Logger Instance Methods

| Method | Description | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | .create(dispatcher?, options?) | Create a new independent logger (inherits dispatchers/options when partial) | | .child(...args) | Create child logger that appends context to every message | | .setDispatchers(dispatcher \| dispatchers[]) | Replace all dispatchers | | .appendDispatchers(...) | Add more dispatchers to existing set | | .logTo(level, ...args) | Log at a custom / minor level | | .trace / .debug / .info / .warn / .notice / .fatal(...args) | Standard levels (varargs) | | .error(error, meta?) | Log at ERROR level; first arg (unknown) is normalized to Error; optional meta?: ErrorMeta (generic on the logger instance) — when supplied, the normalized Error instance is auto-attached under error key and the augmented object becomes the second arg to dispatchers | | .assert(condition, error, meta?) | Log at ERROR only on falsy condition; same error + optional meta?: ErrorMeta semantics as .error (auto-attaches normalized Error under error when meta supplied) | | .stamp(label, id?) | Start high-res perf timer (performance.now); returns ender fn; optional id for .stampEnd | | .stampEnd(id) | End named stamp started with same id on this logger; logs pretty label took X.XXms |

Every method returns a new HaluaLogger when using .create / .child, so they are fully chainable.

setDispatchers and appendDispatchers mutate the dispatcher list on the live instance only. They do not update the blueprint used by later .create(...) or .child(...) calls on that same logger (those continue to inherit the dispatchers that were supplied when the logger was originally built). If you need a fresh logger with the new set, call halua.create(newDispatchers) (or the mutated logger's .create(newDispatchers)).

Error Handling

Halua never throws from logging calls. If a dispatcher fails, the error is reported via console.error (best-effort) and logging continues for other dispatchers.

Using errorMeta with error trackers (Sentry, Rollbar, etc.)

The special .error(unknown, meta?) and .assert(condition, unknown, meta?) methods accept an optional second meta object. When you use a custom send callback with NewTextDispatcher (or NewJSONDispatcher), this meta is delivered as the second argument to your send function.

Halua automatically appends the normalized Error instance (the same one passed to the primary log args) under the error key of the meta object. This makes it trivial to forward the live Error (including .cause, custom props, and accurate stack) to error trackers instead of a string or plain-object snapshot.

This is ideal for attaching correlation IDs, issue keys, user context, or routing hints to your error reporting service without polluting the normal log arguments.

import * as Sentry from "@sentry/node"
import { halua, NewTextDispatcher } from "halua"

// Human-readable logs via TextDispatcher, while still forwarding
// rich errorMeta (issueKey, etc.) to your error tracker.
let errorSink = NewTextDispatcher((line, errorMeta) => {
    if (errorMeta?.issueKey) {
        const err = errorMeta.error
        // Destructure to omit the Error from `extra` (Sentry serializes it nicely on its own)
        const { error: _err, ...context } = errorMeta
        if (err instanceof Error) {
            Sentry.captureException(err, {
                level: "error",
                tags: {
                    issueKey: errorMeta.issueKey,
                    component: errorMeta.component,
                },
                extra: context,
            })
        } else {
            Sentry.captureMessage(line, { extra: context })
        }
    } else {
        // Fallback: still surface the error even without extra context
        Sentry.captureMessage(line, "error")
    }
})

let logger = halua.create(errorSink, { level: "WARN" })

// Normal log — no meta attached (Note that .error will serialize passed string to Error)
logger.error("something odd happened")

// Critical path with traceable issue key — when you supply the second meta arg to .error or .assert,
// Halua auto-appends the normalized Error under `errorMeta.error` (in addition to your fields).
// The formatted line stays clean; your send fn receives the live Error + context for captureException.
logger.error(new Error("Payment declined"), {
    issueKey: "PAY-48291",
    userId: 8472,
    component: "checkout",
    requestId: "req_abc123",
})

The user-supplied portion of meta is never mixed into the formatted args (exception: ConsoleDispatcher) — it (plus the auto-attached error) is always available as a clean second parameter to your send function. When meta is absent, the second argument is undefined.

Advanced / Custom Dispatchers

For simple file (or any sink) logging the easiest approach is to use a built-in factory with your own send function:

import { halua, NewTextDispatcher } from "halua"
import fs from "node:fs"

let logPath = "app.log"

let fileLogger = halua.create(
    NewTextDispatcher((line) => {
        fs.appendFileSync(logPath, line + "\n")
    }),
)

If you need full control (custom dispatch, different prefixing, rotation, binary framing, remote calls, etc.) extend DispatcherBase and use the exported format + getType (or toJSONValue) exactly as the built-ins do:

import { halua, DispatcherBase, format, getType, toJSONValue } from "halua"
import fs from "node:fs"

const NewFileDispatcher = (filePath: string) => {
    return () =>
        new (class FileDispatcher extends DispatcherBase {
            constructor() {
                super((line) => {
                    fs.appendFileSync(filePath, line + "\n")
                })
                this.formatArg = (v) => format({ type: getType(v), value: v }, true)
            }
        })()
}

let fileLogger = halua.create(NewFileDispatcher("app.log"), { level: "INFO" })
fileLogger.warn("something happened", { user: 42 })

The Dispatcher interface (dispatch(meta, args)) + DispatcherBase + format/getType/toJSONValue are the public extension surface. See src/main/dispatchers/text-dispatcher.ts, json-dispatcher.ts for reference implementations.

Semver note for custom dispatchers: Dispatcher, dispatch, DispatcherBase, and the formatter trio are stable within a major version. Changes that would break existing custom Dispatcher implementations are released only as majors and recorded in docs/dr.md.

For most use cases the three built-in dispatchers are sufficient.

TypeScript

Full TypeScript support included. All types are exported.

License

MIT © inshinrei


See also: Tour of Halua for a narrative deep dive and decision records in docs/dr.md.