carlos-menezes/caravan

A flexible, transport-based TypeScript-first logging system for JavaScript applications.

70

stars

23

commits

TypeScript

primary language

Sep 3, 2026

updated

README

/ˈkærəvæn/

header

A small, typed logging library for JavaScript applications. Caravan gives you a core logger with levels, context and pluggable transports, plus a set of transport packages for where the logs actually go.

Packages

Usage

Basic logging

Every logger needs an id and at least one transport to actually send records somewhere. Log methods exist for each level (trace, debug, info, warn, error, fatal) and accept a message, a context object or both.

import { createLogger } from "@caravan-logger/logger";
import { createStreamTransport } from "@caravan-logger/transport-stream";

const logger = createLogger("app", {
  level: "INFO",
  transports: [createStreamTransport({ stream: process.stdout })],
});

logger.info("server started", { port: 3000 });
logger.warn({ port: 3000, retrying: true });
logger.error("failed to bind port", { port: 3000 });

await logger.flush();

Logging to a file

createStreamTransport accepts any Node.js Writable. Each record is written as its own line (eol controls the line ending and defaults to "lf").

import { createWriteStream } from "node:fs";
import { createLogger } from "@caravan-logger/logger";
import { createStreamTransport } from "@caravan-logger/transport-stream";

const logger = createLogger("app", {
  transports: [createStreamTransport({ stream: createWriteStream("app.log", { flags: "a" }) })],
});

logger.info("server started", { port: 3000 });
await logger.flush();

Context and child loggers

Context passed to createLogger is merged into every record the logger writes. Child loggers created with inherit pick up the parent's level, transports and context, and can extend the context further without mutating the parent.

const requestLogger = createLogger({
  inherit: { from: logger },
  context: { requestId: "abc-123" },
});

requestLogger.warn("slow response", { durationMs: 820 });

// per-call context is merged with the logger's own context
requestLogger.info("handled", { statusCode: 200 });

Multiple and per-transport levels

A logger can fan a record out to several transports at once. Each transport can define its own level, so it only receives records at or above that level, regardless of the logger's own level.

import { createStreamTransport } from "@caravan-logger/transport-stream";

const logger = createLogger("app", {
  level: "TRACE",
  transports: [
    // everything goes to stdout
    createStreamTransport({ stream: process.stdout }),
    // only warnings and above go to the error stream
    createStreamTransport({ stream: process.stderr, level: "WARN" }),
  ],
});

Custom formatting and transports

createStreamTransport writes to any Node.js or web WritableStream and accepts a format function to control how records are serialized. You can also build your own transport with createTransport for destinations other than a stream (e.g. an HTTP endpoint or a database).

import { createTransport } from "@caravan-logger/logger";
import { createStreamTransport } from "@caravan-logger/transport-stream";

const prettyStream = createStreamTransport({
  stream: process.stdout,
  format: (record) => `[${record.level}] ${record.message} ${JSON.stringify(record.context)}`,
});

const remoteTransport = createTransport({
  write: async (record) => {
    await fetch("https://logs.example.com/ingest", {
      method: "POST",
      body: JSON.stringify(record),
    });
  },
});

const logger = createLogger("app", {
  transports: [prettyStream, remoteTransport],
});

Middleware

Middleware runs on every record before it reaches transports, in the order provided, and can transform or drop records (by returning undefined). Child loggers inherit their parent's middleware. This is useful for redacting sensitive fields, sampling, or enriching records.

const logger = createLogger("app", {
  transports: [createStreamTransport({ stream: process.stdout })],
  context: { password: "4f53a5c4-bfa5-4035-aba7-11e29a90c260" },
  middleware: [
    (record) => {
      if (!record.context?.["password"]) return record;
      return {
        ...record,
        context: { ...record.context, password: "*".repeat(5) },
      };
    },
  ],
});

logger.info("user authenticated");

Flushing

Transports may buffer or perform async work (e.g. network writes). Call flush() to wait for all pending writes on a logger's transports to settle before your process exits.

await logger.flush();

Benchmarks

Caravan's core is benchmarked against Pino in packages/benchmark, covering basic messages, messages with context and child loggers. See its README for methodology and results.

pnpm bench

Development

This is a pnpm monorepo managed with Turbo.

pnpm install

# build
pnpm build:all           # everything
pnpm build:caravan       # only @caravan-logger/* packages
pnpm build:packages      # only @packages/* packages

# test, lint, format
pnpm test
pnpm lint
pnpm fmt

Contributors

carlos-menezes

23 commits

carlos-menezes/caravan

