Skip to content

console: honor util.inspect.defaultOptions on the global console - #35061

Open
robobun wants to merge 6 commits into
mainfrom
farm/84f7d339/console-inspect-default-options
Open

robobun wants to merge 6 commits into
mainfrom
farm/84f7d339/console-inspect-default-options

Conversation

@robobun

@robobun robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The global console.log/warn/error/dir use Bun's native formatter, which has no knowledge of util.inspect.defaultOptions. Setting depth, maxArrayLength, numericSeparator, etc. correctly changed util.inspect, util.format('%O'), and new console.Console(stream) output, but left global console output untouched, so Node's documented process-wide knob was a silent no-op on the one console most code actually uses.

import util from "node:util";
util.inspect.defaultOptions.depth = 0;
util.inspect.defaultOptions.maxArrayLength = 2;
util.inspect.defaultOptions.numericSeparator = true;
console.log({ a: { b: { c: { d: 1 } } } });
console.log([1, 2, 3, 4, 5, 6]);
console.log(1234567);
Node Bun (before)
depth { a: [Object] } full depth
maxArrayLength [ 1, 2, ... 4 more items ] all 6 items
numericSeparator 1_234_567 1234567

Cause

message_with_type_and_level_ in src/jsc/ConsoleObject.rs builds its FormatOptions from compiled-in defaults (plus --console-depth) and hands them to the native Formatter. Nothing on that path reads inspectDefaultOptions, and the native formatter does not implement most of the option keys anyway.

Fix

util.inspect.defaultOptions now returns a Proxy over the sealed defaults object. The first write installs a pair of JS formatters on the VM (formatWithOptions for log-style calls, inspect for console.dir); once installed, message_with_type_and_level_ routes argument formatting through them instead of the native printer, so every documented option applies. The fast native path is kept for the common case where defaultOptions is never touched. State is per-VM, so workers are isolated.

Verification

$ bun bd test test/js/node/console/console.test.ts
 11 pass
 0 fail

New tests fail on released Bun (depth/maxArrayLength/numericSeparator ignored) and pass with this change; output now matches Node byte-for-byte for the cases above.

Fixes #12762


[review] gate passed · iteration 1 · 5 files touched

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/console/console.test.ts
ninja: Entering directory `/workspace/bun/build/debug'
[1/57] gen cpp.rs (cppbind)
[2/57] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 237 extern-C blocks audited
[3/57] gen JS modules (bundle-modules)
Preprocess modules (9115ms)
Bundle modules (70ms)
Postprocesss modules (26ms)
Bundle Functions (733ms)
Generate Code (8ms)

[9.97s] Bundled "src/js" for development
  2210 kb
  165 internal modules
  13 native modules
  90 internal functions across 19 files
[3/45] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

[11/45] cxx obj/unified/UnifiedSource-src_jsc_bindings_webcore-15.cpp.o
FAILED: obj/unified/UnifiedSource-src_jsc_bindings_webcore-15.cpp.o 
/usr/bin/ccache /usr/lib/llvm-21/bin/clang++ -march=nehalem -O0 -g3 -gz=zstd -glldb -fsanitize=address -fno-exceptions -fno-c++-static-destructors -fno-rtti -fno-omit-frame
... (truncated)

release without fix: 1 FAILED
bun test v1.4.0-canary.1 (9641b92ed)

test/js/node/console/console.test.ts:
(pass) console.Console > global instanceof Console [3.27ms]
(pass) console.Console > new Console instanceof Console [1.28ms]

(pass) console.Console > it can write to a stream [3.26ms]
(pass) console.Console > can enable colors [0.43ms]
(pass) console.Console > stderr and stdout are separate [0.28ms]
(pass) console._stdout [0.05ms]
(pass) console._stderr [0.05ms]
(pass) global console honors util.inspect.defaultOptions > depth / maxArrayLength / numericSeparator on console.log [27.54ms]
(pass) global console honors util.inspect.defaultOptions > Object.defineProperty on defaultOptions [23.66ms]
(pass) global console honors util.inspect.defaultOptions > console.timeLog extra args [36.50ms]
(pass) global console honors util.inspect.defaultOptions > setter form and console.dir [37.50ms]
(pass) global console honors util.inspect.defaultOptions > group indentation still applied [31.50ms]
(pass) global console honors util.inspect.defaultOptions > writes via an inheriting object do not leak into the shared defaults [32.02ms]
(pass) global console honors util.inspect.defaultOptions > defaultOptions r
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/console/console.test.ts
bun test v1.4.0 (4bf7b6024)

test/js/node/console/console.test.ts:
(pass) console.Console > global instanceof Console [3.52ms]
(pass) console.Console > new Console instanceof Console [71.93ms]

(pass) console.Console > it can write to a stream [174.57ms]
(pass) console.Console > can enable colors [21.91ms]
(pass) console.Console > stderr and stdout are separate [19.06ms]
(pass) console._stdout [2.35ms]
(pass) console._stderr [2.67ms]
(pass) global console honors util.inspect.defaultOptions > Object.defineProperty on defaultOptions [941.69ms]
(pass) global console honors util.inspect.defaultOptions > depth / maxArrayLength / numericSeparator on console.log [986.49ms]
(pass) global console honors util.inspect.defaultOptions > console.timeLog extra args [957.48ms]
(pass) global console honors util.inspect.defaultOptions > setter form and console.dir [1066.23ms]
(pass) global console honors util.inspect.defaultOptions > --console-depth still applies when an unrelated default is changed [1761.16ms]
(pass
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1043ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/54] gen cpp.rs (cppbind)
[2/54] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 237 extern-C blocks audited
[3/54] gen JS modules (bundle-modules)
Preprocess modules (8164ms)
Bundle modules (50ms)
Postprocesss modules (206ms)
Bundle Functions (798ms)
Generate Code (120ms)

[9.37s] Bundled "src/js" for production
  2041 kb
  165 internal modules
  13 native modules
  90 internal functions across 19 files
[3/44] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[
... (truncated)
diff hotspot
src/js/internal/util/inspect.js       |  45 +++++++++++-
 src/jsc/ConsoleObject.rs              |  90 ++++++++++++++++++++---
 src/jsc/VirtualMachine.rs             |  10 +++
 src/runtime/node/node_util_binding.rs |  27 +++++++
 test/js/node/console/console.test.ts  | 132 ++++++++++++++++++++++++++++++++++
 5 files changed, 293 insertions(+), 11 deletions(-)

gate history · 4 passed · 0 rejected · iteration 1

evidence per changed file
file                                   reads  edits  tests
src/js/internal/util/inspect.js            7     10      0
src/jsc/ConsoleObject.rs                  10      4      0
src/jsc/VirtualMachine.rs                  6      4      0
src/runtime/node/node_util_binding.rs      4      5      0
test/js/node/console/console.test.ts       2      8      0

The global console.log/warn/error/dir use Bun's native formatter, which
has no knowledge of util.inspect.defaultOptions. Setting options like
depth, maxArrayLength, or numericSeparator changed util.inspect and
new console.Console(stream) output, but left global console output
untouched, so the documented process-wide knob was a silent no-op on
the one console most code actually uses.

util.inspect.defaultOptions now hands back a Proxy over the sealed
defaults object. The first write installs a pair of JS formatters
(formatWithOptions for log-style calls, inspect for console.dir) on
the VM; once installed, the native console routes argument formatting
through them instead of its own printer. The fast native path is kept
for the common case where defaultOptions is never touched.
@robobun

robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced with:

import util from "node:util";
util.inspect.defaultOptions.depth = 0;
util.inspect.defaultOptions.maxArrayLength = 2;
util.inspect.defaultOptions.numericSeparator = true;
console.log({ a: { b: { c: { d: 1 } } } });
console.log([1, 2, 3, 4, 5, 6]);
console.log(1234567);

PR #35061 routes the global console through util.formatWithOptions once defaultOptions has been mutated; the fast native formatter stays in place otherwise. Also covers console.dir, console.timeLog's extra data args, and preserves --console-depth on the fallback path without widening that flag to util.inspect().

CI: the new test/js/node/console/console.test.ts cases pass on every lane across builds 77432/77442/77497. Remaining red is unrelated GC/WeakRef tests (test-tls-connect-memleak.js, test-gc-http-client-connaborted.js) that this diff does not touch; both filed for main-break triage. Other failures passed on retry. Ready for review.

@robobun

robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:32 AM PT - Jul 22nd, 2026

❌ @robobun, your commit 4bf7b60 has 3 failures in Build #77497 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35061

That installs a local version of the PR into your bun-35061 executable, so you can run:

bun-35061 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Add Global Configuration Options for console.log Output Formatting #12762 - Requests global configuration for console.log output formatting via util.inspect.defaultOptions, which is exactly what this PR implements
  2. console.dir regression #31808 - Reports maxArrayLength stopped working with console.dir between v1.1.0 and v1.3.14; this PR fixes console.dir to properly respect options
  3. console.dir() prints strings the same way as console.log() #13878 - Reports console.dir() behaves identically to console.log() instead of properly serializing values; this PR fixes console.dir formatting

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #12762
Fixes #31808
Fixes #13878

🤖 Generated with Claude Code

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Added Fixes #12762 to the description.

The other two suggestions are related but not addressed here:

  • console.dir regression #31808 (console.dir(arr, {maxArrayLength: n})) and console.dir() prints strings the same way as console.log() #13878 (console.dir("x") quoting) are about per-call console.dir options on the native path. This PR only switches the console over to util.formatWithOptions/util.inspect once util.inspect.defaultOptions has been mutated, so those reproduce unchanged when defaultOptions is left alone. Fixing them means either routing console.dir through util.inspect unconditionally or teaching the native formatter the missing options, which I kept out of scope here.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Global console output now uses VM-backed formatter closures after util.inspect.defaultOptions mutations. Console depth integration, cleanup, native formatting fallbacks, and subprocess coverage for formatting behavior are included.

Inspect defaults and console formatting

Layer / File(s) Summary
Default options mutation bridge
src/js/internal/util/inspect.js, src/runtime/node/node_util_binding.rs, src/jsc/VirtualMachine.rs
Reads console depth, detects mutations to inspect.defaultOptions, forwards formatter closures to the VM, and releases them during teardown and test isolation.
Native console fallback
src/jsc/ConsoleObject.rs
Uses VM formatters for console messages and console.timeLog, including indentation, trace handling, and fallback formatting.
Console formatting validation
test/js/node/console/console.test.ts
Tests formatting options, console.dir, console.timeLog, depth configuration, grouping, sealed defaults, and inherited-object writes.

Possibly related PRs

  • oven-sh/bun#34347: Both changes reroute console output through src/jsc/ConsoleObject.rs formatting paths.
  • oven-sh/bun#34491: Both changes adjust trace handling in message_with_type_and_level_.

Suggested reviewers: jarred-sumner, cirospaciari, sosukesuzuki

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements global inspect.defaultOptions-driven console formatting, matching the linked issue's goal for configurable console inspection output.
Out of Scope Changes check ✅ Passed The changes stay focused on console inspection plumbing and tests, with no clear unrelated scope added.
Title check ✅ Passed The title clearly summarizes the main change to global console formatting honoring util.inspect.defaultOptions.
Description check ✅ Passed The description explains the change and includes verification details, though it uses custom headings instead of the template.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/internal/util/inspect.js`:
- Around line 733-736: Condense each new comment to no more than three lines
while preserving its explanation: update the formatter-bridge comment in
src/js/internal/util/inspect.js lines 733-736, the host-function documentation
in src/runtime/node/node_util_binding.rs lines 165-168, the VM-slot
documentation in src/jsc/VirtualMachine.rs lines 326-329, and the fallback-path
explanation in src/jsc/ConsoleObject.rs lines 561-564.

