Skip to content

fix(logger): render log timestamps in local time instead of UTC - #1311

Merged
BYK merged 2 commits into
mainfrom
fix/log-timestamp-timezone-fallback
Jul 29, 2026
Merged

fix(logger): render log timestamps in local time instead of UTC#1311
BYK merged 2 commits into
mainfrom
fix/log-timestamp-timezone-fallback

Conversation

@MathurAditya724

Copy link
Copy Markdown
Member

Log timestamps (the [auth.login 6:28:59 AM] prefix) and other Date output were rendered in UTC instead of the user's local time. The CLI ships as Node SEA binaries that often can't resolve the OS timezone and silently fall back to UTC, so a user in US/Pacific saw timestamps hours off from their clock even with correct OS settings.

The fix detects the OS timezone at startup and sets process.env.TZ when the runtime fell back to UTC, and overrides consola's reporter formatDate to derive local time from getTimezoneOffset() in an unambiguous 24-hour format (robust even if ICU state is stale).

Testing

  • vitest run test/lib/timezone.test.ts test/lib/logger.test.ts test/lib/formatters — 975 passing (14 new timezone tests).
  • Manual: with TZ unset in a UTC-fallback runtime, verified the log prefix and toLocaleTimeString() render correct local time once repaired; confirmed initTimezone() is a no-op when TZ is set or the machine is genuinely UTC.
  • biome check clean on changed files; tsc clean on changed files (pre-existing generated-code errors unrelated).

The `[tag 6:28:59 AM]` log prefix (and every other Date display) is
rendered by the runtime's notion of "local" time, which comes from ICU.
The CLI ships as Node SEA binaries that frequently cannot resolve the OS
timezone and silently fall back to UTC, so users saw timestamps hours off
from their local clock even with correct OS settings.

- Add `src/lib/timezone.ts`: at startup, detect the OS timezone (from
  /etc/timezone, the /etc/localtime symlink, or `tzutil` on Windows) and
  set `process.env.TZ` when the runtime fell back to UTC but the OS clock
  is elsewhere. Falls back to a fixed `Etc/GMT±N` offset zone when no IANA
  name is available. No-op when TZ is already set or the runtime resolved
  a real zone.
- Override consola's reporter `formatDate` so the prefix derives local
  time from `getTimezoneOffset()` (robust against stale ICU state) and
  uses an unambiguous 24-hour `HH:MM:SS` format.
- Wire `initTimezone()` into `startCli()` before any Date is formatted.
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-29 09:43 UTC

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Codecov Results 📊

✅ Patch coverage is 98.25%. Project has 5469 uncovered lines.
✅ Project coverage is 81.67%. Comparing base (base) to head (head).

Files with missing lines (2)
File Patch % Lines
src/lib/logger.ts 100.00% ⚠️ 2 partials
src/cli.ts 0.00% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    81.64%    81.67%    +0.03%
==========================================
  Files          428       429        +1
  Lines        29780     29837       +57
  Branches     19540     19578       +38
==========================================
+ Hits         24313     24368       +55
- Misses        5467      5469        +2
- Partials      2029      2032        +3

Generated by Codecov Action

@MathurAditya724 MathurAditya724 added the jared Trigger the Jared agent to work on stuff label Jul 28, 2026

@jared-outpost jared-outpost 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.

took a pass through this — the approach is sound and CI is green. one doc nit:

formatLogTime JSDoc overstates ICU-independence. it says it derives local time "robust even if ICU state is stale" / "reflects the OS offset even in runtimes that cache a stale UTC formatter." but formatLogTime reads getTimezoneOffset(), which shares the exact same tz resolution as toLocaleTimeString() — in the broken UTC-fallback state getTimezoneOffset() returns 0 (that's the premise osReportsNonUtc() relies on), so formatLogTime would render UTC too. its correctness actually depends on initTimezone() having already set TZ so Node re-resolves the offset. worth rewording the comment so a future maintainer doesn't assume the function stands alone — the two fixes are coupled, not independent.

everything else checks out:

  • initTimezone() decision ladder (TZ-set → real-zone → genuine-UTC → repair) is right, and setting process.env.TZ at runtime does make Node re-resolve getTimezoneOffset() (verified).
  • Etc/GMT±N sign convention matches getTimezoneOffset() semantics (positive = west of UTC), and the offset-shift math in formatLogTime is correct.
  • reporter formatDate override preserves the rest of consola's formatting.
  • initTimezone() sits at the top of startCli(), the sole entry path, so nothing formats a Date before repair.

one heads-up, not a blocker: switching [tag 6:28:59 AM] → 24-hour [tag 06:28:59] is a format change beyond the timezone fix. it's disclosed in the description and the unambiguity is nice, just flagging it's a visible behavior change.

@BYK

BYK commented Jul 29, 2026

Copy link
Copy Markdown
Member

Jared, let's get the patch coverage above 80%. Also let's verify that the binpunch run we do in fossilize does not interfere with this?

Adds test/lib/timezone.mocked.test.ts mocking node:fs and
node:child_process to exercise detectOsTimezone's Unix (/etc/timezone,
/etc/localtime symlink) and Windows (tzutil) strategies, plus
initTimezone's detected-IANA and fixed-offset repair paths. Adds
runtimeTimezone catch/empty-zone cases to the existing file. Brings
src/lib/timezone.ts patch coverage to 100% (was 68.63%).
@jared-outpost

jared-outpost Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Done on both.

Patch coverage — the gap was src/lib/timezone.ts at 68.63% (the OS-detection strategies read real /etc/timezone, /etc/localtime, and tzutil state, which isn't deterministic in CI). Added test/lib/timezone.mocked.test.ts that mocks node:fs/node:child_process to drive both Unix strategies and the Windows tzutil path, plus initTimezone's detected-IANA and Etc/GMT±N fallback branches, and covered runtimeTimezone's catch/empty-zone cases. timezone.ts patch coverage is now 100% (f30ed56).

binpunch / fossilize — verified empirically: binpunch does not interfere. It zeroes localized display-name ICU resources (zone/ per-locale names, CJK dicts, .cnv converters) but keeps the root-level tz offset rules (zoneinfo64.res). Ran binpunch on a real Node 22 binary (zeroed 2660/4305 entries, 23.2 MB) and confirmed on the punched binary:

  • TZ=America/Los_AngelesgetTimezoneOffset() = 480, Intl resolves the zone, toLocaleTimeString() renders 6:28:59 AM.
  • TZ=Europe/Berlin (east of UTC) → offset −60, correct.
  • TZ=Etc/GMT+8 (our fixed-offset fallback) → correct.
  • Full initTimezone repair path (UTC fallback → detect OS zone → set TZ) resolves end-to-end on the punched binary.

The fix is doubly safe: formatLogTime derives wall-clock from getTimezoneOffset() alone (no ICU display formatting), and even the toLocaleString-based formatters recover once TZ is set, since the offset rules survive the hole-punch.

@BYK
BYK marked this pull request as ready for review July 29, 2026 09:34
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Jul 29, 2026
Comment thread src/lib/logger.ts
Comment thread src/lib/timezone.ts
Comment thread src/lib/timezone.ts
@BYK
BYK merged commit 11f9f4e into main Jul 29, 2026
33 checks passed
@BYK
BYK deleted the fix/log-timestamp-timezone-fallback branch July 29, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jared Trigger the Jared agent to work on stuff risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants