tslog v5: fields-first logging, and an upgrade that is not optional for many
📝 tslog - Universal Logger for TypeScript and JavaScript
At a glance
- What is it?
- tslog v5 is an ESM-only redesign with grouped settings, middleware instead of overwrite keys, and a flat JSON shape aimed at log pipelines. The maintainers also state where you can stay instead, which is the more useful half of the release notes.
- Who is it for?
- Adopt tslog v5 if you run Node.js 22 or later with ESM and want structured logs that carry fields rather than interpolated strings, since the fields-first shape is what makes the output usable in a pipeline. Do not upgrade if you are on CommonJS or Node 16 or 18, because there is no CommonJS build in v5 and the maintainers name 4.11.0 as the place to stay.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The upgrade note tells you when not to upgrade
The most useful paragraph in this README is the migration warning, because it is unusually direct about who should not move.
v5 is described as a deliberate, breaking redesign: ESM-only, grouped settings, a new fields-first JSON shape, and middleware instead of `overwrite.*`. Then the guidance. If you are on the 4.x line, 4.11.0 is a safe place to stay, and most of the v5 performance wins were back-ported there with zero breaking changes. Upgrade when you want the new capabilities.
That is a maintainer telling you that the performance argument is not sufficient reason on its own, which is rare and worth respecting. The v4 line is not deprecated and abandoned, it is the recommended destination for anyone who cannot absorb the breaking changes.
The runtime constraint is the hard one. v5 is ESM-only and requires Node.js 22 or later, with 5.0 and 5.1 also running on Node 20. There is no CommonJS build and no `require("tslog")`. If you cannot move to ESM or off Node 16 and 18 yet, stay on 4.11.0, which keeps CJS, Node 16+, and the v4 JSON shape.
Three specific named versions in one note: `[email protected]` if you are on Node 20, `[email protected]` if you are on CommonJS or older Node, and `5.2.0` which is the current version in the package manifest.
Fields-first is the change that alters your output shape
The v5 call convention is the substantive redesign. Both orders work:
import { Logger } from "tslog";
const log = new Logger({ minLevel: "INFO" });
log.info({ port: 3000 }, "server started");
log.info("server started");The first form passes fields first and the message second. The resulting JSON is given in the README, and the shape is the point:
{"message":"tool call done","level":"INFO","levelId":3,
"time":"…","tool":"search","tokens":318,
"_logMeta":{"v":5,"name":"agent:planner",…}}Fields sit at the top level next to `message`, `level`, `levelId`, and `time`, rather than nested under a context object. `_logMeta` carries the schema version and the logger name. That flat shape is what lets a log pipeline index on `tokens` or `tool` without an extraction step, which is the stated goal of observability-ready output.
One behaviour is worth noting because it will surprise you: a lone object spreads its fields and there is no message key. The masking example shows `safeLog.info({ user: {...} })` producing an object with user and level keys at the top level rather than wrapping the object.
`new Logger()` with no `type` is pretty: colorized in an interactive terminal, uncolored when piped, redirected, or in CI. Setting `type` to `"pretty"`, `"json"`, or `"hidden"` makes it explicit.
Secret masking is a grouped setting, not a helper function
The masking example configures keys and paths together:
const safeLog = new Logger({
type: "json",
mask: {
keys: ["password", "apiKey", "token", "prompt"],
paths: ["user.password", "*.token"],
},
});
safeLog.info({ user: { name: "Ada", password: "hunter2" } });which produces `{"user":{"name":"Ada","password":"[***]"},"level":"INFO", …}`.
Two mechanisms there. `keys` matches by field name anywhere in the object, so `token` masks a token at any depth. `paths` matches specific locations, here a literal `user.password` and a wildcard `*.token`.
The highlights list names two more mechanisms not shown in the example: regex matching, and a hashing censor for correlation. The hashing option is the interesting one, since a censor that replaced every secret with the same mask destroys your ability to tell whether two log lines contain the same value. A hash preserves that while keeping the value out of the log.
Masking is configured under a grouped `mask` key rather than a flat `overwrite.*` option, which is part of the v5 settings restructure the upgrade note warns about.
Sub-loggers, middleware, and per-transport configuration
Child loggers are the way to correlate per request or per agent, and the API has two names for the same thing:
const requestLog = log.getSubLogger({ name: "agent:planner" });
requestLog.info({ tool: "search", tokens: 318 }, "tool call done");`child()` aliases `getSubLogger()`. Settings and fields are inherited, and names accumulate, so the child's `_logMeta.name` shows where the line came from.
Transports and middleware replace the v4 `overwrite.*` model. The highlights list per-transport level and format, a `use()` pipeline for middleware, and `flush()` plus disposal. That is a meaningful structural change: instead of one logger object with overwrite flags, you attach transports and compose a pipeline.
The package manifest shows how transports ship. The `exports` map has separate subpaths for `./transports/file` with a `node` condition, `./transports/http`, `./transports/ringbuffer`, and `./transports/worker`. Each has its own types entry. Combined with `sideEffects: false` and tree-shakeable subpaths, the intent is that a browser bundle does not carry the file transport.
The highlight list also notes that transports have per-transport level and format, so you can send a file the full pretty output while shipping a different shape to an HTTP collector.
One package, six runtimes, four conditional exports
The universal claim is backed by the conditional exports map rather than by prose alone. The root `.` export resolves differently per runtime:
".": {
"types": "./dist/types/index.d.ts",
"browser": "./dist/esm/index.browser.js",
"worker": "./dist/esm/index.browser.js",
"deno": "./dist/esm/index.universal.js",
"bun": "./dist/esm/index.universal.js",
"node": "./dist/esm/index.node.js",
"react-native": "./dist/esm/index.universal.js",
"default": "./dist/esm/index.universal.js"
}Note that `worker` maps to the same file as `browser`, which makes sense since a worker is a browser context. Note also that Node gets its own distinct build, which is where Node-specific transports such as the file transport live. React Native gets the universal build.
Installation differs per runtime for the same reason:
npm install tslogbun add tslogand for Deno and the browser there is no install step at all, since an `npm:` specifier or a CDN URL resolves it: `import { Logger } from "npm:tslog"` on Deno, and `import { Logger } from "https://esm.sh/tslog"` in a browser.
The Node setup asks for two things beyond installing the package. Set `"type": "module"` in `package.json`, and run Node with `--enable-source-maps` for accurate line numbers, as in the documented script `"start": "node --enable-source-maps dist/index.js"`. The source-mapped line numbers are a highlight item in their own right, and they only work if you pass that flag.
Errors, masking, and agent-oriented extras
Pretty errors and stack traces are described as structured and fully serializable, and captured only when needed. The setting is `"auto"` for pretty output and `"off"` for JSON. That conditional matters: capturing a stack trace is expensive, and paying for it on every JSON log line to a production collector is a waste. Turning it off for JSON is the sensible default and and those are the defaults for the two output modes.
There is a browser-specific behaviour worth knowing: `pretty.passObjectsNatively`, on by default in browsers, keeps logged objects collapsible in DevTools. That is a small thing that makes browser debugging considerably nicer, since a serialised string in the console is not an expandable object.
The highlights list also names first-class support for agents and LLMs: fields-first calls, agent and session correlation, an `llms.txt`, and OpenTelemetry GenAI presets. The repository does contain an `llms.txt` at the root, so that part is real rather than aspirational, and OpenClaw is cited as using the library for agent logging.
On the engineering side there is a `CHANGELOG.md`, a `MIGRATION_v4_to_v5.md` at the root for the upgrade, `RECIPES.md`, a `RELEASE_NOTES/` directory, `AGENTS.md` and `CLAUDE.md` for automated agents working in the repo, Biome for code style, Vitest for tests, and Playwright with a `playwright.config.ts` and an `e2e/` directory for end-to-end coverage.
Editorial conclusion
Adopt tslog v5 if you run Node.js 22 or later with ESM and want structured logs that carry fields rather than interpolated strings, since the fields-first shape is what makes the output usable in a pipeline. Do not upgrade if you are on CommonJS or Node 16 or 18, because there is no CommonJS build in v5 and the maintainers name 4.11.0 as the place to stay. Verify first whether your existing call sites rely on the v4 JSON shape, since that changed along with everything else.
Frequently asked questions
What are the Node.js and module system requirements for tslog v5?
v5 is ESM-only and requires Node.js 22 or later, with 5.0 and 5.1 also running on Node 20. There is no CommonJS build and no `require("tslog")`, so if you need CommonJS or Node 16 and 18, stay on [email protected], which keeps CJS and the v4 JSON shape.
How do I redact secrets from tslog output?
Use the grouped `mask` setting with `keys` to match field names anywhere in the object, and `paths` to match specific locations such as `user.password` or `*.token`. The README also documents regex matching and a hashing censor that keeps correlation possible without logging the value.
How do I install tslog on Deno, Bun, and in a browser?
Bun uses `bun add tslog`. Deno and the browser need no install step: import from `npm:tslog` on Deno, which caches from npm, or from `https://esm.sh/tslog` in a browser. The package manifest resolves the root export differently per runtime, including a distinct build for Node and a shared browser and worker build.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fullstack-build-tslog)Was this useful?