In `@src/jsc/ConsoleObject.rs`:
- Around line 592-599: Update the newline handling in the default-indentation
branch so formatter::write_indent_n is called only when rest contains content
after the matched newline. Do not emit indentation for a terminal newline, while
preserving indentation between newline-separated content segments and the
existing final write_all behavior.

In `@src/jsc/VirtualMachine.rs`:
- Around line 326-333: Clear both console formatter slots, console_util_format
and console_util_dir, in swap_global_for_test_isolation() before replacing
self.global, ensuring every isolation lifecycle exit releases callbacks from the
outgoing realm. Add a regression test that mutates inspect.defaultOptions in one
file and verifies default console formatting in the next file is unaffected.

In `@test/js/node/console/console.test.ts`:
- Around line 104-156: The console tests cover assignment and setter updates but
not the Object.defineProperty mutation path. Add a concurrent test near “setter
form and console.dir” that redefines util.inspect.defaultOptions.depth via
Object.defineProperty and asserts console output reflects depth 0, along with
successful exit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bded88a7-000a-4675-bbab-5a097e7d7dba

📥 Commits

Reviewing files that changed from the base of the PR and between 03eb4be and 4af90cb.

📒 Files selected for processing (5)
  • src/js/internal/util/inspect.js
  • src/jsc/ConsoleObject.rs
  • src/jsc/VirtualMachine.rs
  • src/runtime/node/node_util_binding.rs
  • test/js/node/console/console.test.ts

