diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a055a77a..de51ed2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,21 +2,21 @@ name: CI on: push: - branches: [master, v4, development] + branches: [master, v4, v5, development] pull_request: - branches: [master, development] + branches: [master, v4, v5, development] jobs: test-node: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - node-version: [20.x] + node-version: ['20', '22', '24', '26', 'latest'] env: CI: true - COVERAGE: ${{ matrix.node-version == '20.x' && true || false }} steps: - uses: actions/checkout@v5 @@ -26,10 +26,27 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm ci - # Node test suite + coverage. - - name: Run coverage + # Fail fast if the AI-facing docs (llms.txt/RECIPES.md) drifted from the source. + - name: Check doc sync + run: npm run check-doc-sync + + # Guard the gzip budgets of the slim and full browser bundles. + - name: Check bundle size + run: npm run check-bundle-size + + # Run the full test suite on every supported Node version. + # Coverage + Codecov report only on the minimum supported version (20) + # to avoid redundant uploads and overhead. + - name: Run tests (with coverage) + if: matrix.node-version == '20' run: npm run coverage + + - name: Run tests + if: matrix.node-version != '20' + run: npm test + - name: Report coverage + if: matrix.node-version == '20' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -46,7 +63,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 20.x + node-version: 22 - run: npm ci - name: Install Playwright ${{ matrix.browser }} run: npx playwright install --with-deps ${{ matrix.browser }} @@ -59,7 +76,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 20.x + node-version: 22 - uses: oven-sh/setup-bun@v2 with: bun-version: latest @@ -73,7 +90,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 20.x + node-version: 22 - uses: denoland/setup-deno@v2 with: deno-version: v2.x diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ae67eb54..dda53f8e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Install docs dependencies run: npm ci working-directory: docs diff --git a/.gitignore b/.gitignore index 47ffdd1b..99c677c2 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,9 @@ bower_components # Playwright test results /test-results/ +# Deno lockfile (written by the deno test adapter run; jsr deps are pinned per-run in CI) +/deno.lock + # Agent config # AGENTS.md and CLAUDE.md are shared with contributors (committed). # Personal, machine-local overrides go in CLAUDE.local.md (never committed). diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e7d4eb9a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "js/ts.tsdk.path": "node_modules/@typescript/native-preview" +} diff --git a/AGENTS.md b/AGENTS.md index 116bf72a..f6bc32bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,20 @@ -# AGENTS.md — tslog - ## Project Overview -tslog is a TypeScript logger for Node.js, browsers, Deno, and Bun. It supports JSON and pretty-printed output, stack traces, error formatting, custom transports, and sub-loggers. Current version: 4.11.0. +tslog is a TypeScript logger for browsers, Node.js, Deno, and Bun, React Native, and workers. It supports JSON and pretty-printed output, stack traces, error formatting, custom transports, and sub-loggers. v5 adds an always-`pretty` default `type` (colorization and `pretty.passObjectsNatively` — on by default in real browsers — are the env-aware parts, never the format), flat fields-first JSON, async-context correlation, path/regex masking, a middleware chain, and pino/otel/genai presets. Current version: 5.x (the `5` line; `_logMeta.v` in JSON output is `5`). + +### Settings are grouped (v5) + +There are no flat settings keys. Configuration is organized into groups passed to `new Logger({ ... })`: + +- Top-level: `type` (`"json" | "pretty" | "hidden"`, defaults to `"pretty"`), `name`, `minLevel` (name or 0..6), `prefix`, `customLevels`, `strictConfig`, `clock` (injectable `() => Date`). +- `mask: { keys, paths, regex, caseInsensitive, placeholder, censor }` — redact secrets/PII/prompts by key, dotted path (`*` = one segment), or regex. +- `json: { messageKey, levelKey, levelIdKey, timeKey, time, errorKey, numericLevel, stableKeyOrder }` — structured-output key names/shape (`time`: `"iso" | "epoch" | false | fn`). +- `pretty: { enabled, template, errorTemplate, style, timeZone, styles, levelMethod, inspectOptions, ... }`. +- `stack: { capture: "off" | "lazy" | "auto" | "full", internalFramePatterns }`. +- `meta: { property, attachContext }`. + +New runtime surface: `getSubLogger`/`child` (aliases), `runInContext`/`getContext` (AsyncLocalStorage correlation), `attachTransport`, `use` (middleware), `flush`, `addLevel`, `isLevelEnabled`, `[Symbol.asyncDispose]`/`[Symbol.dispose]`, `Logger.fromEnv`, and the `defineConfig` helper. Presets ship as subpaths: `tslog/presets/pino`, `tslog/otel`, `tslog/presets/genai`; transports as `tslog/transports/{file,http,ringbuffer}`. + ## Quick Reference @@ -12,30 +24,30 @@ npm test # Run Vitest test suite npm run test:browser # Run Playwright browser tests (Chromium, Firefox, WebKit) npm run test:bun # Run Vitest under Bun npm run test:deno # Build + run Deno test adapter -npm run build # Full build: types → ESM/CJS → browser bundle → prepare dist +npm run build # Full build: types → ESM → browser IIFE bundle → prepare dist npm run lint # Biome lint check npm run format # Biome format all files npm run check # Biome lint + format (write) npm run coverage # Run tests with coverage report -npm run dev-ts # Watch mode (nodemon + ts-node on examples) ``` ## Build System -- **TypeScript (tsc)** for ESM (`dist/esm/`) and CJS (`dist/cjs/`) builds -- **esbuild** (`build.js`) for browser IIFE bundle (`dist/browser/`) -- **Type declarations** generated separately to `dist/types/` -- Dual ESM/CJS publishing with conditional exports in package.json -- `"type": "module"` — project uses ESM by default +- **ESM-only.** There is no CJS build and no `require("tslog")` — v5 dropped dual publishing. +- **tsgo** (`@typescript/native-preview`, the TypeScript 7 native compiler) emits the ESM output (`dist/esm/`) and the declaration files (`dist/types/`). +- **esbuild** (`build.js`) bundles the browser IIFE (`dist/browser/index.js`, global `tslog`) from `src/index.browser.ts`. +- `"type": "module"` — the project is ESM throughout. +- `npm run build` = `build-types` → `build-esm` → `build-browser` → `prepare-publish`. +- **Conditional exports** in `package.json` pick the entry per runtime: `node` → `index.node.js`, `browser`/`worker` → `index.browser.js`, `deno`/`bun`/`react-native`/`default` → `index.universal.js`. Subpaths (`tslog/transports/*`, `tslog/presets/*`, `tslog/otel`, `tslog/serializers`, `tslog/lite`, `tslog/slim`, `tslog/console`, `tslog/testing`, `tslog/pretty/box`, `tslog/throttle`, `tslog/cli`) are individually mapped and tree-shakeable (`sideEffects: false`, audited by `npm run audit-sideeffects`; gzip budgets for the slim/full browser bundles enforced by `npm run check-bundle-size`). +- The `tslog` bin (NDJSON pretty-printer) is `dist/esm/subpaths/cli.js`. ### Build configs | Config | Purpose | |---|---| -| `tsconfig.json` | Base (ES2020, strict, NodeNext) | -| `tsconfig.esm.json` | ESM output | -| `tsconfig.cjs.json` | CJS output | -| `tsconfig.types.json` | Declaration files only | +| `tsconfig.json` | Base (ES2022, strict, NodeNext) | +| `tsconfig.esm.json` | ESM output (`dist/esm/`) | +| `tsconfig.types.json` | Declaration files only (`dist/types/`) | ## Testing @@ -61,25 +73,68 @@ npm run dev-ts # Watch mode (nodemon + ts-node on examples) ## Source Architecture +v5 is organized into `core/` (runtime-agnostic logic), `env/` (per-runtime environment providers), `internal/` (small shared helpers), `render/` (output), and `subpaths/` (tree-shakeable add-ons). The entry points differ only in which environment factory they bind; the `Logger` class itself is shared. + ``` src/ -├── BaseLogger.ts # Core logger: settings, masking, formatting, transports -├── index.ts # Logger class (extends BaseLogger), default log levels -├── index.browser.ts # Browser entry point (Safari detection, CSS styling) -├── interfaces.ts # All TypeScript interfaces and types -├── formatTemplate.ts # Template string replacement -├── prettyLogStyles.ts # Default pretty-print styles -└── internal/ - ├── environment.ts # Runtime detection (Node, browser, Deno, Bun, worker) - ├── stackTrace.ts # Stack trace parsing - ├── errorUtils.ts # Error chain collection - ├── metaFormatting.ts # Metadata formatting for pretty output - └── jsonStringifyRecursive.ts # Circular-safe JSON serialization +├── index.ts # Logger class (extends BaseLogger) + shared exports +├── index.node.ts # Node entry — binds createNodeEnvironment +├── index.universal.ts # Deno/Bun/React Native/default entry +├── index.browser.ts # Browser/worker entry (Safari detect, CSS styling) +├── BaseLogger.ts # Core logger plumbing +├── interfaces.ts # All TypeScript interfaces and types +├── formatTemplate.ts # Template string replacement +├── formatNumberAddZeros.ts # Zero-padding for template date/time parts +├── urlToObj.ts # URL → plain-object serialization +├── core/ # Runtime-agnostic logic +│ ├── settings.ts # Grouped-settings normalization/merge + v4-key migration warnings +│ ├── config.ts # defineConfig + TslogConfigError +│ ├── pipeline.ts # log() → mask → logObj → meta → format → transport +│ ├── masking.ts # keys / paths / regex / censor masking +│ ├── logObj.ts # Build the structured log object +│ ├── meta.ts # _logMeta assembly +│ ├── levels.ts # DefaultLogLevels + custom levels +│ ├── transports.ts # Transport registry + isolation +│ ├── asyncContext.ts # runInContext / getContext (AsyncLocalStorage) +│ ├── fromEnv.ts # Logger.fromEnv (TSLOG_LEVEL/TYPE/NAME) +│ ├── levelPersistence.ts # persistLevel (browser localStorage) +│ ├── features.ts # CoreFeatures seam (what a build ships: masking, JSON renderer, …) +│ └── features.full.ts # Full feature set injected by the standard entries +├── env/ # Per-runtime environment providers +│ ├── environment.ts # EnvironmentProvider contract + factory types +│ ├── environment.node.ts # createNodeEnvironment +│ ├── environment.browser.ts# createBrowserEnvironment +│ ├── environment.universal.ts # createUniversalEnvironment / selectEnvironment +│ ├── environment.slim.ts # Minimal provider for tslog/slim +│ ├── providerBase.ts # Shared provider base (meta markup, transport formatting) +│ ├── shared.ts # Helpers shared across providers (paths, meta text) +│ ├── stackTrace.ts # Stack capture/parse +│ ├── sourceMap.node.ts # Source-map error-position resolution (Node/Bun/Deno) +│ └── stdoutSink.node.ts # Buffered stdout sink (Node JSON output) +├── internal/ # Small shared helpers +│ ├── environment.ts # Runtime detection, TTY/color probing, resolveDefaultType +│ ├── errorUtils.ts # Error introspection helpers +│ ├── exitHooks.ts # Process/page exit flushing for transports +│ ├── jsonStringifyRecursive.ts # Circular-safe JSON stringify +│ ├── metaFormatting.ts # Pretty meta template rendering +│ ├── nativeConsole.ts # Untouched console method access +│ └── InspectOptions.interface.ts # Runtime-free InspectOptions type +├── render/ # Output formatting +│ ├── json.ts # JSON serialization (flat, fields-first, _logMeta.v) +│ ├── inspect.ts # native util.inspect (Node) +│ ├── inspect.polyfill.ts # off-Node inspect polyfill +│ └── styles.ts # pretty styling +└── subpaths/ # Tree-shakeable add-ons (each its own export) + ├── presets/{pino,otel,genai}.ts + ├── transports/{file,http,ringBuffer,worker,worker.runner}.ts + ├── serializers/std.ts ├── pretty/box.ts + ├── lite.ts ├── slim.ts ├── testing.ts ├── throttle.ts + ├── wrapConsole.ts └── cli.ts # tslog bin (NDJSON pretty-printer) ``` -**Log lifecycle:** `log()` → mask → toLogObj → addMeta → format → transport +**Log lifecycle:** `log()` → mask → build logObj → attach `_logMeta` → middleware (`use`) → format → transports (`render/` formats; per-transport `format` can override). -**7 log levels:** silly(0), trace(1), debug(2), info(3), warn(4), error(5), fatal(6) +**7 default log levels:** silly(0), trace(1), debug(2), info(3), warn(4), error(5), fatal(6). Add more via the `customLevels` setting or `addLevel()`. ## Git Workflow @@ -92,15 +147,15 @@ src/ - Uses **np** for releases (`npm run release`) - Publishes from `dist/` directory (`publishConfig.directory: "dist"`) -- `scripts/prepare-publish.mjs` copies package.json/LICENSE/README into dist with adjusted paths -- Supports: Node.js (ESM/CJS), browsers (IIFE), Deno, Bun, React Native +- `scripts/prepare-publish.mjs` writes a dist-relative `package.json` and copies `LICENSE`, `README.md`, `llms.txt`, `RECIPES.md`, and `MIGRATION_v4_to_v5.md` into `dist/` +- Supports: Node.js (ESM), browsers (IIFE), Deno, Bun, React Native — all ESM, no CJS ## Key Conventions -- Node.js 16+ required (`.nvmrc`) +- Node.js 20+ required (`package.json` `engines`); ES2022 target - npm only (engine-strict in `.npmrc`) -- No additional runtime dependencies -- TypeScript strict mode throughout +- Zero runtime dependencies +- TypeScript 7 (tsgo) strict mode throughout; ESM-only - Tests are numbered by feature area (e.g., `1_json_loglevel`, `5_pretty_Log_Types`) - Browser-specific code isolated in `index.browser.ts` and `tests/support/` @@ -109,4 +164,10 @@ src/ - Target **100% test coverage** — but only with meaningful tests, no padding - Every new feature **must** have corresponding tests - Every new feature **must** be reflected in the docs (`docs/`) -- Don't write tests just to hit coverage numbers; each test should verify real behavior \ No newline at end of file +- Don't write tests just to hit coverage numbers; each test should verify real behavior + +### AI-facing docs (`llms.txt`, `RECIPES.md`) + +- `llms.txt` (machine-readable API surface for code generation) and `RECIPES.md` (copy-paste patterns) are published with the package and must stay consistent with each other and with the public API. When you change a public setting/method or one of these files, update both — and the README — in the same change. +- `npm run check-doc-sync` (part of `npm run check`, run by the pre-commit hook and CI) enforces the mechanical invariants: every setting/symbol named in the docs exists in the source, the masking examples don't conflict, and relative links resolve. +- The script only checks structural drift. For semantic accuracy/completeness, run the deeper `audit-ai-docs` workflow before a release. diff --git a/CHANGELOG.md b/CHANGELOG.md index d6c546c7..5def7894 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,57 @@ All notable changes to this project are documented here. This project adheres to [Semantic Versioning](https://semver.org/). +## [5.0.0] - 2026-06-30 + +A ground-up rewrite. tslog is now ESM-only, zero-dependency, Node >=20, and built with TypeScript 7 / ES2022. Settings are grouped, JSON output is fields-first, and the logger gains a middleware pipeline, async transports, JSONPath masking, OpenTelemetry/pino/GenAI presets, ready-made file/http/ringbuffer/worker transports, and tree-shakeable subpath modules. v5 also adds first-class support for agents and LLMs — fields-first calls, agent/session correlation, and OTel-GenAI attributes; [OpenClaw](https://openclaw.ai) uses tslog for its agent logging. This is a breaking release — see [MIGRATION_v4_to_v5.md](MIGRATION_v4_to_v5.md) for the upgrade path. + +### Added +- **Grouped settings** — related options now live under `pretty`, `json`, `mask`, `stack`, and `meta` groups instead of a flat list of `prettyLog*`/`maskValues*` keys. Sub-loggers merge groups rather than overwrite them. +- **Fields-first JSON output** — every level method is overloaded pino-style: `info(fields, message?, ...args)` as well as `info(message, ...args)`. A single object spreads its fields to the top level, a leading object plus string spreads fields and sets `message`, and positional args land under `message`/`"1"`/… Runtime metadata moves under `_logMeta` carrying a `v: 5` schema marker. All JSON keys are configurable via the `json` group. +- **Middleware pipeline** — `logger.use(middleware)` runs functions over each log context to mutate `logObj`/`meta` or drop the log entirely (return `null`/`false`). +- **Async transports** with `attachTransport()` returning a detach function, `logger.flush()`, and `Symbol.asyncDispose`/`Symbol.dispose` support (`await using`). Each transport may declare its own `minLevel` and `format` (`"pretty"`, `"json"`, or a custom formatter). +- **Advanced masking** — `mask.paths` (JSONPath-ish patterns such as `user.password` or `*.token`), `mask.regex`, and a `mask.censor` of `"remove"`, `"hash"`, a string, or a function (with `mask.hashLabel`). +- **Presets** — `tslog/presets/pino` (`pinoFormat`, `pinoTransport`, `toPinoLevel`), `tslog/otel` (`otelFormat`, `toOtelRecord`, `levelToSeverityNumber`, `OtelSeverityNumber`, `otelTraceContext`, `stringifyOtelRecord`), and `tslog/presets/genai` (`genai`, `genaiAttributes`, `genaiSummary` emitting OTel `gen_ai.*` fields). +- **Built-in transports** — `tslog/transports/file` (`fileTransport`, non-blocking, flush/dispose), `tslog/transports/http` (`httpTransport`, batched), `tslog/transports/ringbuffer` (`ringBufferTransport` with `.dump()`/`.clear()`), and `tslog/transports/worker` (`workerTransport`, Node-only off-thread sink I/O). +- **Standard serializers** — `tslog/serializers` exports `stdSerializers` (`err`, `req`, `res`, `user`), the individual serializers, and a `serialize(map)` middleware helper. +- **Context propagation** — `runInContext(ctx, fn)` uses AsyncLocalStorage to attach context fields to `_logMeta` when `meta.attachContext` is enabled. Auto-resolves on Node/Deno/Bun; on Cloudflare Workers inject one via the `contextStorage` setting (graceful no-op in browsers, with a one-time development warning). +- **Custom levels** via the `customLevels` setting and `log(levelId, levelName, ...args)`. +- **New API surface** — `child()` (alias of `getSubLogger()`), `isLevelEnabled()`, `getContext()`, `addLevel()`, `logger.if(condition)`, `Logger.fromEnv()`, `defineConfig()`, and `TslogConfigError` (thrown when `strictConfig` is on). +- **Subpath modules** (all tree-shakeable) — `tslog/lite` (minimal console wrappers preserving native line numbers), `tslog/cli` (also the `tslog` bin, an NDJSON pretty-printer for stdin), `tslog/testing` (`createTestLogger`, `mockLogger`), `tslog/throttle` (rate-limit middleware), `tslog/pretty/box` (`box`, `tree`), and `tslog/console` (`wrapConsole`, `restoreConsole`, `isConsoleWrapped`). +- **Env-aware colorization** — when `type` is omitted, output is `pretty` everywhere (server, CI, browser, React Native); only the coloring adapts to the environment: colored on an interactive TTY (CSS in the browser) and uncolored when stdout is 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. `NO_COLOR` strips colors without switching the format; `FORCE_COLOR` forces styled pretty. Applies to both `new Logger()` and the ready-made `log`. +- **React Native support** — detected via `navigator.product` (`_logMeta.runtime: "react-native"`, Hermes engine version when available), Hermes/JSC stack frames parsed with a hybrid parser, pretty output by default. +- **Real hostname in server JSON logs** — `_logMeta.hostname` resolves from `HOSTNAME`/`HOST`/`COMPUTERNAME`, then the OS hostname (`Deno.hostname()` / `node:os` via `process.getBuiltinModule`), instead of defaulting to `"unknown"`. +- **Tree-shakeable exports** — `sideEffects: false` (audited) with per-runtime conditional exports. +- **`tslog/slim`** — the smallest structured-JSON build (~9KB gzip vs ~19KB for the full browser entry, budget-checked in CI): the same pipeline minus masking, pretty output, and stack capture; `mask` settings and `type: "pretty"` throw instead of silently degrading. +- **Buffered stdout sink (Node)** — the Node entry writes `type: "json"` lines through a batched `process.stdout.write` (one write per event-loop turn, early flush past ~8KB) instead of per-line `console.log`; drained by `logger.flush()`, `await using`, and guarded `beforeExit`/`exit` hooks (a bare `process.exit()` loses nothing). Browser/universal entries keep `console.log`. +- **Time seam** — an injectable top-level `clock: () => Date` (deterministic tests, offset/monotonic stamping; inherited by sub-loggers, hostile clocks ignored) and `json.time: "iso" | "epoch" | false | fn` controlling the top-level timestamp representation (`_logMeta.date` stays UTC ISO). +- **Deterministic test output** — `createTestLogger(settings, { now, normalize })`: `now` freezes only that logger's clock (no fake-timer sledgehammer), `normalize: true` yields snapshot-stable records/lines; plus a standalone `normalizeMeta(recordOrLine)` scrubber (all in `tslog/testing`). +- **Real OTLP/JSON in `tslog/otel`** — `otlpFormat`/`toOtlpJson`/`toOtlpLogRecord`/`toOtlpAnyValue`/`stringifyOtlpRequest` emit the collector wire format (camelCase proto3 fields, typed attributes, `resourceLogs[].scopeLogs[].logRecords[]` envelope, `exception.*` semconv mapping for logged errors), and `otlpBatchBody` pairs with the http transport's new `encodeBody` option to POST merged batches straight to `/v1/logs`. +- **`httpTransport({ encodeBody })`** — custom body encoder for endpoints whose payload is neither NDJSON nor a JSON array (used by the OTLP pairing above). +- **Conditional logging** — `logger.if(condition)` returns the logger when the condition is truthy and a no-op stand-in when falsy, so a per-call guard reads as a fluent chain (`log.if(!ok).warn("failed", { id })`). Use `isLevelEnabled()` to skip expensive payload construction. +- **Browser-native pretty objects** — `pretty.passObjectsNatively` hands non-`Error` arguments to the console by reference (on by default in real browsers), so DevTools renders collapsible, interactive trees; pair with `pretty.levelMethod` for native warn/error stack groups. Set `false` for log-time snapshots or text-matchable console output. +- **Source-mapped error positions** — on Node, Bun, and Deno, logged `Error` stack frames resolve through discoverable source maps back to original `.ts` file/line/column (automatic outside production; override with `TSLOG_SOURCE_MAPS=on`/`off`). + +### Changed +- **ESM-only** and **Node >=20**; the project now targets **TypeScript 7 / ES2022**. +- **JSON output on Node no longer goes through `console.log`** (see the buffered stdout sink above) — code intercepting `console.log` must spy on `process.stdout.write` or use `type: "hidden"` plus a transport. +- **`tslog/otel` resource precedence** — in `toOtelRecord`, `resource` attributes now win over colliding per-record fields (resource identity semantics); in the OTLP shape they live in the envelope, separate from record attributes. +- The default JSON shape is fields-first with `_logMeta.v: 5`; `name`/`parentNames` appear only when set (no `"[undefined]"` noise). +- **Masking is off by default** — `mask.keys` starts empty; enable it explicitly. + +### Removed +- The **CommonJS build** and `require("tslog")` — the package is ESM-only. +- The **`overwrite.*` hooks** (`mask`, `toLogObj`, `addMeta`, `formatMeta`, `formatLogObj`, `transportFormatted`, `transportJSON`, `addPlaceholders`) — use middleware and per-transport `format` instead. +- **Flat settings keys** — `prettyLogTemplate`/`prettyError*`/`prettyLog*`, `stylePrettyLogs`, `maskValuesOfKeys`/`maskValuesRegEx`/`maskPlaceholder`, `metaProperty`, and `stackDepthLevel` (now the `callerFrame` constructor parameter). +- **`hideLogPositionForProduction`** — superseded by the `stack` group and env-aware defaults. +- The **`loggerEnvironment`/`createLoggerEnvironment` singleton** — each entry point exports its own environment factory (`createNodeEnvironment`, `createBrowserEnvironment`, `createUniversalEnvironment`/`selectEnvironment`). +- The nested `{"0": message}` JSON shape. + +### Fixed +- During the rewrite: `URL` values now render correctly instead of as empty objects; caller-frame detection no longer over-matches internal frames; the pino fields-first overload no longer collides with the string-first signature; and bare `Error` arguments preserve their `cause` chain instead of dropping it. +- The browser stack parser now handles Windows drive-letter paths served by Vite (`/@fs/C:/…`), so log positions resolve correctly on Windows instead of being truncated to `/@fs/C`. (#323, #302) + + ## [4.11.0] - 2026-07-07 A backward-compatible release that adds several requested features, fixes a batch of reported bugs, unifies code-position detection across every runtime, and modernises the test/build tooling. No breaking changes — see the upcoming **v5** for those. diff --git a/MIGRATION_v4_to_v5.md b/MIGRATION_v4_to_v5.md new file mode 100644 index 00000000..43b22e37 --- /dev/null +++ b/MIGRATION_v4_to_v5.md @@ -0,0 +1,730 @@ +# 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 (`"[