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

@callsy/logging

v1.0.0

Published

Logging utilities.

Readme

@callsy/logging

A structured logging library for Node.js and TypeScript with a fluent builder API and pluggable buffer system for batched, async log delivery.

Features

  • Fluent builder API — immutable, chainable methods that return new instances. Build up tags, data, and severity without mutation.
  • Pluggable buffer system — attach one or more buffers (ConsoleBuffer, OtlpBuffer, or your own) to control where logs are sent.
  • Five severity levels — DEBUG, INFO, WARNING, ERROR, and CRITICAL with convenience methods for each.
  • Structured logging — attach tags and arbitrary key-value data to every log entry.
  • Automatic caller extraction — ERROR and CRITICAL logs automatically capture the calling class and method name from the stack trace.
  • Batched async flushing — logs are buffered and flushed in configurable batches on a timer or when the buffer is full. No log-per-request overhead.
  • OTLP support — built-in OtlpBuffer sends logs to any OpenTelemetry Protocol-compatible backend (Grafana, Datadog, etc.).
  • Serverless-friendly — optional backgroundTask wrapper integrates with waitUntil and similar patterns for edge and serverless runtimes.
  • TypeScript-first — fully typed with exported interfaces, generics, and declaration files. Zero runtime dependencies.

Installation

npm install @callsy/logging

Requirements: Node.js >= 18.0.0

Quick start

import { Logging, ConsoleBuffer } from '@callsy/logging'

// 1. Create a buffer — at least one buffer is required for logs to go anywhere.
const buffer = new ConsoleBuffer({})

// 2. Create a logger with the buffer attached.
const logger = new Logging({ buffers: [buffer] })

// 3. Log a message.
logger.info('Application started')
// Console output: [INFO] Application started

// 4. Chain tags and data for structured context.
logger
  .withTag('AUTH')
  .withData({ userId: 'user-123' })
  .info('Login successful')
// Console output: [INFO] Login successful

Every with* method returns a new Logging instance — the original is never mutated. You can safely derive specialized loggers from a shared base.

Buffers

Logging itself does not decide where logs go — buffers do. At least one buffer must be provided, otherwise calls to .info(), .error(), etc. are silent no-ops. Multiple buffers can be attached to send logs to several destinations simultaneously.

| | ConsoleBuffer | OtlpBuffer | Custom (FlushableBuffer) | |---|---|---|---| | Destination | console.log | OTLP-compatible HTTP endpoint | Your flushAction callback | | Output format | [LEVEL] message | OTLP JSON over HTTP | Defined by you | | Use case | Local development, debugging | Production observability (Grafana, Datadog) | Any custom backend (CloudWatch, file, webhook) | | Configuration | None required | HTTP endpoint, method, headers | flushAction callback | | External dependency | None | OTLP-compatible collector | Varies |

ConsoleBuffer

The simplest buffer. Logs each entry to the console in [LEVEL] message format. Best for local development and debugging.

import { Logging, ConsoleBuffer } from '@callsy/logging'

const buffer = new ConsoleBuffer({})
const logger = new Logging({ buffers: [buffer] })

logger.info('Server listening on port 3000')
// [INFO] Server listening on port 3000

logger.error('Connection refused')
// [ERROR] Connection refused

OtlpBuffer

Sends logs to any OpenTelemetry Protocol-compatible backend. Logs are batched and sent asynchronously — they are not sent one-by-one.

import { Logging, OtlpBuffer } from '@callsy/logging'

const buffer = new OtlpBuffer({
  httpEndpoint: 'https://otel-collector.example.com/v1/logs',
  httpMethod: 'POST',
  httpHeaders: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_API_KEY'
  },
  resourceAttributes: { 'service.name': 'my-app', 'environment': 'production' },
  scopeName: 'my-app-logger',
  scopeVersion: '1.0.0'
})

const logger = new Logging({ buffers: [buffer] })
logger.info('Order processed')

| Property | Type | Required | Default | Description | |---|---|---|---|---| | httpEndpoint | string | Yes | — | Full URL of the OTLP logs endpoint. | | httpMethod | string | Yes | — | HTTP method (typically "POST"). | | httpHeaders | Record<string, string> | Yes | — | HTTP headers (content type, auth, etc.). | | resourceAttributes | JsonDict | No | {} | OTLP resource attributes (e.g. service name). | | scopeName | string | No | "callsy-logging" | OTLP instrumentation scope name. | | scopeVersion | string | No | undefined | OTLP instrumentation scope version. |

