Skip to main content

consola vs tslog vs roarr: Structured Logging Beyond pino & winston (2026)

·PkgPulse Team

TL;DR

consola (by UnJS) is the elegant logging utility — beautiful console output in development, structured JSON in production, browser-compatible, integrates with Nuxt/Nitro. tslog is the TypeScript-native structured logger — zero dependencies, source-mapped stack traces, circular reference handling, and runtime type safety. roarr is the environment-agnostic JSON logger — always outputs JSON (never pretty-prints), designed for log aggregation pipelines, works in Node.js and browser. In 2026: consola for full-stack apps and UnJS projects, tslog for TypeScript-first structured logging, roarr for strict JSON-only log pipelines.

Key Takeaways

  • consola: ~8M weekly downloads — UnJS, pretty dev output + JSON production, reporters, browser
  • tslog: ~200K weekly downloads — zero deps, TypeScript-first, source-mapped errors, log masking
  • roarr: ~500K weekly downloads — always JSON, environment-agnostic, log context inheritance
  • consola auto-detects environment — pretty in terminal, JSON in CI/production
  • tslog has built-in sensitive data masking — redact passwords, tokens from logs automatically
  • roarr NEVER pretty-prints — every log is JSON, ideal for log shipping (Loki, Datadog, ELK)

When to Use These vs pino/winston

pino / winston:
  Production HTTP logging, high throughput (100K+ logs/sec)
  Established ecosystem with transport plugins
  Most popular for Express/Fastify API logging

consola / tslog / roarr:
  Application logging (business logic, errors, debugging)
  Beautiful dev output that switches to JSON in production
  TypeScript-first projects that want type-safe logging
  Full-stack apps (Node + browser)

Many teams use BOTH:
  pino → HTTP request/response logging (middleware)
  consola/tslog → Application-level logging (services, utils)

consola

consola — elegant universal logger:

Basic usage

import { consola } from "consola"

// Log levels (auto-pretty in dev, JSON in production):
consola.info("Server started on port 3000")
consola.success("Package data cached successfully")
consola.warn("Rate limit approaching threshold")
consola.error("Failed to fetch npm data", new Error("ECONNREFUSED"))
consola.debug("Cache hit for package: react")

// In development terminal (pretty):
// ℹ Server started on port 3000
// ✔ Package data cached successfully
// ⚠ Rate limit approaching threshold
// ✖ Failed to fetch npm data
//   Error: ECONNREFUSED
//     at ...

// In production (JSON):
// {"level":3,"type":"info","message":"Server started on port 3000","timestamp":"2026-03-09T..."}

Scoped loggers (with tags)

import { consola } from "consola"

// Create scoped logger:
const dbLog = consola.withTag("database")
const cacheLog = consola.withTag("cache")
const apiLog = consola.withTag("api")

dbLog.info("Connected to PostgreSQL")
// [database] ℹ Connected to PostgreSQL

cacheLog.warn("Redis connection slow: 500ms")
// [cache] ⚠ Redis connection slow: 500ms

apiLog.error("npm API returned 503")
// [api] ✖ npm API returned 503

Log level control

import { consola, LogLevels } from "consola"

// Set log level:
consola.level = LogLevels.warn  // Only warn + error

// Or from environment:
// CONSOLA_LEVEL=debug → shows everything
// CONSOLA_LEVEL=warn → warn + error only
// CONSOLA_LEVEL=silent → nothing

// Per-scope level:
const debugLog = consola.create({ level: LogLevels.debug })

Custom reporters

import { consola, createConsola } from "consola"

const logger = createConsola({
  reporters: [
    // Pretty reporter for terminal:
    { log: (logObj) => console.log(JSON.stringify(logObj)) },
  ],
  // Or use built-in reporters:
  fancy: process.env.NODE_ENV !== "production",
})

Box and prompt utilities

import { consola } from "consola"

// Box (highlighted message):
consola.box("PkgPulse API v2.0.0\nRunning on port 3000")
// ╭──────────────────────────╮
// │ PkgPulse API v2.0.0     │
// │ Running on port 3000     │
// ╰──────────────────────────╯

// Interactive prompt:
const name = await consola.prompt("Package name:", { type: "text" })
const confirm = await consola.prompt("Deploy?", { type: "confirm" })

tslog

tslog — TypeScript-native structured logger:

Setup

import { Logger } from "tslog"

const log = new Logger({
  name: "pkgpulse-api",
  type: "pretty",            // "pretty" | "json" | "hidden"
  minLevel: 0,               // 0=silly, 1=trace, 2=debug, 3=info, 4=warn, 5=error, 6=fatal
  prettyLogTemplate: "{{yyyy}}.{{mm}}.{{dd}} {{hh}}:{{MM}}:{{ss}} {{logLevelName}} ",
})

log.info("Server started")
log.warn("Rate limit approaching")
log.error("Database connection failed", new Error("ECONNREFUSED"))

