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

@joinremba/catalog

v0.7.0

Published

Production-ready logging and error event layer for TypeScript backends, built on Pino.

Downloads

1,614

Readme

@joinremba/catalog

npm version License CI Bun

Production-ready logging and error event layer for TypeScript backends, built on Pino.

Features

  • Event-name-first APIcatalog.info("user.created", { userId }) for consistent, searchable log events.
  • Pino under the hood — Ultra-fast structured JSON logging with full Pino transport ecosystem.
  • Automatic sensitive data redaction — Built-in denylist of PII, secrets, and credentials (email, phone, SSN, API keys, tokens, etc.), plus configurable path patterns.
  • Multi-transport — Single transport or array of targets for console, file, rolling files (pino-roll), or external sinks.
  • envTransport() — Auto-configure file transport per environment with zero config.
  • Child loggers.child() and .scope() for request-scoped and module-scoped context binding.
  • Error serializationsafeError() helper strips internals from Error objects for safe API responses.
  • Framework adapters — First-class support for Hono, Express, and Fastify.
  • Audit & Security events — Structured modules for audit trails and security event logging.
  • Webhook forwarding — Batch log delivery to external webhook endpoints with retry and HMAC signing.
  • OpenTelemetry bridge — Correlate logs with active span context.
  • Log sampling — Deterministic or custom sampling to control volume.
  • Cloud log ingestion — Optional @joinremba/core client for remote log batching.
  • Strict TypeScript — Full type exports for Catalog, CatalogOptions, LogLevel, and more.

Installation

bun add @joinremba/catalog

Requires Bun >= 1.3.1.

Quick Start

import { createCatalog } from "@joinremba/catalog";

const catalog = createCatalog({
  service: "my-api",
  environment: process.env.NODE_ENV ?? "development",
  level: "info",
});

catalog.info("app.started", { port: 3000 });
catalog.info("user.created", { userId: "usr_abc123" });
catalog.error("payment.failed", { amount: 4999, currency: "USD" });

Output is newline-delimited JSON (NDJSON) written to stdout by default. Pipe through pino-pretty for dev:

bun run start | bunx pino-pretty

Log Levels

| Level | Method | Usage | | ------- | -------------------- | --------------------------------- | | trace | catalog.trace(...) | Diagnostic detail during dev | | debug | catalog.debug(...) | Development debugging | | info | catalog.info(...) | Normal operational events | | warn | catalog.warn(...) | Unexpected but handled situations | | error | catalog.error(...) | Recoverable errors | | fatal | catalog.fatal(...) | Unrecoverable failures |

Each method supports event-name-first, object-style, and event-only:

catalog.info("user.created", { userId: "usr_abc123" }); // event-name-first
catalog.info({ userId: "usr_abc123", message: "User created" }); // object-style
catalog.info("app.started"); // event only

Set level to the minimum level to emit — events below it are dropped.

Child Loggers

Use .child() to bind context that appears in every log entry:

const reqLog = catalog.child({ requestId: crypto.randomUUID() });
reqLog.info("request.handled", { path: "/api/users" });
// Includes "requestId": "uuid-..."

Use .scope(name) for module-scoped logging:

const dbLog = catalog.scope("database");
dbLog.warn("query.slow", { query: "SELECT ..." });
// Includes "module": "database"

Child loggers inherit all parent options (redaction, mixin, transport, etc.).

Transport

Single Transport

const catalog = createCatalog({
  service: "my-app",
  transport: {
    target: "pino/file",
    options: { destination: "./logs/app.log", mkdir: true },
  },
});

Multi-Transport (array of targets)

const catalog = createCatalog({
  service: "my-app",
  transport: [
    { target: "pino/file", options: { destination: "./logs/app.log" } },
    { target: "pino-roll", options: { file: "./logs/out.log", frequency: "daily", mkdir: true } },
  ],
});

Environment-Aware Transport (envTransport())

Use envTransport() to automatically configure the right transport per NODE_ENV:

import { createCatalog, envTransport } from "@joinremba/catalog";

const catalog = createCatalog({
  service: "my-api",
  ...envTransport(),
});

| NODE_ENV | Transport | Output | Level | | ------------- | ------------------- | ----------------------- | ------ | | development | pino/file | ./logs/dev.log | debug | | test | (none) | silent | silent | | staging | pino/file | ./logs/staging.log | info | | production | pino-roll (daily) | ./logs/production.log | info |

Override the environment explicitly:

const catalog = createCatalog({
  service: "my-api",
  ...envTransport("production"),
});

Import from the subpath:

import { envTransport } from "@joinremba/catalog/env-transport";

Redaction

Catalog automatically redacts a comprehensive set of sensitive field names (case-insensitive, recursive):

password, passwordHash, secret, apiKey, apiSecret, token, accessToken, refreshToken, idToken, ssn, taxId, passportNumber, driverLicense, phone, phoneNumber, mobile, email, emailAddress, accountNumber, routingNumber, iban, swift, cardNumber, cvv, cvc, expiryDate, pin, bvn, nin, bvnHash, ninHash, ip, ipAddress, userAgent, firstName, lastName, fullName, dateOfBirth, dob, address, location, otp, securityAnswer

Add extra fields with the redact option:

const catalog = createCatalog({
  service: "secure-app",
  redact: ["authorization", "x-api-key"],
});

For path-level control, use Pino's built-in redactPaths:

const catalog = createCatalog({
  service: "secure-app",
  redactPaths: ["user.ssn", "headers.authorization"],
});

safeError()

Converts an Error into a plain object safe for API responses — strips stack traces:

import { safeError, createCatalog } from "@joinremba/catalog";

try {
  await riskyOperation();
} catch (err) {
  catalog.error("operation.failed", safeError(err));
  return Response.json(safeError(err), { status: 500 });
}

Output: { "message": "...", "name": "Error", "code": "ECONNREFUSED" }

requestId Mixin

Use the mixin option to inject request context into every log entry:

const catalog = createCatalog({
  service: "my-api",
  mixin: () => ({ requestId: crypto.randomUUID() }),
});

// Every log includes requestId
catalog.info("request.started", { path: req.url });

Child loggers inherit and extend the mixin:

const child = catalog.child({ userId: "usr_42" });
child.info("user.action"); // includes both requestId and userId

Error Event Layer

Use event-name conventions to build a consistent error taxonomy:

catalog.error("db.connection_failed", { database: "users", error: safeError(err) });
catalog.error("payment.declined", { provider: "stripe", reason: "insufficient_funds" });
catalog.fatal("system.out_of_memory", { heapUsed: process.memoryUsage().heapUsed });

Framework adapters map HTTP status to log level automatically:

| Status Range | Log Level | | ------------ | --------- | | 200–399 | info | | 400–499 | warn | | 500+ | error |

Integration with @joinremba/core

Pass a Client instance for cloud log ingestion. Logs buffer locally and flush remotely:

import { createCatalog } from "@joinremba/catalog";
import { createClient } from "@joinremba/core";

const client = createClient({ apiKey: process.env.REMBA_API_KEY });

const catalog = createCatalog({
  service: "my-api",
  environment: "production",
  client, // enables remote ingestion
  transport: { target: "pino-roll", options: { file: "./logs/out.log", frequency: "daily" } },
});

catalog.info("user.created", { userId: "usr_abc123" });
// Flushes every 100 events or on process exit

Sub-Modules

Audit

import { auditLogger } from "@joinremba/catalog/audit";
const audit = auditLogger(catalog);
audit.log({
  action: "user.role_changed",
  actor: "[email protected]",
  resource: "user",
  resourceId: "usr_abc123",
  outcome: "success",
  details: { fromRole: "viewer", toRole: "admin" },
});

Security

