The logger module replaces scattered console.log calls with one logger. It is off by default and has no dependencies.
findplate create my-app --modules loggerWhy not pino
packages/logger is small and has no dependencies. pino is great, but its worker thread transports and its bundling quirks cost more in a Bun api and in Next server bundles than they give back for a starter. If you outgrow it, keep the Logger interface and swap the body for pino.
What you get
- one json line per entry in production, one readable colored line in dev
LOG_LEVEL(debug,info,warn,error, defaultinfo)- redaction: any key that looks like
password,secret,token,authorization,cookie,api key,sessionordsnis replaced with[redacted], at any depth, headers included - a request id on every line, read from the inbound
x-request-id(when it is short and plain) or generated, and returned as thex-request-idresponse header - one
requestline per api call with method, path, status and duration (/healthonly atdebug)
import { logger } from "@acme/logger"
logger.info("order paid", { orderId, token }) // token is redacted
const log = logger.child({ job: "digest" })The request id lives in an AsyncLocalStorage, so it reaches every line logged while the request runs without passing it around, and getRequestId() returns it if you need it. The api also routes console.* through the logger, so existing calls (and libraries) get the same format and the request id.
Web and dashboard
apps/web/instrumentation.ts and apps/dashboard/instrumentation.ts use the Next instrumentation hook. On the Node runtime they route server console.* output through the logger and log every failed render or route handler (onRequestError) with its route.
With glitchtip and openobserve
Both are optional and null safe. With openobserve on, lines logged inside a traced request carry trace_id and span_id. With glitchtip on they carry the Sentry ones.
Env keys
LOG_LEVEL=info