// Pretty output:
// 2026.03.09 14:30:00 INFO  Server started
// 2026.03.09 14:30:01 WARN  Rate limit approaching
// 2026.03.09 14:30:02 ERROR Database connection failed
//   Error: ECONNREFUSED
//     at Object.<anonymous> (/src/db.ts:42:11)  ← source-mapped!

Child loggers

const log = new Logger({ name: "app" })

const dbLog = log.getSubLogger({ name: "database" })
const cacheLog = log.getSubLogger({ name: "cache" })
const apiLog = log.getSubLogger({ name: "api" })

dbLog.info("Query executed in 12ms")
// 2026.03.09 14:30:00 INFO [app → database] Query executed in 12ms

cacheLog.warn("Cache miss rate: 45%")
// 2026.03.09 14:30:01 WARN [app → cache] Cache miss rate: 45%

Sensitive data masking

const log = new Logger({
  name: "api",
  maskValuesOfKeys: ["password", "token", "secret", "authorization"],
  maskPlaceholder: "[REDACTED]",
})

log.info("User login", {
  email: "user@example.com",
  password: "super-secret-123",       // Will be masked
  token: "eyJhbGciOiJIUzI1NiIs...",   // Will be masked
})

// Output:
// INFO User login
//   email: "user@example.com"
//   password: "[REDACTED]"
//   token: "[REDACTED]"

JSON mode (production)

const log = new Logger({
  name: "pkgpulse",
  type: process.env.NODE_ENV === "production" ? "json" : "pretty",
})

log.info("Package fetched", { name: "react", score: 92 })

// Pretty (dev):
// INFO  Package fetched { name: "react", score: 92 }

// JSON (production):
// {"0":"Package fetched","1":{"name":"react","score":92},"_meta":{"name":"pkgpulse","date":"2026-03-09T...","logLevelId":3,"logLevelName":"INFO"}}

Transport to external services

import { Logger, ILogObj } from "tslog"

const log = new Logger()

// Attach transport:
log.attachTransport((logObj: ILogObj) => {
  // Send to Loki, Datadog, ELK, etc.:
  fetch("https://logs.example.com/ingest", {
    method: "POST",
    body: JSON.stringify(logObj),
  })
})

roarr

roarr — always-JSON logger:

Basic usage

import { Roarr as log } from "roarr"

log.info("Server started")
log.warn("Rate limit warning")
log.error("Database error")

// ALWAYS outputs JSON (even in dev):
// {"context":{},"message":"Server started","sequence":"0","time":1709985600,"version":"2.0.0"}

Context (structured data)

import { Roarr } from "roarr"

const log = Roarr.child({ service: "pkgpulse-api", version: "2.0.0" })

log.info({ packageName: "react", score: 92 }, "Package score calculated")
// {"context":{"service":"pkgpulse-api","version":"2.0.0","packageName":"react","score":92},
//  "message":"Package score calculated","sequence":"1","time":...}

// Child with additional context:
const dbLog = log.child({ module: "database" })
dbLog.error({ query: "SELECT * FROM packages", duration: 5000 }, "Slow query")
// context includes: service, version, module, query, duration

Adopt pattern (per-request context)

import { Roarr } from "roarr"

// Middleware — add request context:
app.use((req, res, next) => {
  const requestLog = Roarr.child({
    requestId: crypto.randomUUID(),
    method: req.method,
    path: req.path,
  })

  req.log = requestLog
  next()
})

// In route handler:
app.get("/api/packages/:name", async (req, res) => {
  req.log.info({ packageName: req.params.name }, "Fetching package")
  // All logs from this request include requestId, method, path
})

Environment control

# roarr is SILENT by default — enable with env var:
ROARR_LOG=true node server.js

# Filter by context:
ROARR_LOG=true ROARR_FILTER='{"context.service":"pkgpulse-api"}' node server.js

# Pretty print in dev (via roarr-cli):
ROARR_LOG=true node server.js | npx roarr pretty-print

Feature Comparison

Featureconsolatslogroarr
Pretty dev output✅ (auto)❌ (always JSON)
JSON production✅ (auto)✅ (always)
Child/scoped loggers✅ (withTag)✅ (getSubLogger)✅ (child)
Sensitive data masking
Source maps
Browser support
Interactive prompts
Zero dependencies
Log level env var✅ (CONSOLA_LEVEL)❌ (config)✅ (ROARR_LOG)
Weekly downloads~8M~200K~500K

When to Use Each

Choose consola if:

  • Building with Nuxt, Nitro, or UnJS ecosystem
  • Want beautiful terminal output in dev, JSON in production automatically
  • Need interactive prompts alongside logging
  • Full-stack app that logs in both Node.js and browser

Choose tslog if:

  • TypeScript-first project that wants structured logging
  • Need automatic sensitive data masking (passwords, tokens)
  • Want source-mapped stack traces in error logs
  • Zero dependencies is a priority

Choose roarr if:

  • All logs MUST be JSON — no exceptions, no pretty-printing
  • Building for log aggregation pipelines (ELK, Loki, Datadog)
  • Want context inheritance across deeply nested function calls
  • Strict structured logging discipline

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on consola v3.x, tslog v4.x, and roarr v7.x.

Compare logging and observability packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.