Skip to content

feat(logger): Publish the logger as @sentry/logger - #1773

Merged
BYK merged 5 commits into
mainfrom
feat/cli-logger-package
Nov 1, 2026
Merged

BYK merged 5 commits into
mainfrom
feat/cli-logger-package

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1772.

Moves the logger from #1771 out of the CLI into packages/logger, published to npm as @sentry/logger: a small structured logger for Node.js and Bun with no dependencies.

Design

  • Each log call becomes a LogRecord (date, args, type, level, tag, plus attributes) and fans out to the logger's reporters. Records keep consola's shape, so consola-style reporters still read them.
  • createLogger({ level, reporters }) returns a root logger. withTag() and withAttributes() return scoped loggers that share the root's level and reporter list.
  • Reporters: fancyReporter (consola's fancy layout, the default), jsonReporter (NDJSON), logfmtReporter, and sentryReporter(Sentry.logger). The Sentry reporter takes the SDK's logger as an argument and is typed structurally, so the package has no @sentry/* dependency, optional or otherwise.
  • The core uses only node:util and node:path. About 5 KB minified, 2.5 KB gzipped, with every reporter.
  • The README explains why we wrote another logger, with the sizes of the alternatives measured in perf(cli): Replace consola with a small in-house logger #1771.

CLI changes

  • Prompts are no longer part of the logger. They live in packages/cli/src/lib/prompt.ts (same @clack/prompts lazy import and SENTRY_CLI_COMPLETION_BUILD guard), and call sites use prompt() instead of log.prompt(). Tests mock the prompt module instead of a fake logger's prompt.
  • The CLI configures fancyReporter({ env: getEnv }) and adds sentryReporter(Sentry.logger) after Sentry.init().
  • Telemetry change: CLI logs in Sentry now carry logger.tag / logger.type and sentry.origin: auto.log.sentry_logger instead of Sentry.createConsolaReporter()'s consola.* attributes. I found nothing in this repo that queries the old names. Saved queries or dashboards that do will need updating.

Packaging and release

  • In the workspace, exports point at src/*.ts, so the CLI bundle, vitest, and tsc need no build step. publishConfig.exports points the tarball at the tsdown ESM/CJS dist/.
  • logger craft workspace (tag prefix logger@, release branch release/logger/*) and a logger option in the Release workflow. It has no preReleaseCommand: Craft v3 bumps package.json in the workspace directory through the npm target. Post-merge jobs pack the tarball into the commit's artifact.
  • The Logger Package workflow runs test/package/check.sh: it packs the package, installs it into the consumer project in test/package/consumer/, then typechecks and runs it as ESM and as CommonJS.
  • MIT license. Version 1.0.0-dev.0, so the first Release run publishes 1.0.0 whichever bump it uses.
  • The Release workflow still works out the version from package.json in a script, because Craft applies bump types to the latest tag (0.0.0 for a new workspace). Filed Bump types ignore the workspace's manifest version craft#946.

--

View Session in Sentry

Move the logger from packages/cli/src/lib/logger.ts into a new
packages/cli-logger workspace package, published to npm as
@sentry/cli-logger. createLogger() takes the CLI-specific pieces as
options: env (the CLI passes getEnv for library mode) and prompt (the
CLI keeps its completion-build guard around clackPrompt). formatLogTime
moves into the package as the default formatLocalTime.

The CLI's logger.ts keeps its exports, so callers and test mocks do not
change. Releases go through the Release workflow's new cli-logger
product.

Fixes #1772

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Nov 1, 2026 3:45pm UTC

Request Review

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3b25814. Configure here.

Comment thread .craft.yml Resolved
Comment thread .github/workflows/release.yml Resolved
Comment thread docs/README.md Resolved
Comment thread packages/cli-logger/src/index.ts Resolved
Comment thread packages/cli-logger/src/prompt.ts Resolved
Comment thread packages/cli-logger/LICENSE.md Resolved
Comment thread packages/cli-logger/package.json Resolved
Comment thread packages/cli-logger/README.md Resolved
Address review feedback on the package:

- Rename @sentry/cli-logger to @sentry/logger (packages/logger), since
  nothing in it is CLI-specific. Start at 1.0.0-dev.0 under MIT.
- Restructure around structured records: each call becomes a LogRecord
  (level, type, tag, args, attributes) that fans out to reporters.
  Ship fancyReporter (consola-style), jsonReporter, logfmtReporter, and
  sentryReporter, which takes the SDK's `logger` so the package has no
  Sentry dependency. Add withAttributes() for structured fields.
- Move prompts out of the package into the CLI (src/lib/prompt.ts), so
  the logger has no dependencies at all.
- Drop the logger's preReleaseCommand: Craft v3 bumps package.json in
  the workspace directory through the npm target.
- Keep the packed-consumer check as real files under
  test/package/consumer, run by test/package/check.sh.
- Explain in the README why we wrote another logger.

The CLI now forwards logs with sentryReporter(Sentry.logger), so its
Sentry log attributes change from consola.* to logger.tag/logger.type.
@sentry-junior sentry-junior Bot changed the title feat(cli-logger): Publish the CLI logger as @sentry/cli-logger feat(logger): Publish the logger as @sentry/logger Nov 1, 2026
Comment thread .craft.yml Outdated
Comment on lines +106 to +117
# No preReleaseCommand: Craft bumps package.json through the npm target.
packages/logger:
changelog:
policy: auto
releaseBranchPrefix: release/logger
targets:
- name: github
tagPrefix: "logger@"
includeNames: '/^sentry-logger-\d.*\.tgz$/'
- name: npm
id: "@sentry/logger"
includeNames: '/^sentry-logger-\d.*\.tgz$/'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Logger Craft workspace never bumps package.json on release

packages/logger has no preReleaseCommand, so Craft prepare leaves package.json at 1.0.0-dev.0 while tagging the release version; packed/published artifacts stay on the dev version and later bumps recompute the same target.

Evidence
  • Release prepare passes an explicit new version into Craft (release.yml → getsentry/craft@v3), but versioning only lands in the repo if preReleaseCommand writes it.
  • packages/cli and packages/mcp-server both bump via preReleaseCommand (npm version "$CRAFT_NEW_VERSION" / bump script); mcp even bumps packages/mcp-server itself that way.
  • The new logger workspace comment claims the npm target bumps package.json, but that target only publishes includeNames: '/^sentry-logger-\d.*\.tgz$/' artifacts built earlier by pnpm pack in merge-jobs from whatever version is already in packages/logger/package.json.
  • Starting version is 1.0.0-dev.0, so without a prepare-time bump the release branch, tarball, and npm publish remain on the dev version while the GitHub tag can still be logger@<new>.

Identified by Warden · code-review · 924-ZLC

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not a bug. Without a preReleaseCommand, Craft v3's runPreReleaseCommand (src/commands/prepare.ts) runs automatic version bumps from the workspace's targets when minVersion >= 2.21.0 (here 2.33.0). The npm target's bumpVersion runs npm version <new> --no-git-tag-version in the workspace directory (getReleaseUnitDir(), getsentry/craft#936).

I checked it with Craft 3.5.0 (the v3 tag), using this .craft.yml and packages/logger/package.json. With workspace packages/logger selected, the resolved config has no preReleaseCommand, targets [github, npm], and root dir packages/logger. runPreReleaseCommand returned true, and package.json went from 1.0.0-dev.0 to 1.0.0.

The bump is committed to release/logger/<version>. That push triggers the post-merge pnpm pack, so the tarball gets the release version.

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great work. Let's also measure the performance/overhead of the library and make sure there's close to zero overhead.

Comment thread .github/workflows/logger-package.yml Resolved
Comment thread packages/logger/src/reporters/logfmt.ts Resolved
Comment thread .craft.yml Resolved
- Drop the separate Logger Package workflow. The package declares
  `sentryCi.e2e: test:package`, so the Test workflow's project matrix
  packs it and runs the clean consumer check whenever the logger or a
  dependent changes.
- Use Craft's {{workspaceName}} and {{packageSimpleName}} placeholders
  for the logger workspace's branch prefix, tag prefix, and tarball
  names. Craft 3.5 resolves them to the same values as before.
- Follow logfmt's key rules: only spaces, `=`, `"`, and control
  characters are replaced, so dotted keys like `http.status_code` pass
  through unchanged.
Measure the logger against the cheapest code that does the same job, and
cut the costs the benchmark found.

- Add `pnpm --filter @sentry/logger run bench`, which times each case
  in its own Node process, and record its results in the README.
- Format each record's ISO timestamp once per second instead of calling
  `toISOString()` per line.
- Build the structured fields as an object, so the JSON reporter
  stringifies it directly; keep a `__proto__` attribute as a field.
- Measure visible width with a loop instead of an ANSI regex replace,
  and skip the markup regexes when a line has no delimiters.
- Test logfmt keys before replacing characters in them.

Output is byte-identical to the previous build across about 24,600
formatted records.
@sentry-junior

sentry-junior Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

@BYK I measured it and trimmed the hot paths in 1dd94a8. The logger is not literally zero overhead, but the parts it controls are now close to the cost of the work itself.

pnpm --filter @sentry/logger run bench (packages/logger/bench/run.mjs) times each case against the cheapest code that does the same job. Each case runs in its own Node process, so JIT state from one case cannot affect another. Medians from one run on Node 22, linux-x64:

Case Baseline Logger
Call below the level (debug at info) 11 ns (no-op call) 1 ns
Call that reaches a no-op reporter 11 ns (no-op call) 104 ns
Fancy line in CI 70 ns (util.format + write) 316 ns (was 451)
Fancy line with colors and the time 70 ns (util.format + write) 1.7 µs (was 2.4)
JSON line 1.5 µs (toISOString + JSON.stringify + write) 1.0 µs (was 2.9)
logfmt line 1.5 µs (same) 1.7 µs (was 2.8)
Sentry structured log 78 ns (util.format + Sentry.logger.info) 314 ns
Startup 28.0 ms (import node:util, node:path) 28.3 ms
  • A call below the level costs nothing measurable. It times at about 1 ns, less than a bare no-op call; most likely V8 inlines the level check.
  • An enabled call costs about 100 ns. That is the record object and the Date.
  • Formatting is now in the same range as the baseline. JSON is faster than the naive toISOString() + JSON.stringify version because each ISO timestamp is formatted once per second.
  • A colored line costs more because it does more. It renders the local time, adds ANSI codes, and right-aligns the line. For comparison, one real process.stderr.write to a file takes about 1.2 µs on the same machine.
  • Startup adds about 0.3 ms on top of the Node built-ins the logger imports, which the CLI loads anyway.

The fixes:

  • Format the ISO timestamp once per second.
  • Stringify the field object directly.
  • Measure ANSI width with a loop instead of a regex replace. That was the largest cost on colored lines.
  • Skip the markup regexes when a line has no delimiters.
  • Test logfmt keys before replacing characters in them.

Output from the new build is byte-identical to the previous one across about 24,600 records: every reporter, layout, env combination, and edge-case date. I added tests for the timestamp cache (millisecond padding, second boundaries, negative epochs, year 20000) and for a __proto__ attribute. The numbers above are also in the README under Performance.

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Keep addressing my comments but I think this is mostly good to go now.

Attribute values can now be objects. The logfmt and Sentry reporters
expand them into dotted keys (`http.status_code=404`); the JSON reporter
keeps them nested. Expansion stops five levels down and writes
`[Object]` there, which caps the cost and stops reference cycles.

Flat attributes cost the same as before: a nested attribute adds about
200 ns to a logfmt line, and the Sentry reporter got slightly faster by
dropping `Object.entries`. The benchmark gains a nested-attribute case.
@BYK
BYK enabled auto-merge (squash) November 1, 2026 15:49
@BYK
BYK merged commit 2b5c206 into main Nov 1, 2026
39 checks passed
@BYK
BYK deleted the feat/cli-logger-package branch November 1, 2026 15:59
Comment thread .craft.yml
Comment on lines +110 to +116
releaseBranchPrefix: "release/{{workspaceName}}"
targets:
- name: github
tagPrefix: "{{workspaceName}}@"
includeNames: '/^{{packageSimpleName}}-\d.*\.tgz$/'
- name: npm
includeNames: '/^{{packageSimpleName}}-\d.*\.tgz$/'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Logger Craft config leaves release placeholders unresolved

The logger workspace still uses literal {{workspaceName}} / {{packageSimpleName}} values, so release branches, tags, and artifact filters will not match release/logger/** or the real sentry-logger-*.tgz pack name and the release cannot publish.

Evidence
  • Sibling workspaces use concrete values (release/cli, release/mcp, cli@, /^sentry-mcp-server-\d.*\.tgz$/), while logger keeps mustache-style placeholders in releaseBranchPrefix, tagPrefix, and both includeNames.
  • .github/workflows/merge-jobs.yml only builds release artifacts for release/logger/**, which does not match a branch prefix of release/{{workspaceName}} or release/packages/logger.
  • packages/logger/test/package/check.sh packs @sentry/logger as sentry-logger-*.tgz, so /^{{packageSimpleName}}-\d.*\.tgz$/ (or even /^logger-\d.*\.tgz$/) will not select the uploaded artifact.

Identified by Warden · code-review · 4C3-EPT

Comment on lines +34 to +36
} else if (error.cause) {
cause = `${stack ? `\n\n${" ".repeat(depth + 1)}` : " "}[cause]: ${formatWithOptions(INSPECT_OPTIONS, error.cause)}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Falsy Error.cause values are silently dropped

else if (error.cause) skips valid falsy causes (0, "", false, null), so they never appear in the formatted message despite the function's contract to render causes.

Evidence
  • formatError only appends a non-ErrorLike cause when error.cause is truthy (else if (error.cause)).
  • Error accepts any cause, including 0, "", false, and null; Node's own console/util.inspect print those as [cause]: ….
  • Missing cause is undefined, so the correct presence check is error.cause !== undefined (or 'cause' in error), not truthiness.
  • Result: formatMessage([new Error("x", { cause: 0 })], { stack: false }) yields "x" instead of "x [cause]: 0".

Identified by Warden · code-review · 79U-429

Comment on lines +56 to +63
export function formatMessage(
args: readonly unknown[],
{ stack }: { stack: boolean },
): string {
return formatWithOptions(
INSPECT_OPTIONS,
...args.map((arg) => (isErrorLike(arg) ? formatError(arg, 0, stack) : arg)),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Error-as-first-arg is treated as a util.format string

Pre-stringifying Errors before formatWithOptions makes an Error in args[0] act as a printf format string, so %s/%d in the error message consume later arguments and corrupt the log line.

Evidence
  • File contract: format "the way console.log would" / with "util.format semantics".
  • formatMessage maps ErrorLike args through formatError to plain strings, then spreads them into formatWithOptions.
  • util.format(error, extra) does not treat Error as a format string; util.format("failed %s\n…", extra) does.
  • Trigger: formatMessage([new Error("failed %s"), "payload"], { stack: false }) returns "failed payload" instead of keeping %s and appending payload.

Identified by Warden · code-review · PKW-NHK

sentry-junior Bot added a commit that referenced this pull request Nov 1, 2026
Brings in #1773 (@sentry/logger) and #1759 (flag completion kinds).

Conflicts:
- .craft.yml, merge-jobs.yml, release.yml: keep main's logger release
  workspace, branch, packaging, and product; keep the removal of the
  separate mcp release (the CLI release publishes @sentry/mcp-server).
- packages/cli/package.json: depend on @sentry/logger and
  @sentry/mcp-core, not the retired @sentry/mcp-server workspace.
- pnpm-lock.yaml: regenerated with pnpm install.

Follow-ups for the merged tree:
- `sentry mcp` string flags parse with parseText, as #1759's lint rule
  requires.
- completion-target.ts now imports commands/local/loopback.ts, so allow
  that leaf module in the mcp-server bundle's CLI-only input check.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 1, 2026
Checks for a single package should run from the `test.yml` per-project
matrix, set up through `package.json#sentryCi`. Nothing pointed
contributors or agents there, so #1773 first added a standalone
`logger-package.yml`, copying the existing `mcp-server-package.yml`.

- **Moves the `@sentry/mcp-server` clean-consumer typecheck into the
matrix.** It's now `sentryCi.e2e: "test:package"`, laid out like
`@sentry/logger`'s check: `test/package/check.sh` plus a `consumer/`
fixture with the same `index.ts` the workflow used to write inline.
`mcp-server-package.yml` is removed. The matrix already selects the
package when it or one of its workspace dependencies (`mcp-core`,
`mcp-server-mocks`, the tsconfig package) changes, and for root changes
and pushes to `main`. The check now also sits behind the required `test`
job; before, "Clean Consumer Typecheck" was not a required check. One
small difference: matrix E2E steps run on Node 24, while the old
workflow ran on Node 22.
- **Documents `sentryCi`.** `docs/operations/github-actions.md` gets a
"Package CI checks" section: the default script for each field, the
override fields, an example, and when a separate workflow is still
justified. `AGENTS.md` and `docs/contributing/quality-checks.md` add a
short rule that links to it.
- **Adds a guard.** `scripts/workflow-policy.test.mjs`, run by
`test:ci-projects` in the quality job, fails when a workflow triggers on
`pull_request`, `pull_request_target`, or `merge_group` and isn't on its
list with a reason. The failure message points to `sentryCi`. The
existing PR workflows are on the list: eval, install scripts, Local
multi-OS E2E, PR risk, smoke tests, and token cost.

Fixes #1777

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3Ac828b56c95c28d1bcb6f29fa -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3Ac828b56c95c28d1bcb6f29fa/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 1, 2026
This fixes the Warden findings that were posted on #1773 after it
merged.

- **Falsy causes:** `formatError` showed a cause only when it was
truthy, so `new Error("x", { cause: 0 })` printed just `x`. It now shows
the cause whenever the error has one (`0`, `""`, `false`, `null`,
`undefined`), as `util.inspect` does.
- **An error as the first argument:** `formatMessage` turned errors into
strings before calling `util.format`, so an error passed first became
the format string. `logger.info(new Error("Failed for %s"), "extra")`
printed `Failed for extra`. The `%s` now stays literal and the remaining
arguments are added after it, as `console.log` does.
- **Craft `minVersion`:** raised from 2.33.0 to 3.5.0. The logger
workspace uses `{{workspaceName}}` and `{{packageSimpleName}}`, and
Craft only resolves those from 3.5.0 onward. The release workflow's
`getsentry/craft@v3` already installs 3.5.0. Warden said the
placeholders are left unresolved, which is wrong for the release
workflow, but an older local Craft would have left them as literal text
without any error. This raises the minimum for every workspace in this
repository.

Tests cover each falsy cause and an error message containing `%s`. For
other output, the build is byte-identical to `main` across about 24,600
formatted records.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3A052a28796a4c37b4b8b5f916 -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A052a28796a4c37b4b8b5f916/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>

This branch was previously deployed

1 inactive deployment
Preview — d6135284 Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish the CLI logger as a standalone package

1 participant