flutter_rust_bridge icon indicating copy to clipboard operation
flutter_rust_bridge copied to clipboard

Add documentation about how to let Rust do logging and let Dart get those logs

Open fzyzcjy opened this issue 3 years ago • 19 comments

Context:

https://github.com/fzyzcjy/flutter_rust_bridge/issues/443#issuecomment-1147261290 , where @thomas725 asks about this.

fzyzcjy avatar Jun 06 '22 09:06 fzyzcjy

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);
}

fzyzcjy avatar Jun 06 '22 09:06 fzyzcjy

/cc @thomas725

fzyzcjy avatar Jun 06 '22 09:06 fzyzcjy

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_str found for struct simplelog::ConfigBuilder in the current scope (line 37)
  • no method named is_some found for enum Result in the current scope method not found in Result<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)

thomas725 avatar Jun 06 '22 10:06 thomas725

btw parking_lot is a public Rust library, find it using Google

fzyzcjy avatar Jun 06 '22 10:06 fzyzcjy

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

fzyzcjy avatar Jun 06 '22 10:06 fzyzcjy

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.

thomas725 avatar Jun 06 '22 10:06 thomas725

Sure. Those are minor details and surely you can choose your own :)

fzyzcjy avatar Jun 06 '22 10:06 fzyzcjy

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?

thomas725 avatar Jun 06 '22 21:06 thomas725

"Permission denied" - sounds like a separate problem. Maybe create a new issue. And check do you have /tmp folder?

fzyzcjy avatar Jun 07 '22 00:06 fzyzcjy

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 /tmp folder?

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.

thomas725 avatar Jun 07 '22 05:06 thomas725

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?

thomas725 avatar Jun 08 '22 21:06 thomas725

See how rust do standard logging. use log::info; info!("wow");

fzyzcjy avatar Jun 08 '22 23:06 fzyzcjy

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).

thomas725 avatar Jun 09 '22 07:06 thomas725

You are welcome! log 0.4.14 in my case, but anyway just modify code accordingly

fzyzcjy avatar Jun 09 '22 07:06 fzyzcjy

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).

thomas725 avatar Jun 09 '22 10:06 thomas725

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... 😅

w-ensink avatar Jun 10 '22 16:06 w-ensink

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 :)

fzyzcjy avatar Jun 10 '22 23:06 fzyzcjy

@thomas725 That is a rust-language problem unrelated to my package indeed :) Try asking on StackOverflow, the logging package, etc

fzyzcjy avatar Jun 10 '22 23:06 fzyzcjy

Feel free to add some doc at proper places and make a PR :)

Done :-) (#507)

w-ensink avatar Jun 11 '22 17:06 w-ensink

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.

stale[bot] avatar Aug 11 '22 04:08 stale[bot]

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

fzyzcjy avatar Aug 11 '22 05:08 fzyzcjy

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.

github-actions[bot] avatar Aug 25 '22 05:08 github-actions[bot]