Skip to content

feat: Add serverpod_logging_cli bridge package#124

Open
nielsenko wants to merge 5 commits into
serverpod:mainfrom
nielsenko:feat-serverpod-cli-logger
Open

feat: Add serverpod_logging_cli bridge package#124
nielsenko wants to merge 5 commits into
serverpod:mainfrom
nielsenko:feat-serverpod-cli-logger

Conversation

@nielsenko
Copy link
Copy Markdown
Contributor

@nielsenko nielsenko commented Jun 5, 2026

New serverpod_logging_cli bridge package.

Includes:

  • ServerpodCliLogger (a cli_tools.Logger built on Log/LogWriter)
  • StdOutLogWriter (a LogWriter build on cli_Tools.StdOutLogger),
  • plus the isolate-backed IsolatedLogWriter

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

Too many files changed? Review this PR in Change Stack to see how the pieces fit before you dive in.

Review Change Stack

Warning

Review limit reached

@nielsenko, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 27 minutes and 21 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 41368724-908c-4004-b36b-e53e67c51eba

📥 Commits

Reviewing files that changed from the base of the PR and between cfdb63e and c223e7b.

📒 Files selected for processing (21)
  • .github/workflows/ci.yml
  • .github/workflows/publish-serverpod_logging_cli.yaml
  • packages/serverpod_logging/CHANGELOG.md
  • packages/serverpod_logging/lib/serverpod_logging.dart
  • packages/serverpod_logging/lib/src/isolated_log_writer.dart
  • packages/serverpod_logging/test/isolated_log_writer_test.dart
  • packages/serverpod_logging/test/log_dispatch_test.dart
  • packages/serverpod_logging/test/log_scope_test.dart
  • packages/serverpod_logging/test/progress_nesting_test.dart
  • packages/serverpod_logging/test/progress_relabel_test.dart
  • packages/serverpod_logging/test/progress_sync_runner_test.dart
  • packages/serverpod_logging_cli/CHANGELOG.md
  • packages/serverpod_logging_cli/LICENSE
  • packages/serverpod_logging_cli/README.md
  • packages/serverpod_logging_cli/analysis_options.yaml
  • packages/serverpod_logging_cli/lib/serverpod_logging_cli.dart
  • packages/serverpod_logging_cli/lib/src/serverpod_cli_logger.dart
  • packages/serverpod_logging_cli/lib/src/std_out_log_writer.dart
  • packages/serverpod_logging_cli/pubspec.yaml
  • packages/serverpod_logging_cli/test/serverpod_cli_logger_test.dart
  • pubspec.yaml
📝 Walkthrough

Walkthrough

This PR introduces two new Dart packages—isolated_object and serverpod_logging_cli—and extends serverpod_logging with isolated execution support. The core addition is IsolatedObject<T>, a utility that spawns a child isolate to run an object and forwards method calls via an asynchronous request/response protocol. IsolatedLogWriter and ServerpodCliLogger layer on top, enabling off-isolate logging and CLI framework integration. Multiple logging tests and a bug fix to MultiLogWriter.add complete the set.

Changes

Isolated Object Package

Layer / File(s) Summary
Package structure and documentation
packages/isolated_object/pubspec.yaml, CHANGELOG.md, LICENSE, analysis_options.yaml, README.md, lib/isolated_object.dart
Package metadata, linting configuration, documentation, and library entrypoint for the new isolated_object package.
IsolatedObject core implementation
packages/isolated_object/lib/src/isolated_object.dart
Generic IsolatedObject<T> class implementing isolate spawning, port wiring, request ID assignment, in-flight request completion, and idempotent shutdown with pending request failure recovery.
IsolatedObject test suite
packages/isolated_object/test/close_test.dart, create_test.dart, evaluate_test.dart, lifecycle_test.dart
Comprehensive tests covering synchronous/asynchronous/throwing factories, sequential and concurrent evaluations, error propagation via RemoteError, state persistence, and graceful close behavior.

Logging Infrastructure Extensions

