You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现从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元素,需保证线程安全,且日志逻辑与展示逻辑分离。
  • 排除JSsetInterval轮询方案:该方案不够优雅,且多调用方调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 06:06:04