All notable changes to this project are documented here. This project adheres to Semantic Versioning.
- Named sub-loggers on
tslog/lite—LiteLoggergainsname/nameSeparatoroptions andgetSubLogger()/child(). The label is partially applied withFunction.prototype.bind, so a sub-logger's level methods are still the bound nativeconsole.*functions and the devtools file:line badge keeps pointing at the caller. Every%in a label is escaped so a (possibly runtime-derived) name can never act as a console format specifier and consume a logged value. Sub-loggers nest to any depth, join names with the parent'snameSeparator(default":"), and inheritminLeveland the sink unless overridden; an unresolvableminLeveloverride inherits the parent's level, matching the fullLogger. Known trade-off (documented): on a named lite logger the label occupies the console's format-string slot, so printf-style specifiers in messages print literally. - Bundle-size budget for
tslog/lite—check-bundle-sizenow probes the lite subpath (~0.8 KB gzip measured, 1 KB budget).
- Readable default styling on light devtools themes — the default pretty styles color timestamps, log positions, and logger names
white, which the browser CSS path rendered as a near-white hex: invisible on light consoles (the Safari/Chrome default theme). Foregroundwhite/whiteBrightnow contribute nocolor:declaration, so that text keeps the console's own default color and is readable in both light and dark themes. Terminal ANSI output is unchanged; background tokens keep their palette hex.
- Next.js full-stack logging recipe — new RECIPES §12c: the full
Loggeron the server (pretty with original.tspositions in dev, JSON in production; optionally guarded withserver-only) andtslog/litein"use client"components, whose level methods are the bound nativeconsole.*functions — so the devtools file:line badge points at your component instead of a logger wrapper in a bundled chunk, and logged objects stay live and collapsible. The same split works for TanStack Start and other Vite-SSR frameworks.
A patch release that makes source-mapped error positions work out of the box with modern bundler output (Turbopack/Next.js dev, Rollup, Webpack) — verified end-to-end against live Next.js 16 (next dev --turbopack) and TanStack Start (Vite) dev servers — and fixes the default browser console output (error styling, stack parsing, log positions), verified in real Chromium, Firefox, and WebKit. No API or settings changes.
- Framework E2E suite (
e2e/, CI jobtest-e2e-apps) — boots real Next.js + Turbopack and TanStack Start + Vite dev servers against the packed tslog tarball and asserts that logged error frames and call-site positions resolve to the original.tssources. The fixtures track the latest framework releases, so upstream bundler changes surface in CI instead of in user issues.
- Indexed source maps (
sections) — the resolver now understands the sectioned map format emitted by Turbopack (Next.js dev) and other concatenating bundlers: section offsets are walked, positions are shifted into the section's coordinate space, and per-section sub-maps (inlinemapor externalurl) resolve like flat maps. Previously such frames kept pointing at the generated chunk. Everything — including section sub-maps — is parsed once per file and cached, so logging through a sectioned map stays as cheap as through a flat one. Verified against real Rollup and Webpack (ts-loader) output in the test suite. - Source-map resolution inside bundled server apps — bundlers rewrite tslog's dynamic
require(name)into an always-throwing stub (Turbopack: "expression is too dynamic"), which silently disabled resolution in bundled apps even when the maps were fine.node:fsis now acquired viaprocess.getBuiltinModulefirst (a plain runtime call bundlers leave untouched), withcreateRequirekept as the fallback for Node < 20.16. - Percent-encoded
sourceMappingURLs — the reference is a URL, so file names with[/]arrive percent-encoded (Turbopack:%5Broot-of-the-server%5D__x._.js.map); it is now decoded for the on-disk lookup, with the raw name as fallback for files literally containing%. - Turbopack virtual source paths — bracket-prefixed sources such as
[project]/src/app.tsnow reduce to the clean project-relativesrc/app.tsin log output (aswebpack://sources already did) instead of being wrongly anchored to the map's directory. - Caller detection around bundler runtime frames — unremapped Turbopack runtime chunks (
[root-of-the-server]__….js,[turbopack]_runtime.js) are skipped when locating the user's call site. - Browser console error styling — pretty-printed errors (
logger.error(new Error(…))) reached the browser console carrying raw ANSI escape codes, which only Chromium's DevTools interprets; Firefox and Safari printed literal[97m[101m…noise. Error blocks are now re-expressed as%cCSS segments (same red badge and colors as the terminal output, styled in every engine); literal%in error text is escaped so it can never consume a%cstyle argument. Whenpretty.passObjectsNativelytrails native args, the error text follows them ANSI-stripped instead. - Browser stack parsing on dev servers — a
host:portauthority (i.e. everylocalhost:5173-style dev server) broke browser stack-frame matching, and root-level scripts (/app.js) were rejected by the parser's two-segment minimum: log-position meta came up empty and pretty errors printed a bareerror stack:label with no frames. The scheme + authority is now stripped before path matching (the full URL still lands infullFilePath), sofilePathis consistently the origin-relative path — previously port-less hosts leaked into it as a bogus first segment (/example.com/script.js). - Error message noise on Firefox/Safari — Firefox stamps
fileName/lineNumber/columnNumberand WebKitline/column/sourceURLas own properties on everyError, and the pretty message line (which joins own properties) rendered them:Error http://localhost:5173/app.js, 3, 14, test. Engine position properties are now excluded, as is an ownnameproperty (thethis.name = "HttpError"subclass pattern) — the name is already rendered as the error badge, soHttpError Not Found, HttpError, 404collapses toHttpError Not Found, 404. Custom properties (err.codeetc.) still join the message. - Log position pointed into the tslog bundle for
<script src>/CDN usage — the browser IIFE has noimport.meta.url, so tslog couldn't recognize its own stack frames and caller auto-detection reported tslog's internal frame (tslog.js:24) as every log's position. The browser provider now detects the file it is served from at construction time (from a stack capture inside tslog code) and skips that file's frames — exact-match, so same-origin app scripts are unaffected, and when tslog is inlined into the app bundle behavior falls back to the previous first-frame result.
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 uses tslog for its agent logging. This is a breaking release — see MIGRATION_v4_to_v5.md for the upgrade path.
- Grouped settings — related options now live under
pretty,json,mask,stack, andmetagroups instead of a flat list ofprettyLog*/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 asinfo(message, ...args). A single object spreads its fields to the top level, a leading object plus string spreads fields and setsmessage, and positional args land undermessage/"1"/… Runtime metadata moves under_logMetacarrying av: 5schema marker. All JSON keys are configurable via thejsongroup. - Middleware pipeline —
logger.use(middleware)runs functions over each log context to mutatelogObj/metaor drop the log entirely (returnnull/false). - Async transports with
attachTransport()returning a detach function,logger.flush(), andSymbol.asyncDispose/Symbol.disposesupport (await using). Each transport may declare its ownminLevelandformat("pretty","json", or a custom formatter). - Advanced masking —
mask.paths(JSONPath-ish patterns such asuser.passwordor*.token),mask.regex, and amask.censorof"remove","hash", a string, or a function (withmask.hashLabel). - Presets —
tslog/presets/pino(pinoFormat,pinoTransport,toPinoLevel),tslog/otel(otelFormat,toOtelRecord,levelToSeverityNumber,OtelSeverityNumber,otelTraceContext,stringifyOtelRecord), andtslog/presets/genai(genai,genaiAttributes,genaiSummaryemitting OTelgen_ai.*fields). - Built-in transports —
tslog/transports/file(fileTransport, non-blocking, flush/dispose),tslog/transports/http(httpTransport, batched),tslog/transports/ringbuffer(ringBufferTransportwith.dump()/.clear()), andtslog/transports/worker(workerTransport, Node-only off-thread sink I/O). - Standard serializers —
tslog/serializersexportsstdSerializers(err,req,res,user), the individual serializers, and aserialize(map)middleware helper. - Context propagation —
runInContext(ctx, fn)uses AsyncLocalStorage to attach context fields to_logMetawhenmeta.attachContextis enabled. Auto-resolves on Node/Deno/Bun; on Cloudflare Workers inject one via thecontextStoragesetting (graceful no-op in browsers, with a one-time development warning). - Custom levels via the
customLevelssetting andlog(levelId, levelName, ...args). - New API surface —
child()(alias ofgetSubLogger()),isLevelEnabled(),getContext(),addLevel(),logger.if(condition),Logger.fromEnv(),defineConfig(), andTslogConfigError(thrown whenstrictConfigis on). - Subpath modules (all tree-shakeable) —
tslog/lite(minimal console wrappers preserving native line numbers),tslog/cli(also thetslogbin, an NDJSON pretty-printer for stdin),tslog/testing(createTestLogger,mockLogger),tslog/throttle(rate-limit middleware),tslog/pretty/box(box,tree), andtslog/console(wrapConsole,restoreConsole,isConsoleWrapped). - Env-aware colorization — when
typeis omitted, output isprettyeverywhere (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 viatype: "json",TSLOG_TYPE=json, or a JSON transport.NO_COLORstrips colors without switching the format;FORCE_COLORforces styled pretty. Applies to bothnew Logger()and the ready-madelog. - 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.hostnameresolves fromHOSTNAME/HOST/COMPUTERNAME, then the OS hostname (Deno.hostname()/node:osviaprocess.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;masksettings andtype: "pretty"throw instead of silently degrading.- Buffered stdout sink (Node) — the Node entry writes
type: "json"lines through a batchedprocess.stdout.write(one write per event-loop turn, early flush past ~8KB) instead of per-lineconsole.log; drained bylogger.flush(),await using, and guardedbeforeExit/exithooks (a bareprocess.exit()loses nothing). Browser/universal entries keepconsole.log. - Time seam — an injectable top-level
clock: () => Date(deterministic tests, offset/monotonic stamping; inherited by sub-loggers, hostile clocks ignored) andjson.time: "iso" | "epoch" | false | fncontrolling the top-level timestamp representation (_logMeta.datestays UTC ISO). - Deterministic test output —
createTestLogger(settings, { now, normalize }):nowfreezes only that logger's clock (no fake-timer sledgehammer),normalize: trueyields snapshot-stable records/lines; plus a standalonenormalizeMeta(recordOrLine)scrubber (all intslog/testing). - Real OTLP/JSON in
tslog/otel—otlpFormat/toOtlpJson/toOtlpLogRecord/toOtlpAnyValue/stringifyOtlpRequestemit the collector wire format (camelCase proto3 fields, typed attributes,resourceLogs[].scopeLogs[].logRecords[]envelope,exception.*semconv mapping for logged errors), andotlpBatchBodypairs with the http transport's newencodeBodyoption 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 })). UseisLevelEnabled()to skip expensive payload construction. - Browser-native pretty objects —
pretty.passObjectsNativelyhands non-Errorarguments to the console by reference (on by default in real browsers), so DevTools renders collapsible, interactive trees; pair withpretty.levelMethodfor native warn/error stack groups. Setfalsefor log-time snapshots or text-matchable console output. - Source-mapped error positions — on Node, Bun, and Deno, logged
Errorstack frames resolve through discoverable source maps back to original.tsfile/line/column (automatic outside production; override withTSLOG_SOURCE_MAPS=on/off).
- 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 interceptingconsole.logmust spy onprocess.stdout.writeor usetype: "hidden"plus a transport. tslog/otelresource precedence — intoOtelRecord,resourceattributes 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/parentNamesappear only when set (no"[undefined]"noise). - Masking is off by default —
mask.keysstarts empty; enable it explicitly.
- 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-transportformatinstead. - Flat settings keys —
prettyLogTemplate/prettyError*/prettyLog*,stylePrettyLogs,maskValuesOfKeys/maskValuesRegEx/maskPlaceholder,metaProperty, andstackDepthLevel(now thecallerFrameconstructor parameter). hideLogPositionForProduction— superseded by thestackgroup and env-aware defaults.- The
loggerEnvironment/createLoggerEnvironmentsingleton — each entry point exports its own environment factory (createNodeEnvironment,createBrowserEnvironment,createUniversalEnvironment/selectEnvironment). - The nested
{"0": message}JSON shape.
- During the rewrite:
URLvalues 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 bareErrorarguments preserve theircausechain 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)
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.
prettyLogLevelMethod— map log levels to specificconsolemethods (e.g. routeWARNtoconsole.warn,ERROR/FATALtoconsole.error), with a*fallback andconsole.logdefault. Useful for browser DevTools filtering and log aggregators. (#330)DefaultLogLevelsenum — the default log level ids (SILLY…FATAL) are now exported as a typed enum, usable forminLeveland custom loggers. (#308)includeDefaultMetaInAddMeta— when set, a customoverwrite.addMetahandler receives the default runtime meta as a fourth argument so it can extend rather than replace it. (#303)internalFramePatterns— register additional stack-frame patterns to treat as "internal" when auto-detecting the calling code position, so wrapper/custom loggers report their caller instead of the wrapper file. (#282)fileNameWithLineadded toIPrettyLogStylesso it can be styled like other placeholders; the inlineprettyLogStylestype now reusesIPrettyLogStyles. (#310)IMetaStatictypes now exposehostname,runtimeVersion, andbrowser, which were already populated at runtime but missing from the public type. (#268)
- Unified code-position detection across all runtimes. The browser entry no longer uses hardcoded Safari/other stack depths (
4/5); both entry points now use the same pattern-based auto-detection that finds the first non-tslog frame. Verified to resolve the correct caller on Node, Bun, Deno, web workers, Chrome, and Safari/WebKit. ManualstackDepthLeveloverrides still work. - Attached transports are now invoked in isolation: a transport that throws no longer crashes logging or prevents other transports (and the default console output) from running; the error is reported via
console.error. - Migrated the test toolchain to Vitest and Playwright (replacing Jest/Puppeteer), added a cross-runtime suite (Node, browser, Deno, Bun, workers) and a per-engine browser matrix (Chromium, Firefox, WebKit), and reached 100% coverage on the measured source.
- Replaced ESLint/Prettier with Biome, switched docs from docsify to Starlight, and modernised git hooks (Husky v9).
- BigInt values are rendered with the trailing
n(e.g.100n) instead of as an empty object{}. (#334) - Invalid
Datevalues render asInvalid Dateinstead of throwingRangeError: Invalid time value. (#266) - In
localtime zone,{{rawIsoStr}}now carries the real UTC offset (e.g.+02:00) instead of a misleadingZ, and round-trips to the correct instant. (#207) - Web workers are detected as CSS-capable consoles, so they receive
%cstyling instead of leaking ANSI control characters (notably in Firefox workers). (#262) maskValuesRegExplaceholders containing$1,$&, etc. are now inserted literally instead of being interpreted as regex substitution patterns (which could leak parts of a masked value).- Numeric
maskValuesOfKeys(e.g.[123]) now correctly match string property names. - Inspect options passed via
prettyInspectOptions(e.g.depth,colors) are now actually applied; previously_extendmutated a discarded copy of the context. (#331, #285, #327) - Logging an error whose property is a null-prototype object or has a throwing
toString/Symbol.toPrimitiveno longer crashes formatting. (#335, #294)
- Fixed the CommonJS build and included the
READMEin the publisheddistfolder.
- Internal release-tooling fixes: refactored the pre-commit hook and corrected
package.jsonpaths used during publishing. No user-facing changes.
- Custom
transportFormattedoverrides now receivelogMetaas the fourth argument; pass five parameters to also receivesettings, otherwise adjust implementations that previously readsettingsfrom the fourth position. - Deprecated runtime entry points under
src/runtime/**and related browser mappings have been removed; use the primaryLoggerexport instead of importing runtime-specific helpers. - Logger metadata now exposes lowercase runtime identifiers (for example
node,browser,deno,bun,worker) and normalized versions without the leadingv; adjust consumers that compared againstNodejsor relied on the old format.
- Introduced universal runtime detection that recognises Node.js, browsers, web workers, Deno, and Bun, enriching metadata with runtime versions and hostnames when available.
- Documented first-class Deno and Bun usage, refreshed examples under
examples/server, and aligned development scripts (npm run dev-ts*). - Pretty transports now detect when the browser console supports CSS, rendering styled output with
%ctokens and gracefully falling back when styling is unavailable. - Error formatting captures chained
Error.causeentries (up to depth five) and includes them in both pretty error blocks and JSON error objects.
- The core logger automatically locates the first user stack frame instead of relying on hard-coded depths, producing stable file and line metadata across bundlers; manual
stackDepthLeveloverrides continue to work. - Placeholder formatting now routes through a shared
buildPrettyMetautility, improving consistency for custom templates and nested style tokens. - Masking internals normalise and cache case-insensitive keys, reducing repeated allocations and keeping behaviour consistent when toggling mask options.
- Browser styling defaults keep ANSI colouring enabled unless explicitly disabled, letting CSS-capable consoles honour
stylePrettyLogswithout runtime-specific tweaks.
- Runtime error detection now treats objects with an
Error-suffixed name as errors, ensuring they are formatted via the error transport. - Browser stack parsing guards against malformed frames, avoiding crashes when devtools emit unexpected stack entries.
- Logging no longer fails when
process.cwd()throws (for example under restricted permissions); environment helpers fall back to cached working directories and hostname detection across Node, Deno, and Bun.