Add Rust-to-Dart logging bridge#3079
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a logging bridge that routes Rust log records to the Dart logging package, including a new enable_frb_logging! macro and a Dart-side FrbDartLogging utility. Feedback focuses on several critical areas: the generated Dart initialization code needs to await asynchronous calls to prevent type errors, and the Rust logger initialization must handle Flutter Hot Restarts gracefully to avoid panics. Additionally, improvements were suggested for adding error handling to the Dart stream subscription and adopting more idiomatic log level mappings between the two languages.
| let max_level = max_level_func | ||
| .map(|f| format!("api.{}()", f.name_dart_wire())) | ||
| .unwrap_or_else(|| "3".to_owned()); | ||
| let setup_default_output = setup_default_output_func | ||
| .map(|f| format!("api.{}()", f.name_dart_wire())) | ||
| .unwrap_or_else(|| "true".to_owned()); |
There was a problem hiding this comment.
The calls to api.frbLoggingMaxLevel() and api.frbLoggingSetupDartLoggingOutput() are likely asynchronous in the generated Dart code (as they are not explicitly marked with #[frb(sync)] in the Rust macro). Since these are used as arguments to api.frbInitLogger, they must be awaited to avoid passing a Future where a value is expected.
Note: await is safe to use here even if the functions were sync, as Dart allows awaiting non-future values.
| let max_level = max_level_func | |
| .map(|f| format!("api.{}()", f.name_dart_wire())) | |
| .unwrap_or_else(|| "3".to_owned()); | |
| let setup_default_output = setup_default_output_func | |
| .map(|f| format!("api.{}()", f.name_dart_wire())) | |
| .unwrap_or_else(|| "true".to_owned()); | |
| let max_level = max_level_func | |
| .map(|f| format!("await api.{}()", f.name_dart_wire())) | |
| .unwrap_or_else(|| "3".to_owned()); | |
| let setup_default_output = setup_default_output_func | |
| .map(|f| format!("await api.{}()", f.name_dart_wire())) | |
| .unwrap_or_else(|| "true".to_owned()); |
There was a problem hiding this comment.
I don't recall this - but can look into the code if needed to remember. I think making it #[frb(sync)] would be better - but if adding this attribute if hard, await works as well.
There was a problem hiding this comment.
Yes probably the agent does sth wrong (the code is still in a somehow early stage), and we should fix it
| log::set_boxed_logger(Box::new(FrbDartLogger { sink })) | ||
| .map(|()| log::set_max_level(max_level)) | ||
| .expect("FRB logging should only be initialized once"); |
There was a problem hiding this comment.
Using .expect() on log::set_boxed_logger will cause the application to crash if frb_init_logger is called more than once. This is a critical issue for Flutter's Hot Restart, where the Dart state is reset but the Rust process (and its global logger) persists. Subsequent calls to init will trigger this panic.
Consider handling the error gracefully. Ideally, the logger should be initialized only once (e.g., using std::sync::Once), and the StreamSink should be stored in a static RwLock so it can be updated on each call to frb_init_logger to support re-connection after a hot restart.
There was a problem hiding this comment.
I did this on purpose: My thinking is that nobody would call frb_init_logger a second time intentionally.
Thus, if done accidentally, it should crash rather than silently swallow log messages.
If there is a reason for a second intentional init, the suggested changes sounds fine.
| throw StateError('FRB logging should not be initialized twice'); | ||
| } | ||
|
|
||
| _subscription = rustLogStream.listen((record) { |
| case 'TRACE': | ||
| return Level.FINE; | ||
| case 'DEBUG': | ||
| return Level.CONFIG; |
There was a problem hiding this comment.
The mapping of TRACE to Level.FINE and DEBUG to Level.CONFIG is non-standard for the Dart logging package. Level.CONFIG is typically intended for static configuration messages, not general debug logs. A more idiomatic mapping would be DEBUG -> Level.FINE and TRACE -> Level.FINER or Level.FINEST.
| case 'TRACE': | |
| return Level.FINE; | |
| case 'DEBUG': | |
| return Level.CONFIG; | |
| case 'TRACE': | |
| return Level.FINER; | |
| case 'DEBUG': | |
| return Level.FINE; |
There was a problem hiding this comment.
This suggestion sounds good. I mapped it this way, as Level.CONFIG is between INFO and FINE. But semantics wise gemini could be right.
AFAIRecall, this is just the default mapping a user can override. So not as critical, but the defaults should feel right.
|
docs LGTM |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3079 +/- ##
===========================================
+ Coverage 33.47% 98.24% +64.76%
===========================================
Files 51 473 +422
Lines 1198 19546 +18348
===========================================
+ Hits 401 19202 +18801
+ Misses 797 344 -453 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@fzyzcjy that worked great :) I left some comments. Tell me if I can help further! |
continue from @patmuk #2766