Custom buffers with FlushableBuffer

FlushableBuffer is the generic base class that both ConsoleBuffer and OtlpBuffer extend. Create custom buffers by instantiating it directly with a flushAction callback or by subclassing it.

Direct instantiation:

import { FlushableBuffer, Logging } from '@callsy/logging'
import type { Log } from '@callsy/logging'

const buffer = new FlushableBuffer<Log>({
  flushAction: async (logs: Log[]) => {
    await fetch('https://my-backend.com/logs', {
      method: 'POST',
      body: JSON.stringify(logs)
    })
  },
  maxItems: 50,
  flushIntervalMs: 2000
})

const logger = new Logging({ buffers: [buffer] })
logger.info('This goes to my custom backend')

Subclassing for reusable buffers:

import { FlushableBuffer } from '@callsy/logging'
import type { Log } from '@callsy/logging'
import type { FlushableBufferProps } from '@callsy/logging'

type CloudWatchBufferProps = Omit<FlushableBufferProps<Log>, 'flushAction'> & {
  logGroupName: string
  logStreamName: string
}

class CloudWatchBuffer extends FlushableBuffer<Log> {
  constructor(props: CloudWatchBufferProps) {
    super({
      ...props,
      flushAction: async (logs: Log[]) => {
        // Send logs to CloudWatch.
        await sendToCloudWatch(props.logGroupName, props.logStreamName, logs)
      }
    })
  }
}

FlushableBuffer configuration:

| Property | Type | Required | Default | Description | |---|---|---|---|---| | flushAction | (buffer: T[]) => Promise<void> | Yes | — | Async callback invoked when the buffer flushes. Receives the batch of buffered items. | | backgroundTask | (promise: Promise<unknown>) => void | No | No-op | Wraps flush promises. Use with Vercel waitUntil or similar serverless APIs. | | maxItems | number | No | 100 | Maximum items before an immediate flush is triggered. | | flushIntervalMs | number | No | 1000 | Milliseconds before a time-based flush. | | maxFlushRecursionDepth | number | No | 5 | Maximum recursive flushes when new items arrive during a flush. |

Fluent API

Every with* method returns a new Logging instance, leaving the original unchanged. This makes it safe to create a base logger and derive specialized versions for different modules or requests.

Configuration methods

| Method | Returns | Description | |---|---|---| | withTag(tag) | Logging | Add a single tag. Tags accumulate across chained calls. | | withTags(tags) | Logging | Add multiple tags at once. | | withData(data) | Logging | Merge structured key-value data. Later calls overwrite matching keys. | | withUniqueId(id) | Logging | Set a custom unique ID. Auto-generated UUID if not set. | | withSeverity(severity) | Logging | Set severity directly using the Severity enum. | | withMessage(message) | Logging | Set a default message (used if log() is called without one). | | withBuffer(buffer) | Logging | Attach an additional buffer. | | withInfo() | Logging | Shorthand for withSeverity(Severity.INFO). | | withWarning() | Logging | Shorthand for withSeverity(Severity.WARNING). | | withError() | Logging | Shorthand for withSeverity(Severity.ERROR). | | withDebug() | Logging | Shorthand for withSeverity(Severity.DEBUG). | | withCritical() | Logging | Shorthand for withSeverity(Severity.CRITICAL). |

Logging methods

| Method | Signature | Description | |---|---|---| | debug | debug(message?: string): void | Log at DEBUG level. | | info | info(message?: string): void | Log at INFO level. | | warning | warning(message?: string): void | Log at WARNING level. | | error | error(message?: string): void | Log at ERROR level. Caller automatically captured. | | critical | critical(message?: string): void | Log at CRITICAL level. Caller automatically captured. | | log | log(message?: string): void | Log at the currently set severity (default: INFO). |

Deriving loggers

import { Logging, ConsoleBuffer } from '@callsy/logging'

const buffer = new ConsoleBuffer({})

// Base logger shared across the application.
const baseLogger = new Logging({ buffers: [buffer] })

// Derive a logger for the auth module.
const authLogger = baseLogger.withTag('AUTH')

// Derive further for a specific request.
const requestLogger = authLogger
  .withData({ requestId: 'req-abc', userId: 'user-123' })

requestLogger.info('Token validated')
requestLogger.warning('Token expires in 5 minutes')
requestLogger.error('Token refresh failed')

Severity levels

| Level | Enum value | Caller auto-captured | Typical use | |---|---|---|---| | DEBUG | Severity.DEBUG | No | Verbose diagnostic information for development. | | INFO | Severity.INFO | No | General operational events (startup, request handled). | | WARNING | Severity.WARNING | No | Unexpected situations that are not yet errors. | | ERROR | Severity.ERROR | Yes | Failures that require attention but the system continues. | | CRITICAL | Severity.CRITICAL | Yes | Severe failures that may require immediate intervention. |

When the severity is ERROR or CRITICAL, the library automatically captures the calling class and method name (e.g. "ShopService.sync") from the stack trace and includes it as the caller field in the log entry.

Log entry structure

Each buffer receives Log objects with the following shape:

interface Log {
  timestamp: number      // Unix timestamp in milliseconds.
  data: JsonDict         // Structured key-value data attached via withData().
  level: Severity        // The severity level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
  message: string        // The log message.
  uniqueId: string       // Auto-generated UUID or custom ID from withUniqueId().
  tags: string[]         // Tags attached via withTag() / withTags().
  caller?: string        // "ClassName.MethodName" — present for ERROR and CRITICAL only.
}

Multiple buffers

Attach multiple buffers to send logs to several destinations simultaneously. Each call to .log() pushes the entry to every attached buffer.

import { Logging, ConsoleBuffer, OtlpBuffer } from '@callsy/logging'

const consoleBuffer = new ConsoleBuffer({})
const otlpBuffer = new OtlpBuffer({
  httpEndpoint: 'https://otel-collector.example.com/v1/logs',
  httpMethod: 'POST',
  httpHeaders: { 'Content-Type': 'application/json' }
})

// Logs go to both console and the OTLP backend.
const logger = new Logging({ buffers: [consoleBuffer, otlpBuffer] })
logger.info('Visible everywhere')

You can also use withBuffer() to add buffers after construction:

const logger = new Logging({ buffers: [consoleBuffer] })
  .withBuffer(otlpBuffer)

Serverless integration

In serverless environments (Vercel, AWS Lambda), the process can terminate before async buffer flushes complete. The backgroundTask option on any buffer wraps flush promises so the runtime keeps the function alive until they resolve.

import { Logging, OtlpBuffer } from '@callsy/logging'
import { waitUntil } from '@vercel/functions'

const buffer = new OtlpBuffer({
  httpEndpoint: 'https://otel-collector.example.com/v1/logs',
  httpMethod: 'POST',
  httpHeaders: { 'Content-Type': 'application/json' },
  backgroundTask: (promise) => waitUntil(promise)
})

const logger = new Logging({ buffers: [buffer] })

export async function GET(request: Request) {
  logger.withTag('API').info('Request received')
  return new Response('OK')
}

Exports

Everything is exported from the package entry point:

// Classes
import {
  Logging,              // Main logging class with fluent builder API.
  ConsoleBuffer,        // Logs to console in [LEVEL] message format.
  OtlpBuffer,           // Sends logs to OTLP-compatible backends.
  FlushableBuffer,      // Generic base class for custom buffers.
  Severity,             // Enum: DEBUG, INFO, WARNING, ERROR, CRITICAL.
  LoggingError,         // Error subclass thrown by OtlpBuffer on HTTP failures.
  otlpSeverityNumber    // Map from Severity to OTLP severity numbers.
} from '@callsy/logging'

// Types
import type {
  Log,                  // Shape of a single log entry.
  LogProps,             // Constructor props for Logging.
  JsonDict,             // Record<string, JsonAnyValue> — structured data type.
  JsonPrimitive         // string | number | boolean | null | Date.
} from '@callsy/logging'

Error handling

The library is designed to keep your application running. Buffer failures are caught and retried — they never crash your process.

| Scenario | Behavior | |---|---| | No buffers attached | .log() / .info() / etc. are safe no-ops. No error thrown. | | OtlpBuffer HTTP request fails | LoggingError thrown internally. Buffered items re-queued for retry on the next flush cycle. | | flushAction callback throws | Error logged to console.error. Buffered items re-queued for retry on the next flush cycle. | | Max recursion depth reached during retries | Warning logged to console.warn. Remaining items stay buffered for the next trigger. | | log() called with no message and no withMessage() | Logs with the message "No message". |

License

ISC