Comment thread src/js/internal/util/inspect.js Outdated
Comment thread src/jsc/ConsoleObject.rs
Comment thread src/jsc/VirtualMachine.rs Outdated
Comment thread test/js/node/console/console.test.ts
Comment thread src/js/internal/util/inspect.js Outdated
Writing to an object that inherits from util.inspect.defaultOptions
(Object.create(util.inspect.defaultOptions)) was leaking through to the
shared defaults because the set trap dropped the receiver argument.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/console/console.test.ts`:
- Around line 168-182: Strengthen the test around the inheriting object setup by
capture the console output before my.depth = 10, then compare the post-write
console.log output against that baseline exactly. Keep the existing sharedDepth
and ownDepth assertions, and ensure the console result assertion cannot pass
when no object is emitted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ca33dd3a-f45d-4182-bf80-8b48aad15ea6

📥 Commits

Reviewing files that changed from the base of the PR and between 4af90cb and 774165a.

📒 Files selected for processing (5)
  • src/js/internal/util/inspect.js
  • src/jsc/ConsoleObject.rs
  • src/jsc/VirtualMachine.rs
  • src/runtime/node/node_util_binding.rs
  • test/js/node/console/console.test.ts

Comment thread test/js/node/console/console.test.ts
Comment thread src/jsc/ConsoleObject.rs
Comment thread src/jsc/ConsoleObject.rs
Comment thread src/js/internal/util/inspect.js
robobun and others added 2 commits July 22, 2026 05:09
…pth from --console-depth

console.timeLog's extra data args are formatted by a separate
ConsoleClient hook that was still using the native formatter; check
the same VM slot there.

inspectDefaultOptions.depth now starts at the --console-depth /
bunfig console.depth value when one is set, so touching an unrelated
default (maxArrayLength, etc.) no longer drops the console back to
depth 2.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/ConsoleObject.rs`:
- Around line 6018-6043: Update the formatter path in the console time-log
handling to propagate failures from console_util_format through the same VM
exception/OOM handling used by message_with_type_and_level, rather than
discarding them in nested if let Ok checks. Complete formatting and validate its
result before writing the elapsed-time label or any other output, while
preserving the existing successful formatting and newline/flush behavior.