import { securityLogger } from "@joinremba/catalog/security";
const security = securityLogger(catalog);
security.log({
  action: "login.failed",
  actor: "[email protected]",
  ip: "203.0.113.42",
  severity: "critical",
  details: { attemptCount: 5 },
});

Webhook Forwarding

import { webhookLogger } from "@joinremba/catalog/webhook";
const webhook = webhookLogger(catalog, {
  targets: [
    {
      url: "https://hooks.example.com/logs",
      level: "warn",
      headers: { Authorization: "Bearer tok_xxx" },
      secret: "whsec_xxx",
    },
  ],
  batchIntervalMs: 5000,
  maxBatchSize: 50,
  retryCount: 2,
});
webhook.warn("rate_limit.exceeded", { userId: "usr_42" });
webhook.stop(); // flush remaining on shutdown

OpenTelemetry Bridge

import { otelBridge } from "@joinremba/catalog/otel";
const otelCatalog = otelBridge(catalog, {
  api: trace, // from @opentelemetry/api
  captureSpanEvents: true,
});
otelCatalog.info("user.created", { userId: "usr_abc123" });
// Every log includes trace_id and span_id

Log Sampling

import { samplingCatalog } from "@joinremba/catalog/sampling";
const sampled = samplingCatalog(catalog, {
  rate: 0.1, // log 10% of events
  level: "debug",
});

Centralized Logging (Microservices)

When running multiple services, you need a way to view logs from all of them in one place. Two approaches:

1. Webhook Aggregator

Each service forward logs to a central receiver using @joinremba/catalog/webhook. The receiver collects logs from all services and writes them to a shared sink.

// Each microservice:
import { createCatalog } from "@joinremba/catalog";
import { webhookLogger } from "@joinremba/catalog/webhook";

const catalog = createCatalog({
  service: "user-service", // different name per service
  environment: "production",
  ...envTransport(),
});

const webhook = webhookLogger(catalog, {
  targets: [
    {
      url: "https://logs.internal:4000/ingest", // central receiver
      level: "warn", // only warns and above
      secret: process.env.WEBHOOK_SECRET!, // HMAC signing
    },
  ],
  batchIntervalMs: 5000,
  maxBatchSize: 50,
});

The central receiver can be a simple Hono/Express server that writes to a shared file, Loki, Elasticsearch, or any other sink.

2. File + Log Shipper

Each service writes to a local file. A log shipper (Vector, Filebeat, Fluentd, or Promtail) tails each file and forwards to a central store (Loki, Elasticsearch, ClickHouse).

// Each service writes its own file:
const catalog = createCatalog({
  service: "payment-service", // identifies the source
  environment: "production",
  ...envTransport("production"), // → ./logs/production.log
});

On each host, run:

# Example: Promtail → Loki
promtail --config.file=/etc/promtail.yml

