/ˈkærəvæn/

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.
@caravan-logger/logger: the core logger. Handles levels,
context inheritance and dispatching records to transports.@caravan-logger/transport-stream: a
transport that writes log records to a Node.js or web WritableStream,
optionally formatting records before they're written.@caravan-logger/transport-datadog: a
transport that sends log records to Datadog's Logs API.@caravan-logger/transport-better-stack: a
transport that sends log records to Better Stack's Logs ingesting API.@caravan-logger/transport-sqlite: a
transport that inserts log records as rows into a SQLite database.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();
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 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 });
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" }),
],
});
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 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");
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();
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
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
23 commits
TypeScript
99.9%
/ˈkærəvæn/

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.
@caravan-logger/logger: the core logger. Handles levels,
context inheritance and dispatching records to transports.@caravan-logger/transport-stream: a
transport that writes log records to a Node.js or web WritableStream,
optionally formatting records before they're written.@caravan-logger/transport-datadog: a
transport that sends log records to Datadog's Logs API.@caravan-logger/transport-better-stack: a
transport that sends log records to Better Stack's Logs ingesting API.@caravan-logger/transport-sqlite: a
transport that inserts log records as rows into a SQLite database.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();
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 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 });
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" }),
],
});
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 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");
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();
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
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
23 commits
TypeScript
99.9%