Skip to content

Zero-dependency logging

Logging that stays
out of your way

LogTape is a logging library built library-first. Your code records freely; the application decides if, where, and how those logs play back.

  • 0 dependencies
  • every JavaScript runtime
  • 5.3 KB min+gzip
  • TypeScript-native
$deno add jsr:@logtape/logtape
Runs on
Node.js Deno Bun Browsers Edge
my-app.log REC
INFapp·serverListening on port 3000.
DBGapp·dbLoaded user in 12 ms.
INFapp·authUser u_8f21 signed in.
WRNapp·cacheCache miss for session:42.
ERRapp·paymentCharge card_declined on order #10293.

Real LogTape output: leveled, categorized, and structured. Values stay first-class, not stringified.

The unobtrusive contract

No configuration?
No logs.

A library built on LogTape stays completely silent until the application opts in. No setup means no output, no errors, no side effects, so your dependencies never spam a console or force a logger on anyone. The app stays in full control of if, where, and how logs play back.

shopkit/checkout.tsalways present
// inside your library
import { getLogger } from "@logtape/logtape";

const logger = getLogger(["shopkit", "checkout"]);

export function charge(orderId: string) {
  logger.debug("charging order {orderId}", { orderId });
  // ...
}
app/main.tsthis is what REC runs
// inside the application, opt in when you want
import { configure, getConsoleSink } from "@logtape/logtape";

await configure({
  sinks: { console: getConsoleSink() },
  loggers: [
    { category: ["shopkit"], lowestLevel: "debug", sinks: ["console"] },
  ],
});
shopkit.log IDLE
// no configure() yet, so every logger call is a no-op
idle: library logs are dropped

Hierarchical by design

Turn up the logs
right where you need them.

Most JavaScript loggers give every logger a flat name. LogTape arranges them in a tree: a category like ["app", "db"] is a child of ["app"], and configuration flows from parent to child. Raise one subtree to debug while the rest stays at info, and give each library its own namespace so their logs never collide.

logging.tstwo settings
await configure({
  sinks: { console: getConsoleSink() },
  loggers: [
    { category: ["app"], lowestLevel: "info", sinks: ["console"] },
    { category: ["app", "db"], lowestLevel: "debug" },
  ],
});

One setting on ["app", "db"] turns the whole database subtree verbose; siblings keep inheriting info.

Modern by default

Packaged for the way
you build today.

Dual ESM and CommonJS, published to both npm and JSR, with first-class TypeScript types bundled in: no @types side-package, no transitive dependencies. LogTape follows today's packaging standards, so it installs clean and behaves the same everywhere.

package
Module formats
ESM+CommonJS
Published to
npm+JSR
Types
bundled .d.ts
Runtimes
DenoNode.jsBunBrowsersEdge
Dependencies
0

One TypeScript source, built once, shipped to ESM, CommonJS, and JSR alike.

One package family

23 ways to route,
render, and ship logs.

A small, sharp core surrounded by official packages for the sinks, frameworks, and loggers you already use. Add only what you need.

Framework integrations

Drop-in request logging

Adapters

Already on another logger?

Bring your own

Extending it is
just a function.

No base classes, no plugins to register, no lifecycle to learn. A sink, a filter, and a text formatter are each one function of a log record. The only thing that changes is what you return.

type Sink = (record: LogRecord) => voidwhere each record goes
type Filter = (record: LogRecord) => booleanwhether it passes
type TextFormatter = (record: LogRecord) => stringhow it reads
my-sink.tsa real sink
import type { Sink } from "@logtape/logtape";

// batch records, then flush; it is just a function
const sink: Sink = (record) => {
  buffer.push(record);
  if (buffer.length >= 100) flush(buffer);
};

Need to await inside a sink? Sinks stay synchronous by design, but an AsyncSink returns Promise<void> and fromAsyncSink() adapts it into a regular sink, preserving order and catching errors. Async sinks →

Security built in

Secrets never reach
your sinks.

LogTape scrubs sensitive data three ways: redactByField() drops a field by name, redactByPattern() masks a value by its shape, and createHmacPseudonymizer() turns it into a stable token you can still correlate on, without ever logging the original. Each ships with sensible defaults.

Data redaction guide →
field → redacted
password"s3cr3t!"removedby fieldcontact"alice@corp.com"REDACTED@EMAIL.ADDRESSby patternuserId"u_8f21"hmac-sha256:9Fx2aQ…pseudonym
redaction.tsstable pseudonyms
import {
  createHmacPseudonymizer,
  redactByFieldAsync,
} from "@logtape/redaction";

// same input, same token; the original is never logged
const pseudonymize = await createHmacPseudonymizer({ key });

const sink = redactByFieldAsync(getConsoleSink(), {
  fieldPatterns: [/userId/i, /email/i],
  action: pseudonymize,
});

Pseudonyms use keyed HMAC via Web Crypto, so the same value always maps to the same token across records, and back to nothing without the key.

What you ship

Small, fast, and
dependency-free.

5.3 KB, zero dependencies, fully tree-shakable, and quick: on real console output LogTape sits with Pino at the top, several times ahead of winston, bunyan, and log4js. You pay for it once at install and again at run time, and both bills are small.

See the full comparison →

Bundle size · min + gzip

  • Pino3.1KB1 dep
  • LogTape5.3KB0 deps
  • bunyan5.7KB0 deps
  • log4js12.9KB5 deps
  • Signale16.4KB23 deps
  • winston38.3KB17 deps

Console overhead · Node.js

  • Pino339ns
  • LogTape451ns
  • winston2,130ns
  • bunyan2,320ns
  • log4js3,400ns
  • Signale4,360ns

How it feels

An API that reads
the way you think.

Small, composable functions, no class hierarchies or ceremony. The same call site works whether or not anyone is listening.

auth.tsno config required
import { getLogger } from "@logtape/logtape";

const logger = getLogger(["my-app", "auth"]);

export function signIn(userId: string) {
  logger.info("user {userId} signed in", { userId });
}

Press record

Start logging
in two minutes.

Add LogTape to a library or an application today. It stays silent until you ask for output, so there is nothing to undo if you change your mind.

$deno add jsr:@logtape/logtape

LogTape is free and open source. If it helps you, ♥ Sponsor the work →

Released under the MIT License.