Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
fzyzcjy opened this issue Jun 6, 2022 · 22 comments
Closed
Labels
wontfix This will not be worked on

Comments

@fzyzcjy
Copy link
Owner

fzyzcjy commented Jun 6, 2022

Context:

#443 (comment) , where @thomas725 asks about this.

@fzyzcjy
Copy link
Owner Author

fzyzcjy commented Jun 6, 2022

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
Copy link
Owner Author

fzyzcjy commented Jun 6, 2022

/cc @thomas725

@thomas725
Copy link
Contributor

thomas725 commented Jun 6, 2022

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)

@fzyzcjy
Copy link
Owner Author

fzyzcjy commented Jun 6, 2022

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

@fzyzcjy
Copy link
Owner Author

fzyzcjy commented Jun 6, 2022

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

@thomas725
Copy link
Contributor

thomas725 commented Jun 6, 2022

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.

@fzyzcjy
Copy link
Owner Author

fzyzcjy commented Jun 6, 2022

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

@thomas725
Copy link
Contributor

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?

@fzyzcjy
Copy link
Owner Author

fzyzcjy commented Jun 7, 2022

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

@thomas725
Copy link
Contributor

sounds like a separate problem. Maybe create a new issue.

your right, I did, see: #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
Copy link
Contributor

thomas725 commented Jun 8, 2022

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?

@fzyzcjy
Copy link
Owner Author

fzyzcjy commented Jun 8, 2022

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

@thomas725
Copy link
Contributor

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

@fzyzcjy
Copy link
Owner Author

fzyzcjy commented Jun 9, 2022

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

@thomas725
Copy link
Contributor

thomas725 commented Jun 9, 2022

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

@w-ensink
Copy link
Contributor

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

@fzyzcjy
Copy link
Owner Author

fzyzcjy commented Jun 10, 2022

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
Copy link
Owner Author

fzyzcjy commented Jun 10, 2022

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

@w-ensink
Copy link
Contributor

w-ensink commented Jun 11, 2022

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

Done :-) (#507)

@stale
Copy link

stale bot commented Aug 11, 2022

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 stale bot added the wontfix This will not be worked on label Aug 11, 2022
@fzyzcjy
Copy link
Owner Author

fzyzcjy commented Aug 11, 2022

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 fzyzcjy closed this as completed Aug 11, 2022
@github-actions
Copy link
Contributor

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 github-actions bot locked as resolved and limited conversation to collaborators Aug 25, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

3 participants