# Migrating from tslog v4 to v5 > [!IMPORTANT] > **4.11.0 is the safe staying point. Stay on it until you are ready to follow this guide — do not pressure-upgrade.** > > Most of the v5 performance wins and the GitHub-issue fixes were back-ported into **tslog 4.11.0**: the > faster lazy stack capture, the transport isolation hardening, the masking `$`-escape and numeric-key > fixes, and the new 4.11.0 settings all ship there with **zero breaking changes**. If you are on the 4.x > line today, `npm install tslog@4.11.0` gets you those wins for free and keeps your existing settings, > CJS `require`, Node 16+, and JSON shape exactly as they are. > > v5 is a deliberate, breaking redesign (ESM-only, grouped settings, a new default JSON shape, middleware > instead of `overwrite.*`). Upgrade to v5 when you actually want the new capabilities below — not because > a number changed. There is no deprecation pressure: 4.11.0 is a fine place to live for a long time. --- ## What's new in v5 / why upgrade You should upgrade to v5 when you want one or more of these. None of them exist on the 4.x line. - **Environment-aware colorization.** `new Logger()` and the ready-made `log` stay **pretty everywhere** (as in v4), but the coloring now adapts to the environment: colored on an interactive TTY and uncolored when piped/redirected/CI, so no ANSI escapes leak into files or log collectors. Structured JSON is opt-in via `type: "json"`, `TSLOG_TYPE=json`, or a JSON transport. - **Flat, fields-first JSON.** The default `type: "json"` output is now a flat, observability-friendly object — `message` / `level` / `levelId` / `time` at the top level, your fields spread next to them, runtime metadata nested under `_logMeta` with a `v: 5` schema version. No more positional args under numeric keys or the level buried inside `_logMeta`. Every key name is configurable via the `json` group (`messageKey`, `levelKey`, `timeKey`, `errorKey`, …). - **Drop-in presets** (tree-shakeable subpaths, off by default): - `tslog/presets/pino` — `pinoFormat()` / `pinoTransport()` emit pino-compatible NDJSON (numeric `level`, `time` ms epoch, `msg`), so existing pino tooling (`pino-pretty`, transports) keeps working. - `tslog/otel` — `otelFormat()` / `otelTraceContext()` emit OpenTelemetry log records with the right `SeverityNumber` and trace/span correlation; `otlpFormat()` / `otlpBatchBody()` emit real OTLP/JSON for collectors (pair with `httpTransport({ encodeBody: otlpBatchBody })`). - `tslog/presets/genai` — `genai()` / `genaiAttributes()` / `genaiSummary()` build GenAI/agentic semantic-convention attributes (model, tokens, tool calls) for LLM apps. - **JSONPath-lite masking.** The `mask` group adds `paths` (dotted paths with `*` wildcards, e.g. `"user.password"`, `"*.token"`) alongside key/regex masking, plus a per-match `censor` that can be a replacement string, `"remove"`, a function, or **`"hash"`** — a fast, synchronous, non-cryptographic correlation token (`"[hash:1a2b3c4d]"`) so you can correlate a redacted secret across logs without exposing it. - **Async-context propagation (ALS).** `logger.runInContext(ctx, fn)` / `logger.getContext()` thread request/trace fields onto every log's `_logMeta` across `await`, timers, and nested calls (Node/Bun/Deno; graceful no-op on browsers/edge). - **Async transports + lifecycle.** `attachTransport` accepts a full `Transport` (per-transport `minLevel`, `format`, async `write`, `flush`, `[Symbol.asyncDispose]`) **and returns a detach function**. `logger.flush()` drains buffered transports; `await using log = new Logger()` disposes them. First-class file (`tslog/transports/file`), HTTP/NDJSON (`tslog/transports/http`), ring-buffer (`tslog/transports/ringbuffer`), and worker-thread (`tslog/transports/worker`) transports ship as subpaths. - **Middleware via `logger.use(...)`.** A single composable chain replaces the eight `overwrite.*` hooks: enrich, rewrite, sample, or drop a log. Plus exported, tree-shakeable middleware (`serialize(...)` for pino-style serializers, `otelTraceContext(...)`). - **Faster lazy stack capture.** Stack frames are parsed only when actually needed, driven by the `stack.capture` mode (`"off" | "lazy" | "auto" | "full"`); JSON defaults to `"off"`, pretty to `"auto"`. - **Tree-shakeable subpath architecture & zero import-time side effects** (`sideEffects: false`): the core stays tiny and presets/transports/serializers/testing/box are pulled in only when imported. - **Better AI / agentic DX.** Fields-first call signatures (`log.info({ userId: 42 }, "msg")`), additive custom levels (`customLevels` / `addLevel()`), `Logger.fromEnv()`, strict-config validation (`strictConfig` → `TslogConfigError`), a `tslog/testing` helper, and a `tslog/pretty/box` renderer. - **Browser-native pretty objects.** `pretty.passObjectsNatively` (on by default in real browsers) hands non-`Error` args to the console by reference for collapsible DevTools trees; pair with `pretty.levelMethod` for native warn/error stack groups. - **Conditional logging.** `log.if(condition)` returns the logger when truthy and a no-op when falsy — `log.if(!ok).warn("failed", { id })` — without a surrounding `if` statement. - **Source-mapped error positions.** On Node, Bun, and Deno, logged `Error` stacks resolve through source maps back to original `.ts` positions (automatic outside production; `TSLOG_SOURCE_MAPS=on`/`off` to force). - **`tslog/slim`.** The same structured-JSON pipeline at less than half the bundle size — masking, pretty output, and stack capture are omitted (`mask` and `type: "pretty"` throw instead of silently degrading). --- ## Prerequisites & install | | v4 | v5 | |---|---|---| | Module system | ESM **and** CJS (`require` worked) | **ESM-only** — no CJS, no `require` | | Node.js | 16+ | **20+** | | TS target | es2020 | **es2022** | | Runtime deps | none | none | ```bash npm install tslog@5 ``` v5 publishes **ESM only** with conditional exports. There is no `dist/cjs` and no `require("tslog")`. ```js // v4 (CommonJS) — NO LONGER SUPPORTED in v5 const { Logger } = require("tslog"); // v5 — ESM import import { Logger } from "tslog"; ``` If you cannot move your app to ESM yet, **stay on 4.11.0**. The interop options for an ESM-only dependency from a CJS file are a dynamic `import()` (`const { Logger } = await import("tslog")`) or a bundler — neither is required if you remain on 4.x. Set your `tsconfig.json` to es2022 and a NodeNext module resolution, and write relative imports with `.js` extensions if you author ESM TypeScript. --- ## 1. ESM-only **Breaking:** v5 ships no CommonJS build. `require("tslog")` throws. ```js // BEFORE (v4, CJS) const { Logger, log } = require("tslog"); // AFTER (v5, ESM) import { Logger, log } from "tslog"; ``` The package root (`tslog`) resolves, via conditional exports, to the universal build, which auto-detects Node, browsers, Deno, Bun, React Native, and workers at construction time. The named exports are unchanged: `Logger`, `BaseLogger`, the ready-made `log` instance, and all interface/enum types. --- ## 2. Flat settings → grouped settings (full mapping table) **Breaking:** every flat `prettyLog*` / `maskValues*` / `stack*` / `meta*` key is gone. Settings are now organized into the `pretty`, `json`, `mask`, `stack`, and `meta` groups, with a handful of top-level keys (`type`, `name`, `minLevel`, `customLevels`, `middleware`, `prefix`, `attachedTransports`, `strictConfig`, …). **There is no flat-key fallback** — an old flat key does not configure anything. tslog detects carried-over v4 keys (and typo'd/unknown keys) at construction: in development it warns with the exact new location (e.g. `"maskValuesOfKeys" was removed in v5 — use mask.keys`), and with `strictConfig: true` it throws a typed `TslogConfigError` (`V4_FLAT_KEY` / `UNKNOWN_SETTING`) so a stale config can never ship silently. ```ts // BEFORE (v4) — flat keys const log = new Logger({ type: "pretty", minLevel: "warn", prettyLogTimeZone: "local", stylePrettyLogs: true, prettyLogTemplate: "{{logLevelName}}\t{{filePathWithLine}}\t", maskValuesOfKeys: ["password", "apiKey"], maskValuesOfKeysCaseInsensitive: true, maskPlaceholder: "[REDACTED]", metaProperty: "$meta", }); // AFTER (v5) — grouped const log = new Logger({ type: "pretty", minLevel: "WARN", pretty: { timeZone: "local", style: true, template: "{{logLevelName}}\t{{filePathWithLine}}\t", }, mask: { keys: ["password", "apiKey"], caseInsensitive: true, placeholder: "[REDACTED]", }, meta: { property: "$meta" }, }); ``` ### Mapping table — every v4 flat key → v5 grouped path | v4 flat key | v5 grouped path | |---|---| | `type` | `type` *(unchanged top-level; default now env-aware — see §6)* | | `name` | `name` *(unchanged)* | | `parentNames` | `parentNames` *(unchanged)* | | `minLevel` | `minLevel` *(unchanged; still accepts a name or numeric id)* | | `argumentsArrayName` | `argumentsArrayName` *(unchanged)* | | `prefix` | `prefix` *(unchanged)* | | `prettyLogTemplate` | `pretty.template` | | `prettyErrorTemplate` | `pretty.errorTemplate` | | `prettyErrorStackTemplate` | `pretty.errorStackTemplate` | | `prettyErrorParentNamesSeparator` | `pretty.errorParentNamesSeparator` | | `prettyErrorLoggerNameDelimiter` | `pretty.errorLoggerNameDelimiter` | | `stylePrettyLogs` | `pretty.style` | | `prettyLogTimeZone` | `pretty.timeZone` | | `prettyLogStyles` | `pretty.styles` | | `prettyInspectOptions` | `pretty.inspectOptions` | | *(new)* | `pretty.enabled` — explicit pretty on/off, overriding the env-aware default | | `prettyLogLevelMethod` *(4.11)* | `pretty.levelMethod` — map a level name (or `"*"`) to a `console` method | | *(new)* | `pretty.passObjectsNatively` — hand non-`Error` args to the console by reference (default `true` in browsers) | | `maskValuesOfKeys` | `mask.keys` — **default is now `[]` (masking OFF); see §5** | | `maskValuesOfKeysCaseInsensitive` | `mask.caseInsensitive` | | `maskValuesRegEx` | `mask.regex` | | `maskPlaceholder` | `mask.placeholder` | | *(new)* | `mask.paths` — JSONPath-lite dotted paths with `*` wildcards | | *(new)* | `mask.censor` — `"remove"` / `"hash"` / replacement string / function | | *(new)* | `mask.hashLabel` — label inside the `"hash"` token (`"[