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
Log Shipping and Aggregation Pipeline Integration
The output format each logger produces determines how much post-processing your log aggregation pipeline requires. For production systems shipping logs to Loki, Datadog, or the ELK stack, JSON structure is non-negotiable — log agents parse JSON fields directly into queryable labels and attributes.
roarr is the strictest about this contract. Every log entry is a flat JSON object with a context field containing structured metadata and a message field. The sequence counter is particularly useful for log agents that process batches out of order — you can reconstruct the original emission sequence even when log delivery is not strictly ordered. Loki's logfmt parser and structured metadata labels work directly with roarr's output format without any Logstash transform.
consola in production mode (when NODE_ENV === "production") emits JSON that matches a consistent schema: level, type, message, timestamp, and any additional fields passed as the second argument. This is sufficient for Datadog log parsing, but the schema differs from the Pino JSON schema that many Datadog integrations are pre-configured for. If your infrastructure team has already tuned Datadog's log processing pipeline for Pino, consider whether consola's schema difference justifies a custom parser.
tslog's JSON output includes a _meta field with logger name, log level ID, log level name, and timestamp in a nested object. Log agents that expect flat top-level fields need a transform step to promote _meta.logLevelName to a top-level level field. tslog v4 added the prettyLogTemplate and jsonTemplate options, which give you direct control over the output schema — you can flatten the structure to match whatever schema your downstream pipeline expects. This flexibility comes at the cost of needing explicit configuration; consola and roarr work with sensible defaults.
Sensitive Data Handling and Log Security
tslog's maskValuesOfKeys feature addresses a compliance concern that is frequently underestimated: accidental credential logging. In large applications with many developers, someone eventually logs a full request object that contains an Authorization header, a password field, or an API token. These secrets appear in log aggregators, are exported to monitoring dashboards, and may persist for months in log archives.
tslog's masking operates recursively on the logged data structure. Passing { password: "secret123", nested: { token: "abc" } } to any log method with maskValuesOfKeys: ["password", "token"] produces [REDACTED] for both fields, regardless of nesting depth. This is a defense-in-depth measure — it does not replace proper secrets management, but it significantly reduces the blast radius when a developer accidentally logs a sensitive object.
consola does not provide built-in masking, but you can add it via a custom reporter. A reporter that uses a redaction library (like fast-redact, which pino uses internally) before forwarding to the console provides comparable protection. The downside is that this requires deliberate setup rather than being on by default.
roarr's context-first logging model reduces accidental sensitive data logging through a different mechanism: developers construct a context object explicitly before logging, which makes the structure visible at the call site. This visibility nudge reduces the "log the whole object to see what's in it" debugging pattern that produces most accidental credential leaks. However, for code paths that genuinely need to log request bodies or response payloads for debugging, neither roarr's structure nor tslog's key masking is a substitute for a formal data classification policy.
Performance and Overhead in High-Throughput Services
Logging overhead matters in services that log at high frequency. For request-level logging in an HTTP API handling thousands of requests per second, pino remains the performance benchmark — its asynchronous transport and minimal serialization overhead add roughly 0.1ms per log call. consola, tslog, and roarr are not designed for this use case and add 1–5ms per log call depending on the output format and configuration.
For application-level logging (service initialization, business logic events, error reporting), the performance difference is irrelevant — these logs are emitted at frequencies where nanosecond differences don't accumulate meaningfully. roarr's always-JSON approach has marginally lower overhead than consola's dev/production mode detection and tslog's pretty-print formatting, but all three are comfortably fast enough for any logging use case that isn't high-frequency HTTP request logging.
tslog's source map resolution adds startup overhead. When type: "pretty" is configured and an error is logged, tslog resolves the TypeScript source location from the compiled output's source map. This resolution is synchronous and cached per file, so the first logged error per file takes longer than subsequent ones. In test environments where hundreds of errors might be logged during a failing test run, this can add noticeable latency to test execution. Switching to type: "json" in test environments avoids this overhead.
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 →
In 2026, consola is the default choice for CLI tools and Nuxt ecosystem projects where human-readable output matters. tslog is the choice for TypeScript-first backend services where log typing and field validation are priorities. roarr is for applications where every log must be machine-parseable JSON from day one, trading human readability for guaranteed structure in high-volume production logging pipelines.
See also: pm2 vs node:cluster vs tsx watch and h3 vs polka vs koa 2026, better-sqlite3 vs libsql vs sql.js.