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

@bikiran/logger

v1.0.0

Published

Structured, leveled logging with child loggers, pluggable formatters, and transports. Zero dependencies.

Readme

@bikiran/logger

Structured, leveled logging for Node.js with child loggers, pluggable formatters, and transports.

Zero dependencies. TypeScript-first. Fully portable.

npm version License: MIT TypeScript Node.js


Table of Contents


Installation

# npm
npm install @bikiran/logger

# yarn
yarn add @bikiran/logger

# pnpm
pnpm add @bikiran/logger

Quick Start

import { logger } from "@bikiran/logger";

// Simple messages
logger.info("Server started", { port: 3000 });
logger.error("Connection failed", { error: new Error("timeout") });

// Scoped child logger
const wsLogger = logger.child({ module: "websocket" });
wsLogger.info("Client connected", { clientIp: "10.0.0.1" });

Output (JSON, default):

{"timestamp":"2026-01-15T10:30:00.000Z","level":"info","message":"Server started","port":3000}
{"timestamp":"2026-01-15T10:30:00.123Z","level":"error","message":"Connection failed","error":{"message":"timeout","name":"Error","stack":"..."}}
{"timestamp":"2026-01-15T10:30:00.456Z","level":"info","message":"Client connected","module":"websocket","clientIp":"10.0.0.1"}

Concept & Design

Problem

Raw console.log calls produce unstructured output that is:

  • Hard to parse in log aggregation systems (ELK, Loki, CloudWatch)
  • Missing timestamps, severity levels, and context
  • Scattered across the codebase with no consistency

Solution

This package provides a single, structured logging interface built around five principles:

  1. Structured Output — Every log is a JSON line with timestamp, level, message, and metadata. Machine-parseable, grep-friendly.

  2. Hierarchical Context — Child loggers inherit parent metadata and add their own scope (module, request ID, client IP). No manual context threading.

  3. Separation of ConcernsWhat to log (Logger) is separated from how to format (Formatter) and where to send (Transport).

  4. Performance by Default — Level checks happen before formatting. If a log is suppressed, zero string work is done.

  5. Zero Dependencies — No winston, pino, or bunyan. Just TypeScript targeting Node.js built-ins. Install and go.

Data Flow

  logger.info("msg", { key: "val" })
        │
        ▼
  ┌─────────────┐
  │ Level Check  │──── suppressed? → return (no work done)
  └──────┬──────┘
         │ passes
         ▼
  ┌─────────────┐
  │ Merge Meta   │  base meta + parent meta + call-site meta
  └──────┬──────┘
         │
         ▼
  ┌─────────────┐
  │ Serialize    │  Error objects → { message, name, stack, ...props }
  │ Errors       │
  └──────┬──────┘
         │
         ▼
  ┌─────────────┐
  │  Formatter   │  LogEntry → string (JSON or pretty)
  └──────┬──────┘
         │
         ▼
  ┌─────────────┐
  │  Transport   │  string → destination (console, file, network)
  └─────────────┘

API Reference

Default Logger

import { logger } from "@bikiran/logger";

A pre-configured singleton that reads configuration from environment variables. This is the recommended way to use the logger — most code only needs this import.

createLogger(options?)

import { createLogger, prettyFormatter } from "@bikiran/logger";

const customLogger = createLogger({
  level: "debug",
  formatter: prettyFormatter,
  meta: { service: "my-api", version: "2.1.0" },
});

Options:

| Property | Type | Default | Description | | ----------- | ------------------------- | -------------------------------- | --------------------------------- | | level | LogLevel | LOG_LEVEL env or "info" | Minimum severity to emit | | formatter | LogFormatter | jsonFormatter | Converts LogEntry to string | | transport | LogTransport | consoleTransport | Delivers formatted string | | meta | Record<string, unknown> | {} | Base metadata for every log entry | | timestamp | () => string | () => new Date().toISOString() | Timestamp generator |

