如何实现从Rust向JavaScript异步流式传输log crate日志?
实现Rust log crate异步流式日志到JavaScript
问题背景
需要为Rust的log crate实现日志器,支持异步/流式将日志消息推送到JavaScript,让用户在操作执行过程中实时查看日志,而非操作结束后被动拉取。当前实现依赖JS主动请求日志,无法满足实时展示需求。
现有代码
static LOGGER: Logger = Logger::new(); #[wasm_bindgen] pub struct Logger { logs: Mutex<Vec<String>>, level: Mutex<LogLevel>, } impl Logger { pub fn init_once() { static INIT: Once = Once::new(); INIT.call_once(|| { log::set_logger(&LOGGER).unwrap(); log::set_max_level(log::LevelFilter::max()); }) } } #[wasm_bindgen] impl Logger { pub fn get_logs() -> Vec<String> { let logs = LOGGER .logs .lock() .expect("failed to acquire a lock to logger while getting logs"); logs.clone() } }
技术障碍
尝试为Logger添加add_log_listener/remove_log_listener方法时,发现js_sys::Function本质是原始指针,未实现Send trait,但log::Log trait强制要求实现Send,导致无法直接将JS回调存入日志器。
额外约束
- 无API形式限制,只要能实现异步实时展示日志即可。
- 日志通过Monaco Editor展示,无法直接追加HTML元素,需保证线程安全,且日志逻辑与展示逻辑分离。
- 排除JS
setInterval轮询方案:该方案不够优雅,且多调用方调用get_logs()/clear_logs()时会出现数据一致性问题。
解决方案
方案1:基于通道(Channel)的异步推送
核心思路是用Rust异步通道传递日志消息,日志器作为生产者推送日志,同时暴露JS可调用的异步方法,让JS端持续消费通道消息。
Rust端实现
use crossbeam_channel::{unbounded, Receiver, Sender}; use log::{Log, Metadata, Record}; use once_cell::sync::Lazy; use wasm_bindgen::prelude::*; use std::sync::{Mutex, Once}; use wasm_bindgen_futures::JsFuture; use web_sys::window; // 全局异步通道,用于传递日志消息 static CHANNEL: Lazy<(Sender<String>, Receiver<String>)> = Lazy::new(unbounded); #[wasm_bindgen] pub struct Logger { level: Mutex<log::LevelFilter>, } impl Logger { pub fn new() -> Self { Self { level: Mutex::new(log::LevelFilter::max()), } } pub fn init_once() { static INIT: Once = Once::new(); INIT.call_once(|| { log::set_logger(&LOGGER).unwrap(); log::set_max_level(log::LevelFilter::max()); }) } } // 实现log::Log trait,将日志推送到通道 impl Log for Logger { fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= *self.level.lock().unwrap() } fn log(&self, record: &Record) { if self.enabled(record.metadata()) { let log_msg = format!("[{}] {}", record.level(), record.args()); // 忽略发送失败(比如接收方已关闭) let _ = CHANNEL.0.send(log_msg); } } fn flush(&self) {} } #[wasm_bindgen] impl Logger { /// 暴露给JS的异步方法,持续接收日志并调用回调 pub async fn listen_logs(&self, callback: js_sys::Function) { let receiver = CHANNEL.1.clone(); loop { // 非阻塞轮询通道,避免阻塞JS事件循环 JsFuture::from(js_sys::Promise::new(|res, _| { let receiver_clone = receiver.clone(); let closure = Closure::wrap(Box::new(move || { if let Ok(msg) = receiver_clone.try_recv() { let _ = res.call1(&JsValue::NULL, &msg.into()); } }) as Box<dyn Fn()>); // 每100ms轮询一次,可根据需求调整间隔 window().unwrap().set_interval_with_callback_and_timeout_and_arguments_0( &closure, 100, ).unwrap(); closure.forget(); })).await.unwrap(); } } }
JS端调用示例
import { Logger } from './your_wasm_module'; Logger.init_once(); const logger = new Logger(); // 实时接收日志并更新Monaco Editor logger.listenLogs((msg) => { const editor = monaco.editor.getEditors()[0]; const currentContent = editor.getValue(); editor.setValue(`${currentContent}\n${msg}`); }).catch(err => console.error('日志监听失败:', err));
方案2:基于浏览器自定义事件的推送
利用浏览器事件系统,日志器产生日志时触发自定义事件,JS端监听该事件即可实时接收日志,更贴合浏览器生态。
Rust端实现
use log::{Log, Metadata, Record}; use once_cell::sync::Lazy; use wasm_bindgen::prelude::*; use web_sys::{Event, EventTarget}; use std::sync::{Mutex, Once}; static LOGGER: Logger = Logger::new(); // 全局事件目标,用于触发日志事件 static EVENT_TARGET: Lazy<EventTarget> = Lazy::new(EventTarget::new); #[wasm_bindgen] pub struct Logger { level: Mutex<log::LevelFilter>, } impl Logger { pub fn new() -> Self { Self { level: Mutex::new(log::LevelFilter::max()), } } pub fn init_once() { static INIT: Once = Once::new(); INIT.call_once(|| { log::set_logger(&LOGGER).unwrap(); log::set_max_level(log::LevelFilter::max()); }) } } impl Log for Logger { fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= *self.level.lock().unwrap() } fn log(&self, record: &Record) { if self.enabled(record.metadata()) { let log_msg = format!("[{}] {}", record.level(), record.args()); // 创建自定义事件并携带日志消息 let mut event = Event::new("log").unwrap(); event.set_data(&log_msg.into()); // 触发事件 let _ = EVENT_TARGET.dispatch_event(&event); } } fn flush(&self) {} } #[wasm_bindgen] impl Logger { /// 暴露给JS的方法,获取事件目标用于监听 pub fn get_event_target() -> EventTarget { EVENT_TARGET.clone() } }
JS端调用示例
import { Logger } from './your_wasm_module'; Logger.init_once(); const logEventTarget = Logger.getEventTarget(); // 监听自定义日志事件 logEventTarget.addEventListener('log', (e) => { const msg = e.data; const editor = monaco.editor.getEditors()[0]; editor.setValue(`${editor.getValue()}\n${msg}`); });
方案对比
- 通道方案:灵活性高,支持批量处理、日志过滤等复杂逻辑;但依赖轮询,存在轻微延迟(可通过调整轮询间隔优化)。
- 自定义事件方案:贴合浏览器生态,代码简洁,延迟更低;但仅适用于浏览器环境,无法兼容非浏览器WASM运行场景。
内容的提问来源于stack exchange,提问作者Elias
相关产品推荐
相关产品推荐

