JSPM

  • Created
  • Published
  • Downloads 134
  • Score
    100M100P100Q98582F
  • License MIT

A clean, lightweight, cross-platform logging library for Node.js and modern browsers, fully typed and customizable with scoped loggers, formatter pipelines, and modular handlers offering full flexibility for custom processing and delivery.

Package Exports

  • logry

Readme

Logry

A clean, lightweight, cross-platform logging library for Node.js and modern browsers,
fully typed and customizable with scoped loggers, formatter pipelines,
and modular handlers offering full flexibility for custom processing and delivery.

NPM version Bundle size Coverage Status TypeScript License

๐Ÿ“š Read the Docs

Logging across fullstack apps is messy.
You jump between server and browser but most loggers donโ€™t.
Logry is built for the monorepo era โ€” a fully typed logger that works the same everywhere.


Books Outline


Smiling Face with Sunglasses Features at a Glance

  • ๐ŸŒ Environment Agnostic โ€” Use the same logger in SSR, API routes, and the browser
  • โšก Zero Dependency & Fully Typed โ€” Written in TypeScript with no runtime bloat
  • ๐Ÿ” Built-in Context Support โ€” Inject trace data like requestId, userId and pass it downstream
  • ๐ŸŒณ Scoped Loggers โ€” Organize logs with nested scopes like auth > login > error
  • ๐ŸŽจ Fully Customizable Output โ€” Tweak every part of the format, or define your own
  • ๐Ÿ“ฆ Plugin-Ready Core โ€” Extend via custom handlers and hooks with minimal effort

Glowing Star Output Preview

Hereโ€™s how logs look in Node.js vs. the browser:

Logry node
Console output in Node.js
Logry browser
Console output in Browser

Kissing Cat Not your style? No worries! Itโ€™s fully customizable.


Triangular Flag Installation

npm install logry

or use yarn

yarn add logry

Rocket Quick Start

Using Static Logger Methods

The easiest way to use Logry is by calling its static logging methods.
They work instantly without a logger instance, ignore level restrictions, and default to the โ€œprettyโ€ preset for clean output

import { trace, debug, info, warn, error, fatal } from "logry";

info("๐Ÿ‘‹๐Ÿผ Hi there! Welcome to Logry!");

warn("User session is about to expire", { user: "John Doe" }); // second argument is metadata (meta)

error("Unexpected error occurred", new Error("Something went wrong")); // you can also pass an Error

Creating a Custom Logger Instance

You can create a logger by calling logry().
By default, the log level is set to "warn", so only logs with levels "warn", "error", and "fatal" will be shown.
If you donโ€™t specify an ID, the logger will use "default" as its identifier automatically.

import { logry } from "logry";

// Create a custom logger instance (defaults to id: 'default' and level: 'warn')
const logger = logry();

logger.info("User logged in"); // โŒ This won't be shown โ€” 'info' is lower than the default 'warn' level

logger.warn("User login warning"); // โœ… This will be shown
  • Basic Logger Setup for Development
import { logry } from "logry";

const logger = logry({
  id: "MyLogger",
  level: "debug", // Will show: debug, info, warn, error, fatal (trace will be hidden)
});
  • Full Custom Logger Setup
import { logry } from "logry";

const logger = logry({
  id: "๐ŸŒ My Logger",
  level: "info",
  scope: ["auth", "api"],
  context: { env: "production", appVersion: "2.5.1" },
  preset: "verbose", // "pretty" | "pretty-multi-line" | "minimal" | "verbose"
  normalizerConfig: {
    node: {
      timestamp: { style: "iso" },
      // ...
    },
    browser: {
      timestamp: { style: "pretty" },
      // ...
    },
  },
  formatterConfig: {
    node: {
      id: { ansiColor: "\x1b[35m" },
      message: { customFormatter: ({ part }) => "\n" + part.toUpperCase() },
      // ...
    },
    browser: {
      id: { cssStyle: "color: purple;" },
      context: { format: "compact" },
      // ...
    },
  },
  handlerConfig: {
    // ...
  },
});