A flexible, transport-based TypeScript-first logging system for JavaScript applications.

70

stars

23

commits

TypeScript

primary language

Sep 3, 2026

updated

README

/ˈkærəvæn/

header

A small, typed logging library for JavaScript applications. Caravan gives you a core logger with levels, context and pluggable transports, plus a set of transport packages for where the logs actually go.

Packages

Usage

Basic logging

Every logger needs an id and at least one transport to actually send records somewhere. Log methods exist for each level (trace, debug, info, warn, error, fatal) and accept a message, a context object or both.

import { createLogger } from "@caravan-logger/logger";
import { createStreamTransport } from "@caravan-logger/transport-stream";

const logger = createLogger("app", {
  level: "INFO",
  transports: [createStreamTransport({ stream: process.stdout })],
});

logger.info("server started", { port: 3000 });
logger.warn({ port: 3000, retrying: true });
logger.error("failed to bind port", { port: 3000 });

await logger.flush();

Logging to a file

createStreamTransport accepts any Node.js Writable. Each record is written as its own line (eol controls the line ending and defaults to "lf").

import { createWriteStream } from "node:fs";
import { createLogger } from "@caravan-logger/logger";
import { createStreamTransport } from "@caravan-logger/transport-stream";

const logger = createLogger("app", {
  transports: [createStreamTransport({ stream: createWriteStream("app.log", { flags: "a" }) })],
});

logger.info("server started", { port: 3000 });
await logger.flush();

Context and child loggers

Context passed to createLogger is merged into every record the logger writes. Child loggers created with inherit pick up the parent's level, transports and context, and can extend the context further without mutating the parent.

const requestLogger = createLogger({
  inherit: { from: logger },
  context: { requestId: "abc-123" },
});

requestLogger.warn("slow response", { durationMs: 820 });

// per-call context is merged with the logger's own context
requestLogger.info("handled", { statusCode: 200 });

Multiple and per-transport levels

A logger can fan a record out to several transports at once. Each transport can define its own level, so it only receives records at or above that level, regardless of the logger's own level.

import { createStreamTransport } from "@caravan-logger/transport-stream";

const logger = createLogger("app", {
  level: "TRACE",
  transports: [
    // everything goes to stdout
    createStreamTransport({ stream: process.stdout }),
    // only warnings and above go to the error stream
    createStreamTransport({ stream: process.stderr, level: "WARN" }),
  ],
});

Custom formatting and transports

createStreamTransport writes to any Node.js or web WritableStream and accepts a format function to control how records are serialized. You can also build your own transport with createTransport for destinations other than a stream (e.g. an HTTP endpoint or a database).

import { createTransport } from "@caravan-logger/logger";
import { createStreamTransport } from "@caravan-logger/transport-stream";

const prettyStream = createStreamTransport({
  stream: process.stdout,
  format: (record) => `[${record.level}] ${record.message} ${JSON.stringify(record.context)}`,
});

const remoteTransport = createTransport({
  write: async (record) => {
    await fetch("https://logs.example.com/ingest", {
      method: "POST",
      body: JSON.stringify(record),
    });
  },
});

const logger = createLogger("app", {
  transports: [prettyStream, remoteTransport],
});

Middleware

Middleware runs on every record before it reaches transports, in the order provided, and can transform or drop records (by returning undefined). Child loggers inherit their parent's middleware. This is useful for redacting sensitive fields, sampling, or enriching records.

const logger = createLogger("app", {
  transports: [createStreamTransport({ stream: process.stdout })],
  context: { password: "4f53a5c4-bfa5-4035-aba7-11e29a90c260" },
  middleware: [
    (record) => {
      if (!record.context?.["password"]) return record;
      return {
        ...record,
        context: { ...record.context, password: "*".repeat(5) },
      };
    },
  ],
});

logger.info("user authenticated");

Flushing

Transports may buffer or perform async work (e.g. network writes). Call flush() to wait for all pending writes on a logger's transports to settle before your process exits.

await logger.flush();

Benchmarks

Caravan's core is benchmarked against Pino in packages/benchmark, covering basic messages, messages with context and child loggers. See its README for methodology and results.

pnpm bench

Development

This is a pnpm monorepo managed with Turbo.

pnpm install

# build
pnpm build:all           # everything
pnpm build:caravan       # only @caravan-logger/* packages
pnpm build:packages      # only @packages/* packages

# test, lint, format
pnpm test
pnpm lint
pnpm fmt

Contributors

carlos-menezes

23 commits

Languages

TypeScript

99.9%