Skip to content

Comparison with Other Loggers

How Loggily compares to popular Node.js logging libraries.

Feature Comparison Table

FeatureLoggilyPinoWinstonBunyandebug
Log LevelsYes (5)Yes (6)Yes (7)Yes (6)No
Structured LoggingYesYesYesYesNo
JSON OutputYesYesYesYesNo
Lightweight SpansBuilt-inNoNoNoNo
Near-zero DisabledYes (?.)NoNoNoNo
Child LoggersYesYesYesYesManual
TransportsFile + custom writersYesYesYesNo
Pretty PrintAuto (dev)PluginPluginPluginYes
Browser SupportPartialYesYesYesYes
Bundle Size~3KB~17KB~200KB+~30KB~2KB
TypeScriptNativeYesTypes pkgTypes pkgTypes pkg

vs Pino

Pino is the gold standard for high-performance Node.js logging.

Similarities

  • Performance-focused design
  • JSON output in production
  • Child loggers with inherited context
  • Minimal overhead

Differences

AspectPinoLoggily
Near-zero disabledNoop function (args evaluated)Optional chaining (args skipped)
SpansExternal (pino-opentelemetry)Built-in
TransportsBuilt-in (worker threads)File writer + custom via addWriter
FormattersPlugin systemConsole/JSON auto-switch
SerializersConfigurableFixed (Error auto-handled)

When to Choose

Choose Pino if:

  • You need transport plugins (file rotation, remote logging)
  • You need custom serializers for complex objects
  • You're building a large production system with multiple log destinations

Choose Loggily if:

  • You want near-zero cost disabled logging via optional chaining
  • You need built-in span timing
  • You prefer simplicity over configuration
  • Bundle size matters

Code Comparison

typescript
// Pino
import pino from "pino"
const log = pino({ level: "debug" })
const child = log.child({ requestId: "123" })
child.info({ user: "alice" }, "logged in")

// Loggily
import { createLogger } from "loggily"
const log = createLogger("myapp")
const child = log.logger("request", { requestId: "123" })
child.info("logged in", { user: "alice" })

vs Winston

Winston is the most popular Node.js logger with extensive transport support.

Similarities

  • Multiple log levels
  • Structured logging support
  • Child loggers

Differences

AspectWinstonLoggily
PhilosophyFlexible, configurableSimple, opinionated
Transports10+ built-inFile writer + custom via addWriter
ConfigurationExtensiveMinimal (env vars)
PerformanceModerateHigh
Bundle Size~200KB+~3KB
SpansNoBuilt-in

When to Choose

Choose Winston if:

  • You need multiple transports (file, HTTP, database)
  • You need custom formatters and filters
  • You have complex logging requirements

Choose Loggily if:

  • You want minimal configuration
  • Performance is critical
  • You're logging to stdout (12-factor app)
  • You need built-in timing spans

Code Comparison

typescript
// Winston
import winston from "winston"
const log = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
})
log.info("starting", { port: 3000 })

// Loggily
import { createLogger } from "loggily"
const log = createLogger("myapp")
log.info("starting", { port: 3000 })

vs Bunyan

Bunyan focuses on JSON logging with built-in CLI tools.

Similarities

  • JSON-first output
  • Child loggers
  • Structured data

Differences

AspectBunyanLoggily
Output FormatJSON onlyConsole (dev) / JSON (prod)
CLI Toolsbunyan CLI for viewingNone
StreamsMultiple streamsFile writer + custom via addWriter
SpansNoBuilt-in
APIVerboseSimple

When to Choose

Choose Bunyan if:

  • You want the bunyan CLI for log viewing
  • You need multiple output streams
  • JSON-only output is fine for development

Choose Loggily if:

  • You want readable console output in development
  • You need built-in spans
  • You prefer a simpler API

Code Comparison

typescript
// Bunyan
import bunyan from "bunyan"
const log = bunyan.createLogger({ name: "myapp" })
const child = log.child({ requestId: "123" })
child.info({ user: "alice" }, "logged in")

// Loggily
import { createLogger } from "loggily"
const log = createLogger("myapp")
const child = log.logger("request", { requestId: "123" })
child.info("logged in", { user: "alice" })

vs debug

debug is a tiny debugging utility.

Similarities

  • Minimal footprint
  • Namespace-based organization
  • Environment variable control

Differences

AspectdebugLoggily
Log LevelsNo (on/off)Yes (5 levels)
Output Formatprintf-styleStructured JSON
SpansNoBuilt-in
Conditional.enabled checkOptional chaining
DataInline in messageSeparate object

When to Choose

Choose debug if:

  • You only need simple debugging output
  • You don't need log levels
  • You don't need structured data

Choose Loggily if:

  • You need log levels
  • You need structured data
  • You need timing spans
  • You want near-zero cost disabled logging

Code Comparison

typescript
// debug
import createDebug from "debug"
const debug = createDebug("myapp")
debug("user %s logged in", username)

// Loggily
import { createLogger } from "loggily"
const log = createLogger("myapp")
log.info("user logged in", { username })

See migration-from-debug.md for a detailed migration guide.


Unique Features of Loggily

1. Near-Zero Cost Disabled Logging

Optional chaining skips argument evaluation entirely:

typescript
// Other loggers - args always evaluated
pino.debug(`expensive: ${computeState()}`) // computeState() runs even if disabled

// Loggily - args skipped when disabled
log.debug?.(`expensive: ${computeState()}`) // computeState() NOT called if disabled

Benchmark (10M iterations):

  • Noop with expensive args: 17M ops/s (57.6ns)
  • Optional chaining with expensive args: 408M ops/s (2.5ns) - 22x faster

2. Built-in Spans

No external tracing library needed:

typescript
{
  using span = log.span("db:query", { table: "users" })
  const users = await db.query("SELECT * FROM users")
  span.spanData.count = users.length
}
// → SPAN myapp:db:query (45ms) {count: 100, table: "users"}

Features:

  • Automatic timing on block exit
  • Parent-child relationships tracked
  • Custom attributes via spanData
  • Trace ID for request correlation

3. Disposable Pattern

Uses JavaScript's using keyword for automatic cleanup:

typescript
{
  using span = log.span("operation")
  // ... work ...
} // Span automatically ends and emits timing

// No need for try/finally or .end() calls

4. Auto-format Switching

Console output in development, JSON in production:

bash
# Development (pretty console)
bun run app
# → 14:32:15 INFO myapp starting {port: 3000}

# Production (JSON)
NODE_ENV=production bun run app
# → {"time":"2024-01-15T14:32:15.123Z","level":"info","name":"myapp","msg":"starting","port":3000}

Summary

Use CaseRecommended
Near-zero cost disabled loggingLoggily
Built-in span timingLoggily
Multiple transportsPino or Winston
Extensive configurationWinston
JSON CLI toolsBunyan
Simple debugging onlydebug
Minimal bundle sizedebug or Loggily
TypeScript-firstLoggily or Pino

Released under the MIT License.