Shooting Star Presets

Logry offers several built-in logger presets.
Each preset is a set of normalizer and formatter configs for different log styles.

Preset Description
pretty Formatted, easy to read
pretty-multi-line Multi-line output with line breaks
minimal Simple output with essential info only
verbose Full detail with context and depth

To use a preset, pass it when creating the logger:

const logger = logry({ preset: "pretty" });

Presets are fixed for now.
๐ŸŽฏ Custom presets may come in future versions.


Comet Core Concepts

Logry is built with modularity, precision, and developer experience in mind.
Here are the key concepts that define how it works:

โœจ Log Level

Logry supports seven log levels, ordered from most critical to most verbose:

Level Description
fatal โ— Logs critical system failures. The application may crash or exit immediately
error โŒ Logs runtime errors that should be investigated and typically require action
warn โš ๏ธ Logs recoverable issues or unexpected behaviors that don't prevent operation
info โ„น๏ธ Logs general operational messages, such as successful startups or actions
debug ๐Ÿ› ๏ธ Logs detailed internal information helpful for debugging
trace ๐Ÿ” Logs the most granular details โ€” every step, useful for profiling or deep debugging
silent ๐Ÿค Disables all logging output

The logger only outputs messages at or above the current level.
For example, if the level is set to warn, only warn, error, and fatal logs will be printed.

You can specify the desired log level when creating a logger instance

Core-level configs like level are only applied when creating a new core.
If a core with the same ID exists, those configs will be ignored, and a warning will be logged.

// Initialize a logger with a preferred level (for initial filtering)
const logger = logry({ id: "my-app", level: "debug" });

โœจ Child Loggers

In Logry, every logger instance is lightweight and modular.
You can freely create child loggers that inherit settings from their parent โ€” while overriding only what you need.

Creating a Child Logger

You can use the .child() method to create a scoped or customized logger:

const logger = logry({ id: "main-app", level: "info" });

const authLogger = logger.child({
  level: "debug", // override log level
  scope: "auth", // add a scope
  context: { userType: "admin" }, // inject default context
});
  • Child loggers inherit settings with shallow merging (first-level only):
    • scope: appended
      e.g., ["main"] + "auth" โ†’ ["main", "auth"]
    • context: merged with child overriding
      e.g., { app: "main", user: "guest" } + { user: "admin" } โ†’ { app: "main", user: "admin" }
    • formatterConfig / normalizerConfig: shallow merged per platform (node, browser), with child taking precedence

โœจ Logger Core

The core engine responsible for managing log levels, shared identity (id), and optional configurations for formatting, normalization, and handlers.

  • Multiple logger instances can share a single core by specifying the same id, enabling centralized and synchronized log level management across instances.
  • It supports dynamic runtime control of log verbosity:
    • setLevel(level): updates the active log level
    • resetLevel(): restore to the initial log level

This allows flexible adjustment of log output without needing to recreate logger instances.


Airplane Departure Transporter

When a log is passed to the Transporter,
it first runs through the Normalizer to ensure a consistent and structured payload.
Then, it uses the Formatter to convert the normalized data into styled, readable strings.
Finally, the Transporter outputs the fully formatted log to the console or other targets.

Built-in Transporters

Logry comes with two built-in transporters, automatically selected based on your runtime environment:

Platform Transporter Styling mechanism
Node.js NodeConsoleTransporter Prints logs to the terminal using ANSI styles
Browser BrowserConsoleTransporter Prints logs to the DevTools console using CSS styles

These built-ins provide clean and consistent output across platforms with minimal overhead.

โ€ผ๏ธ Currently, Logry does not support external transporter injection.

๐Ÿ”ฎ For advanced or custom delivery mechanisms (e.g., file output, remote logging),
it is recommended to implement custom handlers.


Shuffle Tracks Button Normalizer

Before any log is formatted or transported, Logry first passes it through a platform-aware normalizer.
This process ensures a consistent structure, reliable data types, and full flexibility for customization.

What it does

The Normalizer transforms a raw log input into a normalized shape, handling core parts like:

  • timestamp
  • id
  • level
  • scope
  • message
  • meta
  • context
  • pid (Node.js only)
  • hostname (Node.js only)

Each part has a dedicated normalizer, all of which can be overridden via custom logic.

Customization

Every normalizer supports a customNormalizer function, letting you override default behavior:

id: {
  customNormalizer: ({ part }) => `node-${part}`, // e.g., "default" โ†’ "node-default"
}

You can also fine-tune behavior using extra options per part.

Part Extra Options Available
timestamp style, useUTC, showTimeOnly
level style
scope separator
meta errorStackLines

Platform Awareness

Normalization logic in Logry adapts based on the runtime environment,
allowing logs to be tailored specifically for Node.js or Browser contexts.

For example:

  • Timestamps appear as full ISO 8601 UTC strings in Node, but as simplified human-readable strings in the browser.

You can define environment-specific behavior using the normalizerConfig structure.
It can be set globally in logry(...), scoped to a logger.child(...), or overridden per log method:

normalizerConfig: {
  node: {
    timestamp: {
      style: "iso",
      useUTC: true,
    },
    level: {
      style: "upper",
    },
    meta: {
      errorStackLines: 10,
    },
  },
  browser: {
    timestamp: {
      style: "pretty",
      useUTC: false,
    },
    level: {
      style: "lower",
    },
  },
},

Artist Palette Formatter

The Formatter layer takes the normalized parts of a log and converts them into the final output strings.
These outputs are styled, easy to read, and can include optional color coding.

What it does

The Formatter receives normalized data and produces formatted strings (or structured content) ready for display.
Handled parts include:

  • timestamp
  • id
  • level
  • scope
  • message
  • meta
  • context
  • pid (Node.js only)
  • hostname (Node.js only)

Each part has its own formatter. All formatters support optional style customizations and can be overridden with custom logic.

Customization

Every formatter supports a customFormatter function, letting you override default behavior:

level: {
  customFormatter: ({ part, rawPart }) => { // `rawPart` is the original unnormalized value
    if (rawPart === "warn") return `โš ๏ธ ${part}`;
    return part;
  },
}

You can also fine-tune behavior using extra options per part.

Platform Part Extra Options Available
Node.js ALL ansiColor
- scope showOnlyLatest, seperator
- meta format, depth (for format: "raw")
- context format, depth (for format: "raw")
Browser ALL cssStyle
- scope showOnlyLatest, seperator
- meta format
- context format

Platform Awareness

Formatter behavior automatically adapts to the runtime platform, whether it is Node.js or the browser.
This ensures that log outputs remain clear, styled, and consistent across environments.

The output behavior varies depending on the platform:

Platform Format output Styling mechanism
Node.js Returns { [k]: string, withAnsiColor: string } Uses ANSI escape codes (e.g. \x1b[31m)
Browser Returns { [k]: string, cssStyle: string } Uses %c and inline CSS

In the browser, the final result will be used with console.log("%c...%c...%c...", styleA, styleB, ...), allowing for per-part CSS styling.

For example:

  • Timestamps appear as full ISO strings with ANSI colors in Node.js and as simplified text styled with CSS in the browser.
  • Meta shows full depth in Node, but gets a prefix like โ€œMETA | โ€œ in Browser.
  • Some parts (like level) can be hidden in one platform but shown in another.

You can define environment-specific behavior using the formatterConfig structure.
It can be set globally in logry(...), scoped to a logger.child(...), or overridden per log method:

formatterConfig: {
  node: {
    timestamp: {
      ansiColor: "\x1b[33m",
    },
    meta: {
      depth: null,
    },
    lineBreaksAfter: 2,
  },
  browser: {
    timestamp: {
      cssStyle: "font-weight: bold; color: orange;",
    },
    meta: {
      prefix: "META | ",
    },
    level: {
      hide: true,
    },
  },
},

Package Handlers and HandlerManager

Each Logger instance internally binds to a HandlerManager, inherited from its LoggerCore.
This module orchestrates all registered log handlers, manages asynchronous tasks, and controls error recovery and flush strategies.

What Are Handlers?

Handlers are modular units that define where and how a log should be delivered,
whether to the console, a file, or a third-party service.

๐Ÿ’ก They receive the raw log payload and can process it synchronously or asynchronously.

You can add or remove them dynamically at runtime:

logger.addHandler(handler, id?, position?); // Adds a handler, returns the assigned ID
logger.removeHandler(id); // Removes the handler by ID

What Is the HandlerManager?

The HandlerManager orchestrates all registered handlers.
It ensures your logs are reliably processed, even in asynchronous or failure-prone environments.

  • โ™ป๏ธ Handler lifecycle
    • Initializes handlers on registration
    • Optionally handles errors via a configurable onError callback
    • Disposes each handler safely when no longer needed
  • ๐Ÿ”Ž Async task tracking
    • Tracks all pending asynchronous log operations
    • Ensures that every delivery completes or fails safely
  • โฑ๏ธ Flush support
    • Call flush(timeout?) to wait for all pending handler tasks
    • Supports flushStrategy for scheduled or event-based flush triggers
  • ๐Ÿ›‘ Error recovery
    • Catches errors during log handling
    • Reports them via the onError callback (with handler ID and context)
  • ๐Ÿงผ Resource cleanup
    • dispose() cancels flush strategies, removes all handlers, and clears internal states

Whether youโ€™re logging to local files, remote servers, or cloud dashboards,
the HandlerManager makes it reliable and composable ๐ŸŒ

Creating Custom Handlers with BaseHandler

To create your own log destinations, you can extend the BaseHandler class.

๐Ÿงฑ This class provides core functionalities such as payload preparation, normalization, formatting,
and safe execution flow, so you only need to focus on implementing the log delivery logic.

The key method to implement is:

abstract handle(rawPayload: RawPayload): Promise<void>;

Here are some useful protected methods you can use inside your custom handler:

Method Signature Description
normalize (rawPayload: RawPayload) => Promise<NormalizedPayload> Normalize the raw log payload into a consistent format.
format (normalized: NormalizedPayload) => NodeFormattedPayload Format the normalized payload into a string or structured output.
compose (rawPayload: RawPayload) => Promise<string> Normalize, format, and compose the raw payload into the final string message.
toJson (rawPayload: RawPayload, options?: { useNormalizer?: boolean; space?: number }) => Promise<string> Convert the raw payload into a JSON string, optionally normalized and pretty-printed.

Example implementation:

class MyCustomHandler extends BaseHandler {
  async handle(rawPayload: RawPayload) {
    const message = await this.compose(rawPayload);
    await sendToExternalService(message);
  }
}

This makes it easy to build reliable and composable handlers,
whether you write files, send to remote servers, or push logs to cloud ingestion pipelines โ˜๏ธ


Hammer and Wrench Devtools

Logry includes small tools to help you debug and inspect logger internals.

inspectLoggerCores()

List all registered LoggerCore instances.

import { inspectLoggerCores } from "logry/devtools";

inspectLoggerCores();

Helps you verify how loggers are created and linked.

inspectHandlerConfig(logger)

Show the resolved handler config for a given logger.

import { inspectHandlerConfig } from "logry/devtools";

inspectHandlerConfig(myLogger);

Good for checking which rules and tasks are active.


Construction Development Mode Detection

This function detects whether the runtime is in development mode.
It is primarily used to control internal logging and error reporting within the library, such as internal-log and internal-error messages.

  • In Node.js, it checks the NODE_ENV environment variable:

    • Returns true if NODE_ENV is not set to 'production'.
    • Defaults to true (development) if NODE_ENV is undefined. x
  • In Browsers, it checks the global flag __LOGRY_DEV__:

    • Returns true if the flag is truthy.
    • Defaults to false (production) if undefined.

This setup assumes Node defaults to development mode for easier local testing, while browsers default to production to avoid unnecessary debug logs.