In `@test/js/node/console/console.test.ts`:
- Around line 153-169: Update the test case around the Bun.spawn call and its
Promise.all to also await proc.stderr.text() alongside stdout and proc.exited,
ensuring the piped stderr stream is drained while preserving the existing stdout
and exit-code assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d27ebe54-c10b-4188-84ba-302a6544eb0d

📥 Commits

Reviewing files that changed from the base of the PR and between 774165a and 7088071.

📒 Files selected for processing (4)
  • src/js/internal/util/inspect.js
  • src/jsc/ConsoleObject.rs
  • src/runtime/node/node_util_binding.rs
  • test/js/node/console/console.test.ts

Comment thread src/jsc/ConsoleObject.rs
Comment thread test/js/node/console/console.test.ts
Comment thread src/js/internal/util/inspect.js Outdated
Seeding inspectDefaultOptions.depth from --console-depth widened the
flag to util.inspect(), util.format, user Console instances, and the
REPL defaults, and made util.inspect.defaultOptions.depth diverge from
Node's documented 2. Instead, keep the Node default and thread the CLI
depth into the fallback closures only, yielding to an explicit
defaultOptions.depth write when the user makes one.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier findings (timeLog routing, --console-depth scope leak) are addressed in 4bf7b60 and no new issues turned up on this revision. Deferring the final call to a maintainer given the design trade-offs — the Proxy-based write detection (accepted isProxy divergence) and the native→util.inspect formatter switch on first mutation are choices worth a human sign-off.

What was reviewed:

  • --console-depth now stays scoped to the global-console fallback; util.inspect.defaultOptions.depth reads 2 and util.inspect()/user Console instances are unaffected (test asserts both).
  • strong::Optional slots are deinit'd on both VM teardown paths; per-VM so workers don't share state.
  • to_slice on the JS-formatted result goes through to_utf8(), so non-ASCII output is written correctly.
  • Proxy set trap forwards receiver for inherited writes — covered by the Object.create(defaultOptions) test.
Extended reasoning...

Overview

The PR wires util.inspect.defaultOptions into the global console by wrapping the sealed defaults in a Proxy whose first write installs two JS formatter closures (formatWithOptionsInternal for log-style, inspect for console.dir) as strong::Optional slots on VirtualMachine. message_with_type_and_level_ and Bun__ConsoleObject__timeLog check those slots and route through the JS formatters when set, otherwise keep the native fast path. Two host functions in node_util_binding.rs bridge --console-depth and the formatter install. ~280 lines across 5 files with 8 new subprocess tests.

Security risks

None. No untrusted-input parsing, auth, or filesystem/network surface. The Proxy handler is __proto__: null and uses captured Reflect primordials; the fallback closures are module-private and only reachable via the native binding.

Level of scrutiny

Medium-high. ConsoleObject.rs is a hot path for every console.log, and the change introduces two design choices a maintainer should ratify: (1) once any default option is mutated, all subsequent global-console output goes through the slower JS util.inspect path instead of the native formatter — reasonable for Node compat but a deliberate perf/fidelity trade; (2) util.inspect.defaultOptions is now observably a Proxy (util.types.isProxy returns true), which the author consciously accepted over per-key accessors. Neither is a bug, but both are the kind of API-shape decision REVIEW.md flags for maintainer agreement.

Other factors

This PR went through three rounds of bot review; each finding (missing timeLog sibling, --console-depth dropped by fallback, then --console-depth leaking into util.inspect defaults) was fixed with a targeted commit and a covering test. All inline threads are resolved. The test suite exercises depth/maxArrayLength/numericSeparator, Object.defineProperty, the setter form, console.dir per-call overrides, console.timeLog, console.group indentation on the fallback, sealed/identity invariants, and inherited-receiver writes. Strong-ref lifecycle is handled in both VM deinit sites. I checked JSValue::to_slice — it converts via to_utf8(), so writing the formatted bytes handles non-Latin1 content. Given the scope and the two design calls above, deferring rather than auto-approving.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Global Configuration Options for console.log Output Formatting

2 participants