consola vs tslog vs roarr: Structured Logging Beyond pino & winston (2026)
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
| Feature | consola | tslog | roarr |
|---|---|---|---|
| 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.