use std::{
cell::RefCell,
collections::HashMap,
fmt,
fmt::Debug,
io,
rc::Rc,
sync::atomic::{AtomicU32, Ordering},
};
use tracing::{Event, Level, Metadata, field::Field, level_filters::LevelFilter};
use tracing_subscriber::{
fmt::{
FmtContext, FormatEvent, FormatFields, FormattedFields, MakeWriter, Subscriber,
format::{DefaultFields, Writer},
},
registry::LookupSpan,
};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
pub type JsLogger;
#[wasm_bindgen(method)]
pub fn debug(this: &JsLogger, data: &JsValue);
#[wasm_bindgen(method)]
pub fn info(this: &JsLogger, data: &JsValue);
#[wasm_bindgen(method)]
pub fn warn(this: &JsLogger, data: &JsValue);
#[wasm_bindgen(method)]
pub fn error(this: &JsLogger, data: &JsValue);
}
#[derive(Debug)]
pub struct MakeJsLogWriter {
logger_id: Option<u32>,
}
thread_local! {
static LOGGER_MAP: RefCell<HashMap<u32, Rc<JsLogger>>> = RefCell::new(HashMap::new());
}
static NEXT_LOGGER_ID: AtomicU32 = AtomicU32::new(0);
impl MakeJsLogWriter {
pub fn new() -> Self {
Self { logger_id: None }
}
pub fn new_with_logger(logger: JsLogger) -> Self {
let logger_id = NEXT_LOGGER_ID.fetch_add(1, Ordering::Relaxed);
let maker = Self { logger_id: Some(logger_id) };
LOGGER_MAP.with_borrow_mut(|m| m.insert(logger_id, Rc::new(logger)));
maker
}
fn make_writer_for_level(&self, level: Level) -> JsLogWriter {
let logger = self.logger_id.map(|logger_id| {
LOGGER_MAP.with_borrow(|m| m.get(&logger_id).expect("logger id not found").clone())
});
JsLogWriter { logger, level }
}
}
impl Drop for MakeJsLogWriter {
fn drop(&mut self) {
if let Some(logger_id) = self.logger_id {
LOGGER_MAP.with_borrow_mut(|m| m.remove(&logger_id));
}
}
}
impl Default for MakeJsLogWriter {
fn default() -> Self {
Self::new()
}
}
impl<'a> MakeWriter<'a> for MakeJsLogWriter {
type Writer = JsLogWriter;
fn make_writer(&'a self) -> JsLogWriter {
self.make_writer_for_level(Level::INFO)
}
fn make_writer_for(&'a self, meta: &Metadata<'_>) -> JsLogWriter {
self.make_writer_for_level(*meta.level())
}
}
pub struct JsLogWriter {
logger: Option<Rc<JsLogger>>,
level: Level,
}
impl Debug for JsLogWriter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JsLogWriter").field("level", &self.level).finish_non_exhaustive()
}
}
impl io::Write for JsLogWriter {
fn write(&mut self, data: &[u8]) -> Result<usize, io::Error> {
use std::str;
let message = str::from_utf8(data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
.trim_end();
let message = JsValue::from_str(message);
match &self.logger {
Some(logger) => write_message_to_logger(self.level, &message, logger.as_ref()),
None => write_message_to_console(self.level, &message),
}
Ok(data.len())
}
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
}
fn write_message_to_logger(level: Level, message: &JsValue, logger: &JsLogger) {
match level {
Level::TRACE => (),
Level::DEBUG => logger.debug(message),
Level::INFO => logger.info(message),
Level::WARN => logger.warn(message),
Level::ERROR => logger.error(message),
}
}
fn write_message_to_console(level: Level, message: &JsValue) {
match level {
Level::TRACE => (),
Level::DEBUG => web_sys::console::debug_1(message),
Level::INFO => web_sys::console::info_1(message),
Level::WARN => web_sys::console::warn_1(message),
Level::ERROR => web_sys::console::error_1(message),
}
}
#[derive(Debug, Default)]
pub struct JsEventFormatter {}
impl JsEventFormatter {
pub fn new() -> Self {
Self::default()
}
}
impl<C, N> FormatEvent<C, N> for JsEventFormatter
where
C: tracing::Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
ctx: &FmtContext<'_, C, N>,
mut writer: Writer<'_>,
event: &Event<'_>,
) -> fmt::Result {
let meta = event.metadata();
write!(writer, "{} {}: ", meta.level(), meta.target())?;
let mut v = FindMessageVisitor::default();
event.record(&mut v);
if let Some(m) = v.message {
writer.write_str(m.as_str())?
}
let mut v = JsFieldVisitor::new(writer.by_ref());
event.record(&mut v);
if let Some(file) = meta.file() {
write!(writer, "\n at {file}")?;
if let Some(line) = meta.line() {
write!(writer, ":{line}")?;
}
}
let span = event.parent().and_then(|id| ctx.span(id)).or_else(|| ctx.lookup_current());
let scope = span.into_iter().flat_map(|span| span.scope());
for span in scope {
let meta = span.metadata();
write!(writer, "\n in {}::{}", meta.target(), meta.name())?;
let ext = span.extensions();
let fields = &ext
.get::<FormattedFields<N>>()
.expect("Unable to find FormattedFields in extensions; this is a bug");
if !fields.is_empty() {
write!(writer, " with {fields}")?;
}
}
Ok(())
}
}
#[derive(Debug, Default)]
struct FindMessageVisitor {
message: Option<String>,
}
impl tracing::field::Visit for FindMessageVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
if field.name() == "message" {
self.message = Some(format!("{value:?}"));
}
}
}
struct JsFieldVisitor<'a> {
writer: Writer<'a>,
result: fmt::Result,
is_empty: bool,
}
impl<'a> JsFieldVisitor<'a> {
fn new(writer: Writer<'a>) -> Self {
Self { writer, result: Ok(()), is_empty: true }
}
fn pad_and_record(&mut self, name: &str, value: &dyn Debug) -> fmt::Result {
if self.is_empty {
self.is_empty = false;
write!(self.writer, "\n ")?;
} else {
write!(self.writer, " ")?;
}
write!(self.writer, "{name}={value:?}")
}
}
impl tracing::field::Visit for JsFieldVisitor<'_> {
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
if self.result.is_err() {
return;
}
let name = field.name();
if name == "message" {
return;
}
self.result = self.pad_and_record(name, value);
}
}
pub type JsLoggingSubscriber =
Subscriber<DefaultFields, JsEventFormatter, LevelFilter, MakeJsLogWriter>;
pub fn make_tracing_subscriber(logger: Option<JsLogger>) -> JsLoggingSubscriber {
let make_writer = match logger {
Some(logger) => MakeJsLogWriter::new_with_logger(logger),
None => MakeJsLogWriter::new(),
};
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.with_ansi(false)
.event_format(JsEventFormatter::new())
.with_writer(make_writer)
.finish()
}
#[cfg(test)]
pub(crate) mod tests {
use matrix_sdk_test_macros::async_test;
use tracing::{debug, subscriber::with_default};
use wasm_bindgen::{JsCast, JsValue};
use crate::js_tracing::make_tracing_subscriber;
#[async_test]
async fn test_make_tracing_subscriber() -> Result<(), js_sys::Error> {
let logger = js_sys::eval(
"({
debug_calls: [],
debug: function (...args) { this.debug_calls.push(args) },
})",
)?;
let subscriber = make_tracing_subscriber(Some(logger.clone().into()));
with_default(subscriber, || {
debug!(value = 1, "Test message");
});
let debug_calls: js_sys::Array =
js_sys::Reflect::get(&logger, &JsValue::from_str("debug_calls"))?
.dyn_into()
.expect("debug_calls not an array");
assert_eq!(debug_calls.length(), 1, "Expected 1 call, got {}", debug_calls.length());
let call_args: js_sys::Array =
debug_calls.get(0).dyn_into().expect("call_args not an array");
assert_eq!(call_args.length(), 1, "Expected 1 argument, got {}", call_args.length());
let message_string = call_args.get(0).as_string().unwrap();
let expected_prefix =
"DEBUG matrix_sdk_common::js_tracing::tests: Test message\n value=1\n";
assert!(
message_string.starts_with(expected_prefix),
"Expected log message to start with '{}', but was '{}'",
expected_prefix,
message_string
);
Ok(())
}
}