Add documentation about how to let Rust do logging and let Dart get those logs
Context:
https://github.com/fzyzcjy/flutter_rust_bridge/issues/443#issuecomment-1147261290 , where @thomas725 asks about this.
I can paste my own code that works in my production environment here.
Btw my code not only logs to Dart, but also logs to Android/iOS "standard" logging as well.
api.rs
pub struct LogEntry {
pub time_millis: i64,
pub level: i32,
pub tag: String,
pub msg: String,
}
pub fn create_log_stream(s: StreamSink<LogEntry>) -> Result<()> {
logger::SendToDartLogger::set_stream_sink(s);
Ok(())
}
pub fn rust_set_up() {
logger::init_logger();
}
logger.rs
use std::sync::Once;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use flutter_rust_bridge::StreamSink;
use lazy_static::lazy_static;
use log::{error, info, warn, Log, Metadata, Record};
use parking_lot::RwLock;
use simplelog::*;
use crate::frb::api::LogEntry;
static INIT_LOGGER_ONCE: Once = Once::new();
pub fn init_logger() {
// https://stackoverflow.com/questions/30177845/how-to-initialize-the-logger-for-integration-tests
INIT_LOGGER_ONCE.call_once(|| {
let level = if cfg!(debug_assertions) {
LevelFilter::Debug
} else {
LevelFilter::Warn
};
assert!(
level <= log::STATIC_MAX_LEVEL,
"Should respect log::STATIC_MAX_LEVEL={:?}, which is done in compile time. level{:?}",
log::STATIC_MAX_LEVEL,
level
);
CombinedLogger::init(vec![
Box::new(SendToDartLogger::new(level)),
Box::new(MyMobileLogger::new(level)),
// #[cfg(not(any(target_os = "android", target_os = "ios")))]
TermLogger::new(
level,
ConfigBuilder::new()
.set_time_format_str("%H:%M:%S%.3f")
.build(),
TerminalMode::Mixed,
ColorChoice::Auto,
),
])
.unwrap_or_else(|e| {
error!("init_logger (inside 'once') has error: {:?}", e);
});
info!("init_logger (inside 'once') finished");
warn!(
"init_logger finished, chosen level={:?} (deliberately output by warn level)",
level
);
});
}
lazy_static! {
static ref SEND_TO_DART_LOGGER_STREAM_SINK: RwLock<Option<StreamSink<LogEntry>>> =
RwLock::new(None);
}
pub struct SendToDartLogger {
level: LevelFilter,
}
impl SendToDartLogger {
pub fn set_stream_sink(stream_sink: StreamSink<LogEntry>) {
let mut guard = SEND_TO_DART_LOGGER_STREAM_SINK.write();
let overriding = guard.is_some();
*guard = Some(stream_sink);
drop(guard);
if overriding {
warn!(
"SendToDartLogger::set_stream_sink but already exist a sink, thus overriding. \
(This may or may not be a problem. It will happen normally if hot-reload Flutter app.)"
);
}
}
pub fn new(level: LevelFilter) -> Self {
SendToDartLogger { level }
}
fn record_to_entry(record: &Record) -> LogEntry {
let time_millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_millis() as i64;
let level = match record.level() {
Level::Trace => Self::LEVEL_TRACE,
Level::Debug => Self::LEVEL_DEBUG,
Level::Info => Self::LEVEL_INFO,
Level::Warn => Self::LEVEL_WARN,
Level::Error => Self::LEVEL_ERROR,
};
let tag = record.file().unwrap_or_else(|| record.target()).to_owned();
let msg = format!("{}", record.args());
LogEntry {
time_millis,
level,
tag,
msg,
}
}
const LEVEL_TRACE: i32 = 5000;
const LEVEL_DEBUG: i32 = 10000;
const LEVEL_INFO: i32 = 20000;
const LEVEL_WARN: i32 = 30000;
const LEVEL_ERROR: i32 = 40000;
}
impl Log for SendToDartLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
let entry = Self::record_to_entry(record);
if let Some(sink) = &*SEND_TO_DART_LOGGER_STREAM_SINK.read() {
sink.add(entry);
}
}
fn flush(&self) {
// no need
}
}
impl SharedLogger for SendToDartLogger {
fn level(&self) -> LevelFilter {
self.level
}
fn config(&self) -> Option<&Config> {
None
}
fn as_log(self: Box<Self>) -> Box<dyn Log> {
Box::new(*self)
}
}
pub struct MyMobileLogger {
level: LevelFilter,
#[cfg(target_os = "ios")]
ios_logger: oslog::OsLogger,
}
impl MyMobileLogger {
pub fn new(level: LevelFilter) -> Self {
MyMobileLogger {
level,
#[cfg(target_os = "ios")]
ios_logger: oslog::OsLogger::new("vision_utils_rs"),
}
}
}
impl Log for MyMobileLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
#[allow(unused_variables)]
fn log(&self, record: &Record) {
#[cfg(any(target_os = "android", target_os = "ios"))]
let modified_record = {
let override_level = Level::Info;
record.to_builder().level(override_level).build()
};
#[cfg(target_os = "android")]
android_logger::log(&modified_record);
#[cfg(target_os = "ios")]
self.ios_logger.log(&modified_record);
}
fn flush(&self) {
// no need
}
}
impl SharedLogger for MyMobileLogger {
fn level(&self) -> LevelFilter {
self.level
}
fn config(&self) -> Option<&Config> {
None
}
fn as_log(self: Box<Self>) -> Box<dyn Log> {
Box::new(*self)
}
}
mycode.dart, where things like Log.d is a pure-Dart logger that I wrote for my internal usage. You may use print etc.
class _VisionUtilsRsImplExtended extends VisionUtilsRsImpl
with
FlutterRustBridgeSetupMixin,
FlutterRustBridgeTimeoutMixin,
VisionUtilsFrbLogMixin,
VisionUtilsFrbErrorReportMixin {
static const _kTag = 'VisionUtilsRsImplExtended';
_VisionUtilsRsImplExtended._raw(super.inner) : super.raw() {
Log.d(_kTag, 'inside constructor, call setupMixinConstructor');
setupMixinConstructor();
}
@override
Future<void> setup() async {
final config = await visionUtilsConfig;
await rustSetUp(
release: config.release, pseudoSessId: config.pseudoSessId, hint: FlutterRustBridgeSetupMixin.kHintSetup);
createLogStream().listen((event) {
Log.instance.log(event.level, _kRustTagPrefix + event.tag, '${event.msg}(rust_time=${event.timeMillis})');
});
}
@override
Duration? get timeLimitForExecuteNormal {
return isInDebugMode ? const Duration(seconds: 30) : const Duration(seconds: 15);
}
@override
void log(String message) => Log.d(_kTag, message);
}
/cc @thomas725
Btw my code not only logs to Dart, but also logs to Android/iOS "standard" logging as well.
that's a lovely feature, thank you for sharing!
I found I could get rid of most errors by adding those 3 lines to my Cargo.toml file:
lazy_static = "1.4.0"
log = "0.4.17"
simplelog = "0.12.0"
Also since my flutter_rust_bridge project layout is copied from https://github.com/Desdaemon/flutter_rust_bridge_template I had to remove :frb: from use crate::frb::api::LogEntry;
I believe use parking_lot::RwLock; is referencing some private code of yours, but I guess replacing with use std::sync::RwLock; should give me the same result.
Now I got 3 errors left:
- no method named
set_time_format_strfound for structsimplelog::ConfigBuilderin the current scope (line 37) - no method named
is_somefound for enumResultin the current scope method not found inResult<RwLockWriteGuard<'_, Option<StreamSink<api::LogEntry>>>, PoisonError<RwLockWriteGuard<'_, Option<StreamSink<api::LogEntry>>>>>(line 67) - type
Result<RwLockWriteGuard<'_, Option<StreamSink<api::LogEntry>>>, PoisonError<RwLockWriteGuard<'_, Option<StreamSink<api::LogEntry>>>>>cannot be dereferenced (lines 69 & 125)
btw parking_lot is a public Rust library, find it using Google
no method named set_time_format_str found for struct simplelog::ConfigBuilder in the current scope (line 37)
That is a method in crate simplelog
btw parking_lot is a public Rust library, find it using Google
oh, thanks for the hint.. it's name sounds very domain specific ;)
That is a method in crate
simplelog
do I maybe need an older version? I've added simplelog = "0.12.0".
EDIT: But I guess I can simply remove the custom timestamp format and go with the default for now, let's first see if I can get everything else working.
Sure. Those are minor details and surely you can choose your own :)
I would have liked to have access to the logger level definitions in flutter, so I though I move them from logger.rs SendToDartLogger struct over into my api.rs LogEntry struct + adding pub keyword to each of them, but now the flutter_rust_bridge_codegen step fails with:
WARN lib_flutter_rust_bridge_codegen::commands] command="sh" "-c" "dart pub global run ffigen --config \"/tmp/.tmpdxyP7q\"" stdout=Running in Directory: '/'
Input Headers: [/tmp/.tmpNcb8i7.h]
stderr=Unhandled exception:
FileSystemException: Cannot create file, path = 'temp_for_macros.hpp' (OS Error: Permission denied, errno = 13)
Did I do something stupid or is support for pub const within a struct a not yet implemented feature of the flutter_rust_bridge?
"Permission denied" - sounds like a separate problem. Maybe create a new issue. And check do you have /tmp folder?
sounds like a separate problem. Maybe create a new issue.
your right, I did, see: https://github.com/fzyzcjy/flutter_rust_bridge/issues/494
And check do you have
/tmpfolder?
I do. Otherwise I guess flutter_rust_bridge_codegen wouldn't be able to complete successfully with that api.rs pub struct impl block with pub consts commented out, since it always writes /tmp/*.h files.
How do I use the logger.rs code you posted above on the rust side, after the stream has been setup by the dart side?
See how rust do standard logging. use log::info; info!("wow");
Oh! Nice! Thank you!
To get it to compile for android, I had to remove the modified_record in lines 173 to 177 and use the original record instead, because:
error[E0599]: no method named `to_builder` found for reference `&Record<'_>` in the current scope
Are you using a different version of the log crate then the latest = 0.4.17? Or are you using a different rust compiler? Mine is 1.60.0 (7737e0b5c 2022-04-04).
You are welcome! log 0.4.14 in my case, but anyway just modify code accordingly
well that's strange, even if I specify version 0.4.14 in my Cargo.toml file, delete Cargo.lock and run cargo update in my rust crate's folder, it regenerates a Cargo.lock that again uses the log crate version 0.4.17
EDIT: oh, it seems I misunderstood what cargo update was supposed to do... https://stackoverflow.com/questions/65677998/how-can-i-find-the-latest-stable-version-of-a-crate-using-cargo - but cargo build produces the same Cargo.lock file..
EDIT2: I found here: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
that exact versions can be specified like that: log = "=0.4.14". But that doesn't change the fact my rust compiler doesn't believe that the type Record has the method to_builder().
Just as it doesn't believe that the type ConfigBuilder (from the crate simplelog version 0.12.0) has the method set_time_format_str(&str) (line 37 of the file logger.rs you posted).
I would like to add something to this thread that might be useful for relative beginners, like me. My IDE kept complaining about the following code snipped, because IntoDart is not implemented for LogEntry, which is true.
pub struct LogEntry {
pub time_millis: i64,
pub level: i32,
pub tag: String,
pub msg: String,
}
pub fn create_log_stream(s: StreamSink<LogEntry>) -> anyhow::Result<()> {
Ok(())
}
Don't try implementing this yourself, because flutter_rust_bridge will generate it for you. It does sound kind of obvious now that I'm writing this down, but it took me an hour to figure this out... 😅
Don't try implementing this yourself, because flutter_rust_bridge will generate it for you. It does sound kind of obvious now that I'm writing this down, but it took me an hour to figure this out...
Feel free to add some doc at proper places and make a PR :)
@thomas725 That is a rust-language problem unrelated to my package indeed :) Try asking on StackOverflow, the logging package, etc
Feel free to add some doc at proper places and make a PR :)
Done :-) (#507)
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
I briefly update the doc to point to this issue, since this issue contains all code already.
https://github.com/fzyzcjy/flutter_rust_bridge/blob/master/book/src/feature/stream.md
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new issue.