Where promtail.yml tails ./logs/*.log and adds service and host labels.

Error Alerting

Catalog does not include built-in alerting, but it provides the hooks to trigger alerts:

Via Webhook Forwarding

Use @joinremba/catalog/webhook to send errors to any alert endpoint:

import { webhookLogger } from "@joinremba/catalog/webhook";

const alerts = webhookLogger(catalog, {
  targets: [
    {
      url: "https://hooks.pagerduty.com/integration/...",
      level: "error", // only errors and fatals
      headers: { Authorization: "Bearer tok_xxx" },
    },
    {
      url: "https://hooks.slack.com/services/...",
      level: "error",
    },
  ],
  batchIntervalMs: 2000, // alert quickly
  maxBatchSize: 5,
});

// Elsewhere in your code:
alerts.error("payment.provider_down", { provider: "stripe" });
// Delivered to PagerDuty + Slack within ~2 seconds

Via Cloud Ingestion

If you use the @joinremba/core client, you can set up alert rules in the Remba cloud dashboard — define thresholds (e.g., >5 errors/min) and notification channels (email, Slack, PagerDuty).

Manual Error Tracking

For custom monitoring, pipe the NDJSON output to a log processor:

bun run start | grep '"level":50' | while read -r line; do
  curl -X POST https://alerts.example.com/error -d "$line"
done

Level values: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal.

Framework Adapters

Hono

import { requestIdMiddleware, httpLoggerMiddleware } from "@joinremba/catalog/adapters/hono";
app.use("*", requestIdMiddleware(catalog));
app.use("*", httpLoggerMiddleware(catalog, { excludePaths: ["/health"] }));

Express

import { requestIdMiddleware, httpLoggerMiddleware } from "@joinremba/catalog/adapters/express";
app.use(requestIdMiddleware(catalog));
app.use(httpLoggerMiddleware(catalog));

Fastify

import { requestIdHook, httpLoggerHook } from "@joinremba/catalog/adapters/fastify";
fastify.addHook("onRequest", requestIdHook(catalog));
fastify.addHook("onRequest", httpLoggerHook(catalog));

Configuration Reference

| Option | Type | Default | Description | | ------------- | ---------------------------------------- | -------------- | --------------------------------------------------------- | | service | string | (required) | Service name included as name in every log entry. | | environment | string | — | Environment tag (e.g. "production"). | | level | LogLevel | "info" | Minimum log level to emit. | | redact | string[] | — | Extra sensitive field names to redact (case-insensitive). | | redactPaths | string[] | — | Pino path-based redact patterns (e.g. "user.ssn"). | | transport | TransportOptions \| TransportOptions[] | stdout | Pino transport config object or array of targets. | | destination | PinoDestination | — | Custom writable stream (overrides transport). | | mixin | () => Record<string, unknown> | — | Function returning extra fields to merge into every log. | | base | Record<string, unknown> | — | Static base fields for every log entry. | | client | Client | — | @joinremba/core client for cloud log ingestion. |

TypeScript

All types are exported from the package root:

import type {
  Catalog,
  CatalogOptions,
  LogLevel,
  TransportOptions,
  PinoDestination,
} from "@joinremba/catalog";

| Type | Description | | ------------------ | -------------------------------------------------------------- | | Catalog | Logger instance returned by createCatalog. | | CatalogOptions | Input options for createCatalog. | | LogLevel | "trace" \| "debug" \| "info" \| "warn" \| "error" \| "fatal" | | TransportOptions | { target: string; options?: Record<string, unknown> } | | PinoDestination | { write: (data: string \| Uint8Array) => void } |

Subpackage types:

import type { AuditEvent } from "@joinremba/catalog/audit";
import type { SecurityEvent } from "@joinremba/catalog/security";
import type { WebhookOptions, WebhookTarget } from "@joinremba/catalog/webhook";
import type { OtelBridgeOptions } from "@joinremba/catalog/otel";
import type { SamplingOptions } from "@joinremba/catalog/sampling";
import type { EnvTransportResult } from "@joinremba/catalog/env-transport";
import type { HonoRequestIdOptions, HttpLogOptions } from "@joinremba/catalog/adapters/hono";
import type { ExpressRequestIdOptions } from "@joinremba/catalog/adapters/express";
import type { FastifyRequestIdOptions } from "@joinremba/catalog/adapters/fastify";

Package Exports

| Path | Contents | | ------------------------------------- | ---------------------------------- | | @joinremba/catalog | Main createCatalog + types | | @joinremba/catalog/audit | auditLogger + AuditEvent | | @joinremba/catalog/security | securityLogger + SecurityEvent | | @joinremba/catalog/webhook | webhookLogger + types | | @joinremba/catalog/otel | otelBridge + types | | @joinremba/catalog/sampling | samplingCatalog + types | | @joinremba/catalog/env-transport | envTransport | | @joinremba/catalog/adapters/hono | Hono middleware | | @joinremba/catalog/adapters/express | Express middleware | | @joinremba/catalog/adapters/fastify | Fastify hooks |

Related Packages

Contributing

See CONTRIBUTING.md.

License

MIT — see LICENSE.