Layer / File(s) Summary
IsolatedLogWriter integration
packages/serverpod_logging/pubspec.yaml, CHANGELOG.md, lib/serverpod_logging.dart, lib/src/isolated_log_writer.dart, test/isolated_log_writer_test.dart
IsolatedLogWriter wraps any LogWriter and executes logging on a dedicated isolate; includes forwarding of scope lifecycle and draining in-flight writes on close.
MultiLogWriter fix and formatting
packages/serverpod_logging/lib/src/log_types.dart, lib/src/log.dart
Fixes MultiLogWriter.add to actually mutate _writers state; reformats LogScope.child, MultiLogWriter.closeScope, and convenience log methods.
Comprehensive logging test coverage
packages/serverpod_logging/test/log_dispatch_test.dart, log_scope_test.dart, multi_log_writer_test.dart, progress_nesting_test.dart, progress_relabel_test.dart, progress_sync_runner_test.dart
New tests verify log-level gating, runtime level changes, scoping with metadata, multi-writer fan-out, nested progress scopes, and synchronous runner support.
serverpod_logging_cli package structure
packages/serverpod_logging_cli/pubspec.yaml, CHANGELOG.md, LICENSE, analysis_options.yaml, README.md, lib/serverpod_logging_cli.dart
Package metadata, linting configuration, documentation, and library entrypoint for the CLI logging bridge.
ServerpodCliLogger implementation
packages/serverpod_logging_cli/lib/src/serverpod_cli_logger.dart
ServerpodCliLogger extends cli.Logger and bridges to serverpod_logging via a LogWriter; implements progress tracking with nested scope management and suppression when log level is set to nothing.
StdOutLogWriter implementation
packages/serverpod_logging_cli/lib/src/std_out_log_writer.dart
StdOutLogWriter extends TextLogWriter and routes entries to cli.StdOutLogger, mapping log levels and extracting CLI log types from metadata.
ServerpodCliLogger test suite
packages/serverpod_logging_cli/test/serverpod_cli_logger_test.dart
Tests cover scope lifecycle, progress/progressStream verdict handling, multi-event nesting, log-level gating, metadata attachment, and error stack trace preservation.

Workspace and CI Automation

Layer / File(s) Summary
Workspace membership
pubspec.yaml
Root workspace pubspec.yaml updated to include isolated_object and serverpod_logging_cli packages.
CI workflow matrix updates
.github/workflows/ci.yml
Restructured dart_format and dart_analyze_test job matrices to explicitly list packages and adjust Dart version coverage.
Package publish workflows
.github/workflows/publish-isolated_object.yaml, .github/workflows/publish-serverpod_logging_cli.yaml
New tag-triggered publish workflows for isolated_object and serverpod_logging_cli packages.
Gitignore update
.gitignore
Added pubspec.lock to ignore rules with a note that CI regenerates the root lockfile.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • serverpod/cli_tools#109: Both PRs modify GitHub Actions CI workflow Dart testing matrix with focus on Dart 3.6 and cli_tools coverage.
  • serverpod/cli_tools#115: Both PRs modify packages/serverpod_logging/lib/src/log_types.dart, specifically fixing the MultiLogWriter.add behavior to actually update _writers.
  • serverpod/cli_tools#24: The new StdOutLogWriter in the main PR depends on cli.StdOutLogger's logToStderrLevelThreshold parameter that was added in the referenced PR.

Suggested reviewers

  • christerswahn
  • SandPod
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: Add serverpod_logging_cli bridge package' accurately summarizes the main change—adding a new CLI bridge package that connects serverpod_logging with cli_tools, which is the primary objective of this changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@nielsenko nielsenko marked this pull request as ready for review June 5, 2026 18:44
@nielsenko nielsenko force-pushed the feat-serverpod-cli-logger branch 6 times, most recently from cfdb63e to 980eb75 Compare June 5, 2026 19:44
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (8)
packages/serverpod_logging/test/isolated_log_writer_test.dart (2)

72-72: ⚡ Quick win

Test name should include the "when" clause.

Per coding guidelines, test names should follow the "when … then" pattern. This test describes the outcome but omits the action/trigger.