Log Methods

All log methods share the same signature:

logger.debug(message: string, meta?: Record<string, unknown>): void
logger.info(message: string, meta?: Record<string, unknown>): void
logger.warn(message: string, meta?: Record<string, unknown>): void
logger.error(message: string, meta?: Record<string, unknown>): void
logger.fatal(message: string, meta?: Record<string, unknown>): void

Examples:

// No metadata
logger.info("Server started");

// With metadata
logger.info("Request handled", { method: "GET", path: "/api", durationMs: 42 });

// With error
logger.error("DB query failed", {
  error: new Error("connection refused"),
  query: "SELECT * FROM users",
});

Child Loggers

Child loggers inherit the parent's configuration (level, formatter, transport) and merge additional base metadata:

const appLogger = createLogger({ meta: { service: "my-app" } });

const wsLogger = appLogger.child({ module: "websocket" });
const dbLogger = appLogger.child({ module: "database" });

wsLogger.info("Client connected", { clientIp: "10.0.0.1" });
// → { service: "my-app", module: "websocket", clientIp: "10.0.0.1", ... }

dbLogger.error("Query failed", { table: "users" });
// → { service: "my-app", module: "database", table: "users", ... }

Child loggers can be nested (grandchild, etc.):

const repoLogger = dbLogger.child({ repository: "conversations" });
repoLogger.info("Row inserted", { sessionId: "abc-123" });
// → { service: "my-app", module: "database", repository: "conversations", sessionId: "abc-123" }

Runtime Level Control

logger.getLevel(); // → "info"
logger.setLevel("debug"); // Now debug logs are visible
logger.setLevel("silent"); // Suppress all output

Log Levels

| Level | Priority | When to use | | -------- | -------- | ---------------------------------------------------------- | | debug | 0 | Detailed diagnostic information for troubleshooting | | info | 1 | Normal operational events (startup, connections, requests) | | warn | 2 | Unexpected but handled situations (rate limit, retry) | | error | 3 | Failures requiring attention (DB errors, API failures) | | fatal | 4 | Unrecoverable errors — process should exit after logging | | silent | 5 | Configuration-only: disables all output |

A log is emitted when its level priority is the configured minimum level.


Formatters

JSON Formatter (default)

Single-line JSON — optimized for log aggregation (ELK, Loki, CloudWatch).

import { jsonFormatter } from "@bikiran/logger";

// Output:
// {"timestamp":"2026-01-15T10:30:00.000Z","level":"info","message":"Started","port":3000}

Field order: timestamp → level → message → ...metadata

Pretty Formatter

Colorized, human-readable — designed for local development terminals.

import { createLogger, prettyFormatter } from "@bikiran/logger";

const devLogger = createLogger({ formatter: prettyFormatter });
devLogger.info("Request", { method: "GET", path: "/api" });

// Output:
// 10:30:00.000  INFO Request  method=GET path=/api

Features:

  • ANSI colors per severity level
  • Time-only display (HH:MM:SS.mmm)
  • Key=value metadata pairs
  • Error stacks on separate indented lines

Custom Formatters

Implement the LogFormatter type:

import type { LogFormatter } from "@bikiran/logger";

const csvFormatter: LogFormatter = (entry) => {
  return `${entry.timestamp},${entry.level},"${entry.message}"`;
};

const logger = createLogger({ formatter: csvFormatter });

Or use the field-selection factory:

import { createJsonFormatter } from "@bikiran/logger";

// Only include specific fields
const minimal = createJsonFormatter(["timestamp", "level", "message"]);

Transports

Console Transport (default)

Routes logs to the appropriate console method:

| Log Level | Console Method | Stream | | ---------------- | --------------- | ------ | | debug | console.debug | stdout | | info | console.log | stdout | | warn | console.warn | stderr | | error, fatal | console.error | stderr |

Noop Transport

Discards all output — useful for testing:

import { createLogger, noopTransport } from "@bikiran/logger";

const silentLogger = createLogger({ transport: noopTransport });

Custom Transports

Implement the LogTransport interface:

import type { LogTransport } from "@bikiran/logger";
import fs from "fs";

const fileTransport: LogTransport = {
  write(level, output) {
    fs.appendFileSync("/var/log/app.log", output + "\n");
  },
};

Or use the function factory:

import { createFnTransport } from "@bikiran/logger";

const lines: string[] = [];
const arrayTransport = createFnTransport((level, output) => {
  lines.push(output);
});

Multi-transport — fan out to multiple destinations:

import { createFnTransport, consoleTransport } from "@bikiran/logger";
import type { LogTransport } from "@bikiran/logger";

function createMultiTransport(...transports: LogTransport[]): LogTransport {
  return {
    write(level, output) {
      for (const t of transports) {
        t.write(level, output);
      }
    },
  };
}

const multi = createMultiTransport(consoleTransport, fileTransport);
const logger = createLogger({ transport: multi });

Error Handling

Error objects passed in metadata are automatically serialized:

const err = Object.assign(new Error("connection refused"), {
  code: "ECONNREFUSED",
  port: 5432,
});

logger.error("DB connection failed", { error: err });

Output:

{
  "timestamp": "...",
  "level": "error",
  "message": "DB connection failed",
  "error": {
    "message": "connection refused",
    "name": "Error",
    "stack": "Error: connection refused\n    at ...",
    "code": "ECONNREFUSED",
    "port": 5432
  }
}

This works for any Error subclass and preserves all custom enumerable properties.


Environment Variables

The default logger singleton reads these environment variables at startup:

| Variable | Values | Default | Description | | ------------ | --------------------------------------------------- | ------- | ---------------------------------------------------------------------- | | LOG_LEVEL | debug, info, warn, error, fatal, silent | info | Minimum severity to emit | | LOG_FORMAT | json, pretty | json | Output format | | NODE_ENV | development, production, etc. | — | Auto-selects pretty format in development if LOG_FORMAT is not set |

# Production
LOG_LEVEL=warn node app.js

# Development
NODE_ENV=development node app.js  # auto-pretty format

# Explicit
LOG_LEVEL=debug LOG_FORMAT=pretty node app.js

Usage Patterns

Per-Module Scoping

// In each module, create a child logger with the module name
import { logger } from "@bikiran/logger";

const log = logger.child({ module: "websocket" });

export function onConnect(clientIp: string) {
  log.info("Client connected", { clientIp });
}

Request-Scoped Context

// Create a child logger per request with correlation ID
function handleRequest(req: Request) {
  const reqLogger = logger.child({
    requestId: req.headers.get("x-request-id"),
    method: req.method,
    path: req.url,
  });

  reqLogger.info("Request started");
  // ... pass reqLogger to downstream services
  reqLogger.info("Request completed", { durationMs: 42 });
}

Service Identification

// In the application entry point, configure the root logger with service info
import { createLogger } from "@bikiran/logger";

export const logger = createLogger({
  meta: {
    service: "my-api",
    version: "1.5.0",
    env: process.env.NODE_ENV || "development",
  },
});

// All logs (including child loggers) will include service, version, env

Conditional Debug Logging

// Expensive debug output is skipped entirely if level > debug
logger.debug("Full payload dump", {
  payload: JSON.parse(rawBody),
});

// For truly expensive operations, guard explicitly:
if (logger.getLevel() === "debug") {
  const analysis = performExpensiveAnalysis(data);
  logger.debug("Analysis result", { analysis });
}

Express / Koa Middleware

import { createLogger, prettyFormatter } from "@bikiran/logger";

const logger = createLogger({
  meta: { service: "api-gateway" },
  formatter: process.env.NODE_ENV === "development" ? prettyFormatter : undefined,
});

app.use((req, res, next) => {
  const start = Date.now();
  const reqLog = logger.child({ requestId: req.id, method: req.method, path: req.path });

  res.on("finish", () => {
    reqLog.info("Request completed", {
      status: res.statusCode,
      durationMs: Date.now() - start,
    });
  });

  next();
});

Testing

Testing Your Code That Uses the Logger

Use noopTransport to suppress output, or createFnTransport to capture logs:

import { createLogger, createFnTransport, noopTransport } from "@bikiran/logger";
import type { LogLevel } from "@bikiran/logger";

// Capture logs for assertions
const captured: { level: LogLevel; output: string }[] = [];
const testLogger = createLogger({
  level: "debug",
  transport: createFnTransport((level, output) =>
    captured.push({ level, output }),
  ),
  timestamp: () => "FIXED",
});

// Inject testLogger into your service
const service = new MyService(testLogger);
service.doWork();

expect(captured).toHaveLength(1);
expect(captured[0].level).toBe("info");

Running the Package Tests

# Run tests
npm test

# Watch mode
npm run test:watch

# Coverage
npm run test:coverage

TypeScript

Full TypeScript support with exported types:

import type {
  LogLevel,
  LogEntry,
  LogFormatter,
  LogTransport,
  LoggerOptions,
  ILogger,
  SerializedError,
} from "@bikiran/logger";

// Use ILogger for dependency injection
class UserService {
  constructor(private readonly logger: ILogger) {}

  createUser(name: string) {
    this.logger.info("Creating user", { name });
  }
}

Exported Types

| Type | Description | | ----------------- | ------------------------------------------------------------ | | LogLevel | "debug" \| "info" \| "warn" \| "error" \| "fatal" \| "silent" | | LogEntry | Structured log entry: { timestamp, level, message, meta } | | LogFormatter | Function signature: (entry: LogEntry) => string | | LogTransport | Interface with write(level, output) method | | LoggerOptions | Configuration options for createLogger() | | ILogger | Public interface — use for mocks and dependency injection | | SerializedError | Shape of serialized Error objects in metadata |

Exported Values

| Export | Description | | --------------------- | ---------------------------------------------------- | | logger | Pre-configured default singleton | | Logger | Logger class (for instanceof checks) | | createLogger() | Factory function | | loggerConfig | Resolved environment config ({ level, format }) | | jsonFormatter | JSON line formatter | | prettyFormatter | Colorized terminal formatter | | createJsonFormatter | Factory for custom JSON formatters | | consoleTransport | Default console transport | | noopTransport | Silent transport for testing | | createFnTransport | Factory for function-based transports | | LOG_LEVEL_PRIORITY | Numeric priority map for level comparisons |


Architecture

src/
├── index.ts              # Public API barrel + default singleton
├── logger.service.ts     # Logger class + createLogger() factory
├── logger.types.ts       # All type definitions (zero runtime code)
├── logger.config.ts      # Env-based configuration resolver
├── logger.formatters.ts  # JSON and Pretty formatters
├── logger.transports.ts  # Console, noop, and factory transports
└── logger.test.ts        # Vitest test suite

| File | Responsibility | Dependencies | | ------------------- | ---------------------------------------- | --------------------------------- | | logger.types | Type definitions, LOG_LEVEL_PRIORITY | None | | logger.config | Reads LOG_LEVEL, LOG_FORMAT env vars | logger.types | | logger.formatters | JSON + Pretty string formatters | logger.types | | logger.transports | Console + noop output writers | logger.types | | logger.service | Logger class, createLogger() | All of the above | | index | Singleton creation, barrel re-exports | logger.service, logger.config |

All imports are internal. Zero external dependencies.


Contributing

# Clone
git clone https://github.com/bikirandev/logger.git
cd logger

# Install
npm install

# Dev workflow
npm test           # Run tests
npm run lint       # Type-check
npm run build      # Compile CJS + ESM
  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT © Bikiran