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.
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.
Outline
- ๐ Features at a Glance
- ๐ Output Preview
- ๐ฉ Installation
- ๐ Quick Start
- ๐ Presets
- โ๏ธ Core Concepts
- ๐ซ Transporter
- ๐ Normalizer
- ๐จ Formatter
- ๐ฆ Handlers and HandlerManager
- ๐ ๏ธ Devtools
- ๐ง Development Mode Detection
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
Output Preview
Hereโs how logs look in Node.js vs. the browser:
![]() Console output in Node.js |
![]() Console output in Browser |
Not your style? No worries! Itโs fully customizable.
Installation
npm install logryor use yarn
yarn add logry
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 ErrorCreating 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: {
// ...
},
});
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.
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
- scope: appended
โจ 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 levelresetLevel(): restore to the initial log level
This allows flexible adjustment of log output without needing to recreate logger instances.
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.
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",
},
},
},
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,
},
},
},
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 IDWhat 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 โ๏ธ
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.
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_ENVis not set to 'production'. - Defaults to true (development) if
NODE_ENVis undefined. x
- Returns true if
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.