Suggested naming improvement
test(
  'when openScope and closeScope are called, then they are forwarded',
  () async {

As per coding guidelines, use the Given-when-then pattern: start test names with "when" to describe the action, followed by "then" for the expected outcome.

🤖 Prompt for 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.

In `@packages/serverpod_logging/test/isolated_log_writer_test.dart` at line 72,
Rename the test named 'then openScope and closeScope are forwarded' in
isolated_log_writer_test.dart to follow the Given-when-then pattern; update the
test() call's description string to something like 'when openScope and
closeScope are called, then they are forwarded' so the name includes the "when"
clause and clearly describes the action and expected outcome for the test that
asserts forwarding of openScope/closeScope.

55-56: ⚡ Quick win

Test name should include the "when" clause.

Per coding guidelines, test names should follow the "when … then" pattern. This test describes the outcome ("then log is forwarded") but omits the action/trigger ("when log is called").

Suggested naming improvement
test(
  'when log is called, then it is forwarded to the writer running in the isolate',
  () async {

As per coding guidelines, use the Given-when-then pattern: start test names with "when" to describe the action, followed by "then" for the expected outcome.

🤖 Prompt for 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.

In `@packages/serverpod_logging/test/isolated_log_writer_test.dart` around lines
55 - 56, Rename the test title in isolated_log_writer_test.dart for the test
invocation currently starting with 'then log is forwarded to the writer running
in the isolate' to follow the Given-when-then pattern by adding the "when"
clause (e.g., change to "when log is called, then it is forwarded to the writer
running in the isolate"); update the string passed to the test(...) call so the
test name begins with "when" and retains the same assertion logic in the test
body.
packages/serverpod_logging/lib/src/isolated_log_writer.dart (1)

25-41: ⚡ Quick win

Consider adding test coverage for stackTrace parameter.

The closeScope implementation forwards the optional stackTrace parameter across the isolate boundary. While the class documentation correctly warns that "payloads must be sendable" (line 8), the current test suite doesn't exercise the case where a stackTrace is provided. Adding a test case would increase confidence that typical StackTrace implementations (e.g., from StackTrace.current) are reliably sendable.

🧪 Example test case to add
test('when closeScope includes a stackTrace, then it is forwarded', () async {
  final writer = IsolatedLogWriter(() => TestLogWriter());
  final scope = LogScope.root('op');
  final st = StackTrace.current;

  await writer.openScope(scope);
  await writer.closeScope(
    scope,
    success: false,
    duration: Duration.zero,
    error: StateError('test'),
    stackTrace: st,
  );

  // Verify the wrapped writer received the stackTrace
  final closedScopes = await writer.evaluate(
    (w) => (w as TestLogWriter).closedScopes,
  );
  expect(closedScopes, hasLength(1));

  await writer.close();
});
🤖 Prompt for 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.

In `@packages/serverpod_logging/lib/src/isolated_log_writer.dart` around lines 25
- 41, Add a unit test that constructs an IsolatedLogWriter (e.g.,
IsolatedLogWriter(() => TestLogWriter())), opens a LogScope
(LogScope.root('op')), captures a StackTrace (StackTrace.current), calls
closeScope(scope, success: false, duration: Duration.zero, error:
StateError('test'), stackTrace: st) and then uses writer.evaluate((w) => (w as
TestLogWriter).closedScopes) to assert the wrapped TestLogWriter received one
closed scope containing the same stackTrace; this ensures closeScope (the method
that forwards error and stackTrace across the isolate via evaluate/closeScope)
properly transmits StackTrace values.
packages/isolated_object/test/close_test.dart (1)

11-21: 💤 Low value

Test name "then it handles it gracefully" is vague.

The guideline states: "Describe behaviour, not implementation, in test names." The test verifies two specific behaviors: (1) calling close() multiple times does not throw, and (2) isClosed is true. Consider a more precise description such as "then it is idempotent and isClosed remains true" or "then subsequent close calls do not throw".

As per coding guidelines: "Describe behaviour, not implementation, in test names (e.g., 'then progress is marked failed' not 'then fail() is called')."

🤖 Prompt for 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.

In `@packages/isolated_object/test/close_test.dart` around lines 11 - 21, Rename
the vague test description to explicitly state the expected behaviors being
verified: that calling IsolatedObject.close() multiple times does not throw and
that IsolatedObject.isClosed is true; update the test's name string (the
test(...) invocation in close_test.dart) to something like "then subsequent
close calls do not throw and isClosed remains true" or split into two focused
tests named "then subsequent close calls do not throw" and "then isClosed
becomes true" so the behavior (idempotence of close and isClosed state) is
clearly described.
packages/isolated_object/test/lifecycle_test.dart (2)

8-16: 💤 Low value

Clarify test name to describe the full state transition.

The test name 'then isClosed is false until close is called' only describes the initial state and omits the transition. The test verifies both pre-close and post-close state with two expect calls, which represents a single behavioral contract (the state transition).

Consider renaming to describe the complete behavior:

📝 Suggested test name
   test(
-      'Given an open IsolatedObject, '
-      'then isClosed is false until close is called', () async {
+      'Given an IsolatedObject, '
+      'when close is called, '
+      'then isClosed transitions from false to true', () async {
     final isolated = IsolatedObject<_Counter>(() => _Counter(0));

As per coding guidelines: "Describe behaviour, not implementation, in test names" and test names should clearly express the when–then relationship.

🤖 Prompt for 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.

In `@packages/isolated_object/test/lifecycle_test.dart` around lines 8 - 16,
Rename the test to describe the full when–then state transition: update the test
declaration that uses test(...) which constructs an IsolatedObject<_Counter> and
asserts isolated.isClosed before and after calling isolated.close(), to a name
that expresses both pre-close and post-close behavior (e.g., "when opened,
isClosed is false and becomes true after close") so the test describes the
complete behavior rather than only the initial state.

7-44: ⚡ Quick win

Organize tests with top-level Given groups.

The coding guidelines require organizing tests with top-level group('Given …') blocks for preconditions and shared setup. Adding a group would improve consistency with the project's test conventions.

📋 Suggested structure
 void main() {
+  group('Given IsolatedObject lifecycle', () {
     test(
         'Given an open IsolatedObject, '
         'then isClosed is false until close is called', () async {
       // ... existing test body
     });

     test(
         'Given a closed IsolatedObject, '
         'when evaluate is called, '
         'then it throws a StateError', () async {
       // ... existing test body
     });

     test(
         'Given keepIsolateAlive: false, '
         'when the object is used, '
         'then it still evaluates correctly', () async {
       // ... existing test body
     });
+  });
 }

As per coding guidelines: "Organize tests with top-level group('Given …') for preconditions and shared setup".

🤖 Prompt for 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.

In `@packages/isolated_object/test/lifecycle_test.dart` around lines 7 - 44, Wrap
the three standalone tests in a top-level group with a "Given …" description to
follow the project's test convention; create a group('Given an IsolatedObject')
{ ... } that contains the tests that reference IsolatedObject<_Counter>, keeping
the existing test bodies (including calls to IsolatedObject, evaluate, close,
and keepIsolateAlive) unchanged and only moving them into the group to
centralize preconditions and shared setup for these lifecycle tests.
packages/isolated_object/test/evaluate_test.dart (1)

9-243: ⚡ Quick win

Rename test groups to follow Given–when–then pattern.

The coding guidelines require top-level groups to start with 'Given …' to describe preconditions. The current groups ('evaluate', 'error handling', etc.) describe features rather than preconditions.

📋 Suggested group names
- group('evaluate', () {
+ group('Given an IsolatedObject with initial state', () {
    // ... existing tests
  });

- group('error handling', () {
+ group('Given an IsolatedObject that encounters errors', () {
    // ... existing tests
  });

- group('concurrent operations', () {
+ group('Given an IsolatedObject with concurrent evaluations', () {
    // ... existing tests
  });

- group('complex objects', () {
+ group('Given an IsolatedObject with complex nested state', () {
    // ... existing tests
  });

As per coding guidelines: "Organize tests with top-level group('Given …') for preconditions and shared setup" and "Start Given group names with 'Given …'".

🤖 Prompt for 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.

In `@packages/isolated_object/test/evaluate_test.dart` around lines 9 - 243,
Rename the top-level test group titles to follow the Given–when–then pattern by
changing the four group(...) declarations currently named 'evaluate', 'error
handling', 'concurrent operations', and 'complex objects' to start with "Given
…" and describe the precondition (e.g. "Given an IsolatedObject with initial
state" for evaluate, "Given an IsolatedObject when errors occur" for error
handling, "Given an IsolatedObject with concurrent requests" for concurrent
operations, and "Given an IsolatedObject with complex state" for complex
objects) so the tests using setUp/tearDown and functions like
IsolatedObject<_Counter>() and IsolatedObject<_ComplexObject>() clearly reflect
the shared preconditions.
packages/isolated_object/test/create_test.dart (1)

7-48: ⚡ Quick win

Organize tests with top-level Given groups.

The coding guidelines require organizing tests with top-level group('Given …') blocks for preconditions and shared setup. While these tests have different preconditions, grouping them would improve structure and make it easier to add shared setup later.

📋 Suggested structure
 void main() {
+  group('Given IsolatedObject creation', () {
     test(
         'Given a synchronous factory, '
         'when IsolatedObject is created, '
         'then it successfully initializes', () async {
       // ... existing test body
     });

     test(
         'Given an async factory, '
         'when IsolatedObject is created, '
         'then it successfully initializes', () async {
       // ... existing test body
     });

     test(
         'Given a factory that throws, '
         'when IsolatedObject is created, '
         'then subsequent operations fail gracefully', () async {
       // ... existing test body
     });
+  });
 }

As per coding guidelines: "Organize tests with top-level group('Given …') for preconditions and shared setup".

🤖 Prompt for 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.

In `@packages/isolated_object/test/create_test.dart` around lines 7 - 48, Wrap the
individual tests inside top-level group('Given …') blocks to reflect
preconditions and allow shared setup/teardown: modify main() to create three
groups (e.g., group('Given a synchronous factory', ...), group('Given an async
factory', ...), group('Given a factory that throws', ...)) and move the
corresponding test(...) calls into their respective group bodies, keeping the
test names and assertions unchanged; if any shared initialization/cleanup for a
group is needed later, add setUp/setUpAll or tearDown/tearDownAll inside the
group where IsolatedObject instances are created or closed.
🤖 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 `@packages/isolated_object/lib/src/isolated_object.dart`:
- Around line 122-135: evaluate has a TOCTOU race: after awaiting _connected it
may still add a completer to inflight even if close() set _isClosed and cleared
inflight; to fix, re-check _isClosed immediately after awaiting _connected
(before touching inflight, incrementing _nextId or calling toChild.send) and
throw the same StateError if closed so no completer is registered or message
sent; update the evaluate(T) method to perform this second check (use the
existing _isClosed and _connected variables and keep inflight and completer
handling unchanged otherwise).

In `@packages/serverpod_logging_cli/test/serverpod_cli_logger_test.dart`:
- Line 166: Rename the test group named "log level handling" to follow the
Given-when-then pattern by starting with "when" (e.g., "when handling log
levels" or "when adjusting log levels") so it matches the nested groups like
"when calling progress" and "when calling progressStream"; update the group(...)
label where it appears in serverpod_cli_logger_test.dart to use the new
lowercase "when …" phrasing.

---

Nitpick comments:
In `@packages/isolated_object/test/close_test.dart`:
- Around line 11-21: Rename the vague test description to explicitly state the
expected behaviors being verified: that calling IsolatedObject.close() multiple
times does not throw and that IsolatedObject.isClosed is true; update the test's
name string (the test(...) invocation in close_test.dart) to something like
"then subsequent close calls do not throw and isClosed remains true" or split
into two focused tests named "then subsequent close calls do not throw" and
"then isClosed becomes true" so the behavior (idempotence of close and isClosed
state) is clearly described.

In `@packages/isolated_object/test/create_test.dart`:
- Around line 7-48: Wrap the individual tests inside top-level group('Given …')
blocks to reflect preconditions and allow shared setup/teardown: modify main()
to create three groups (e.g., group('Given a synchronous factory', ...),
group('Given an async factory', ...), group('Given a factory that throws', ...))
and move the corresponding test(...) calls into their respective group bodies,
keeping the test names and assertions unchanged; if any shared
initialization/cleanup for a group is needed later, add setUp/setUpAll or
tearDown/tearDownAll inside the group where IsolatedObject instances are created
or closed.

In `@packages/isolated_object/test/evaluate_test.dart`:
- Around line 9-243: Rename the top-level test group titles to follow the
Given–when–then pattern by changing the four group(...) declarations currently
named 'evaluate', 'error handling', 'concurrent operations', and 'complex
objects' to start with "Given …" and describe the precondition (e.g. "Given an
IsolatedObject with initial state" for evaluate, "Given an IsolatedObject when
errors occur" for error handling, "Given an IsolatedObject with concurrent
requests" for concurrent operations, and "Given an IsolatedObject with complex
state" for complex objects) so the tests using setUp/tearDown and functions like
IsolatedObject<_Counter>() and IsolatedObject<_ComplexObject>() clearly reflect
the shared preconditions.

In `@packages/isolated_object/test/lifecycle_test.dart`:
- Around line 8-16: Rename the test to describe the full when–then state
transition: update the test declaration that uses test(...) which constructs an
IsolatedObject<_Counter> and asserts isolated.isClosed before and after calling
isolated.close(), to a name that expresses both pre-close and post-close
behavior (e.g., "when opened, isClosed is false and becomes true after close")
so the test describes the complete behavior rather than only the initial state.
- Around line 7-44: Wrap the three standalone tests in a top-level group with a
"Given …" description to follow the project's test convention; create a
group('Given an IsolatedObject') { ... } that contains the tests that reference
IsolatedObject<_Counter>, keeping the existing test bodies (including calls to
IsolatedObject, evaluate, close, and keepIsolateAlive) unchanged and only moving
them into the group to centralize preconditions and shared setup for these
lifecycle tests.

In `@packages/serverpod_logging/lib/src/isolated_log_writer.dart`:
- Around line 25-41: Add a unit test that constructs an IsolatedLogWriter (e.g.,
IsolatedLogWriter(() => TestLogWriter())), opens a LogScope
(LogScope.root('op')), captures a StackTrace (StackTrace.current), calls
closeScope(scope, success: false, duration: Duration.zero, error:
StateError('test'), stackTrace: st) and then uses writer.evaluate((w) => (w as
TestLogWriter).closedScopes) to assert the wrapped TestLogWriter received one
closed scope containing the same stackTrace; this ensures closeScope (the method
that forwards error and stackTrace across the isolate via evaluate/closeScope)
properly transmits StackTrace values.

In `@packages/serverpod_logging/test/isolated_log_writer_test.dart`:
- Line 72: Rename the test named 'then openScope and closeScope are forwarded'
in isolated_log_writer_test.dart to follow the Given-when-then pattern; update
the test() call's description string to something like 'when openScope and
closeScope are called, then they are forwarded' so the name includes the "when"
clause and clearly describes the action and expected outcome for the test that
asserts forwarding of openScope/closeScope.
- Around line 55-56: Rename the test title in isolated_log_writer_test.dart for
the test invocation currently starting with 'then log is forwarded to the writer
running in the isolate' to follow the Given-when-then pattern by adding the
"when" clause (e.g., change to "when log is called, then it is forwarded to the
writer running in the isolate"); update the string passed to the test(...) call
so the test name begins with "when" and retains the same assertion logic in the
test body.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7232dcda-8495-43d9-bc50-5e7443eb945e

📥 Commits

Reviewing files that changed from the base of the PR and between 048465e and cfdb63e.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • .github/workflows/ci.yml
  • .github/workflows/publish-isolated_object.yaml
  • .github/workflows/publish-serverpod_logging_cli.yaml
  • .gitignore
  • packages/isolated_object/CHANGELOG.md
  • packages/isolated_object/LICENSE
  • packages/isolated_object/README.md
  • packages/isolated_object/analysis_options.yaml
  • packages/isolated_object/lib/isolated_object.dart
  • packages/isolated_object/lib/src/isolated_object.dart
  • packages/isolated_object/pubspec.yaml
  • packages/isolated_object/test/close_test.dart
  • packages/isolated_object/test/create_test.dart
  • packages/isolated_object/test/evaluate_test.dart
  • packages/isolated_object/test/lifecycle_test.dart
  • packages/serverpod_logging/CHANGELOG.md
  • packages/serverpod_logging/lib/serverpod_logging.dart
  • packages/serverpod_logging/lib/src/isolated_log_writer.dart
  • packages/serverpod_logging/lib/src/log.dart
  • packages/serverpod_logging/lib/src/log_types.dart
  • packages/serverpod_logging/pubspec.yaml
  • packages/serverpod_logging/test/isolated_log_writer_test.dart
  • packages/serverpod_logging/test/log_dispatch_test.dart
  • packages/serverpod_logging/test/log_progress_test.dart
  • packages/serverpod_logging/test/log_scope_test.dart
  • packages/serverpod_logging/test/multi_log_writer_test.dart
  • packages/serverpod_logging/test/progress_nesting_test.dart
  • packages/serverpod_logging/test/progress_relabel_test.dart
  • packages/serverpod_logging/test/progress_sync_runner_test.dart
  • packages/serverpod_logging_cli/CHANGELOG.md
  • packages/serverpod_logging_cli/LICENSE
  • packages/serverpod_logging_cli/README.md
  • packages/serverpod_logging_cli/analysis_options.yaml
  • packages/serverpod_logging_cli/lib/serverpod_logging_cli.dart
  • packages/serverpod_logging_cli/lib/src/serverpod_cli_logger.dart
  • packages/serverpod_logging_cli/lib/src/std_out_log_writer.dart
  • packages/serverpod_logging_cli/pubspec.yaml
  • packages/serverpod_logging_cli/test/serverpod_cli_logger_test.dart
  • pubspec.yaml

Comment on lines +122 to +135
Future<U> evaluate<U>(FutureOr<U> Function(T) function) async {
if (_isClosed) {
throw StateError('IsolatedObject<$T> is closed');
}

final (toChild, _, inflight) = await _connected;

final id = _nextId++;
final completer = Completer<U>();
inflight[id] = completer;

toChild.send((id: id, function: function));
return completer.future;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

TOCTOU race: evaluate may add to inflight after close clears it.

There is a time-of-check-time-of-use race between Line 123 (_isClosed check) and Line 131 (inflight assignment). If close() runs after evaluate() checks _isClosed but before evaluate() resumes from await _connected, the following sequence occurs:

  1. evaluate() checks _isClosed (false), then suspends at await _connected.
  2. close() sets _isClosed = true, awaits _connected, and clears inflight.
  3. evaluate() resumes and adds its completer to the now-empty inflight map, then sends the action.

The response may arrive with no registered completer (triggering the assertion at Line 62 in debug mode, or silently returning in release). This violates fail-fast semantics—evaluate() should throw immediately when the object is closed.

🔒 Proposed fix: re-check _isClosed after await
 Future<U> evaluate<U>(FutureOr<U> Function(T) function) async {
   if (_isClosed) {
     throw StateError('IsolatedObject<$T> is closed');
   }

   final (toChild, _, inflight) = await _connected;

+  if (_isClosed) {
+    throw StateError('IsolatedObject<$T> is closed');
+  }
+
   final id = _nextId++;
   final completer = Completer<U>();
   inflight[id] = completer;

   toChild.send((id: id, function: function));
   return completer.future;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<U> evaluate<U>(FutureOr<U> Function(T) function) async {
if (_isClosed) {
throw StateError('IsolatedObject<$T> is closed');
}
final (toChild, _, inflight) = await _connected;
final id = _nextId++;
final completer = Completer<U>();
inflight[id] = completer;
toChild.send((id: id, function: function));
return completer.future;
}
Future<U> evaluate<U>(FutureOr<U> Function(T) function) async {
if (_isClosed) {
throw StateError('IsolatedObject<$T> is closed');
}
final (toChild, _, inflight) = await _connected;
if (_isClosed) {
throw StateError('IsolatedObject<$T> is closed');
}
final id = _nextId++;
final completer = Completer<U>();
inflight[id] = completer;
toChild.send((id: id, function: function));
return completer.future;
}
🤖 Prompt for 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.

In `@packages/isolated_object/lib/src/isolated_object.dart` around lines 122 -
135, evaluate has a TOCTOU race: after awaiting _connected it may still add a
completer to inflight even if close() set _isClosed and cleared inflight; to
fix, re-check _isClosed immediately after awaiting _connected (before touching
inflight, incrementing _nextId or calling toChild.send) and throw the same
StateError if closed so no completer is registered or message sent; update the
evaluate(T) method to perform this second check (use the existing _isClosed and
_connected variables and keep inflight and completer handling unchanged
otherwise).

Comment thread packages/serverpod_logging_cli/test/serverpod_cli_logger_test.dart Outdated
@nielsenko nielsenko force-pushed the feat-serverpod-cli-logger branch from 980eb75 to e035dff Compare June 5, 2026 19:53
@nielsenko nielsenko force-pushed the feat-serverpod-cli-logger branch from e035dff to 11d1ccb Compare June 5, 2026 21:15
nielsenko added 2 commits June 5, 2026 23:19
- progress_nesting_test: a log emitted inside a progress runner must be
  attributed to the progress scope via the Zone, and a nested progress
  must child under the outer scope.
- progress_relabel_test: progress must never surface the runner's result
  as a scope label (uses a generic recording writer, no terminal rendering).
These fail to compile if the runner is narrowed to Future<T> Function()
@nielsenko nielsenko force-pushed the feat-serverpod-cli-logger branch from 11d1ccb to ed9e6f2 Compare June 5, 2026 21:19
Copy link
Copy Markdown
Contributor

@marcelomendoncasoares marcelomendoncasoares left a comment

Choose a reason for hiding this comment

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

One nit on the YAML format and some suggestions for improvement on the tests.

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +18 to +25
package:
[
cli_tools,
config,
isolated_object,
serverpod_logging,
serverpod_logging_cli,
]
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.

Suggestion(NIT): Use the more idiomatic YAML format for arrays.

Suggested change
package:
[
cli_tools,
config,
isolated_object,
serverpod_logging,
serverpod_logging_cli,
]
package:
- cli_tools
- config
- isolated_object
- serverpod_logging
- serverpod_logging_cli

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.

addressed in c223e7b

Comment on lines +23 to +41
group('when calling progress (non-stream)', () {
test(
'then it opens and closes exactly one scope with the runner verdict',
() async {
final result = await logger.progress('op', () async => true);

expect(result, isTrue);
expect(writer.openedScopes.map((s) => s.label), ['op']);
expect(writer.closedScopes.single.success, isTrue);
},
);

test('then a false runner closes the scope unsuccessfully', () async {
final result = await logger.progress('op', () async => false);

expect(result, isFalse);
expect(writer.closedScopes.single.success, isFalse);
});
});
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.

Suggestion: Remove the group and move the "when" to each test description detailing the different parameter on each call.

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.

addressed in c223e7b

Comment on lines +43 to +52
group('when calling progressStream', () {
test(
'then each event becomes a sub-operation nested under the parent '
'(the "db shape": one parent span, one child span per event)',
() async {
final result = await logger.progressStream(
'Deploying',
Stream.fromIterable(['build', 'deploy']),
toMessage: (s) => s,
);
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.

Suggestion: Same for these tests. Each one of them call progressStream with different parameters, and this means that they represent different actions.

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.

addressed in c223e7b

Comment on lines +166 to +216
group('log level handling', () {
test('a log entry carries its cli LogType in metadata', () async {
logger.info('hello', type: cli.TextLogType.bullet);
await logger.flush();

final entry = writer.entries.single;
expect(entry.message, 'hello');
expect(entry.metadata?[logTypeKey], cli.TextLogType.bullet);
});

test(
'log/write with cli.LogLevel.nothing is suppressed, not an error',
() async {
logger.log('x', cli.LogLevel.nothing);
logger.write('y', cli.LogLevel.nothing);
await logger.flush();

expect(writer.entries, isEmpty);
},
);

test(
'an error log keeps the stack trace structured on the entry',
() async {
final st = StackTrace.current;
logger.error('bad', stackTrace: st);
await logger.flush();

final entry = writer.entries.single;
expect(entry.level, LogLevel.error);
expect(entry.message, 'bad', reason: 'message is not flattened');
expect(entry.stackTrace, same(st));
},
);

test(
'when silenced (cli.LogLevel.nothing) nothing is logged or scoped',
() async {
logger.logLevel = cli.LogLevel.nothing;

logger.info('dropped');
final result = await logger.progress('op', () async => true);
await logger.flush();

expect(result, isTrue, reason: 'runner still runs while silent');
expect(writer.entries, isEmpty);
expect(writer.openedScopes, isEmpty);
},
);
});
});
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.

Suggestion: These tests are out of the pattern.

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.

addressed in c223e7b

@nielsenko nielsenko force-pushed the feat-serverpod-cli-logger branch from ed9e6f2 to e8f5c21 Compare June 5, 2026 21:27
Framework-level tests (no CLI rendering):

- Level gating skips the entry factory below the threshold
- Runtime logLevel changes take effect; isDebugEnabled tracks it
- Writer errors are swallowed (dispatch is best-effort)
- Writes serialize in invocation order even when earlier ones are slower
- close() drops any further dispatches
- Convenience methods map to the right level; error() attaches error + stack trace
- Logs emitted outside any progress attach to the synthetic root scope
- progress passes metadata through to the opened scope
- LogScope parent/child: root has no parent; a child keeps its own id/label/metadata
@nielsenko nielsenko force-pushed the feat-serverpod-cli-logger branch from e8f5c21 to 94b1bbf Compare June 5, 2026 21:29
nielsenko added 2 commits June 5, 2026 23:36
A LogWriter that runs any wrapped writer on a dedicated isolate (via
IsolatedObject), so timer-driven spinners keep animating even when the
calling isolate is blocked.
A cli_tools.Logger implementation (ServerpodCliLogger) backed by the
serverpod_logging LogWriter architecture, plus StdOutLogWriter (a TextLogWriter
that renders LogType-aware output via cli_tools' StdOutLogger).
@nielsenko nielsenko force-pushed the feat-serverpod-cli-logger branch from 94b1bbf to c223e7b Compare June 5, 2026 21:48
@nielsenko nielsenko self-assigned this Jun 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants