use napi::bindgen_prelude::{self, JsObjectValue};
use napi::threadsafe_function::ThreadsafeFunctionCallMode;
use tracing::{Level, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::fmt::{FormatFields, FormattedFields};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::registry::LookupSpan;
struct VscodeLayer<F>
where
F: for<'writer> FormatFields<'writer> + 'static,
{
threadsafe_function: napi::threadsafe_function::ThreadsafeFunction<
(&'static str, String),
(),
(&'static str, String),
napi::Status,
false,
false,
0,
>,
field_formatter: F,
}
impl<F> VscodeLayer<F>
where
F: for<'writer> FormatFields<'writer> + 'static,
{
fn new(env: &napi::Env, name: &str, field_formatter: F) -> Result<Self, napi::Error> {
let output_channel = super::window::create_log_output_channel(env, name)?;
let threadsafe_function: bindgen_prelude::Function<(&str, String), ()> = env
.create_function_from_closure("tracing_event_handler", |function_context| {
let ((method, message),): ((String, String),) = function_context.args()?;
let output_channel: bindgen_prelude::Object = function_context.this()?;
let log_function: bindgen_prelude::Function<String, ()> =
output_channel.get_named_property(&method)?;
log_function.apply(output_channel, message)
})?;
let threadsafe_function = threadsafe_function
.bind(output_channel)?
.build_threadsafe_function()
.callee_handled::<false>()
.max_queue_size::<0>()
.weak::<false>()
.build()?;
Ok(Self {
threadsafe_function,
field_formatter,
})
}
fn call_output_channel<S: Into<String>>(&self, level: Level, message: S) {
let level = match level {
Level::TRACE => "trace",
Level::DEBUG => "debug",
Level::INFO => "info",
Level::WARN => "warn",
Level::ERROR => "error",
};
let status = self.threadsafe_function.call(
(level, message.into()),
ThreadsafeFunctionCallMode::Blocking,
);
}
fn internal_message<S: core::fmt::Display>(&self, level: Level, message: S) {
self.call_output_channel(level, format!("Internal tracing message: {message}"));
}
}
impl<F, S> Layer<S> for VscodeLayer<F>
where
F: for<'writer> FormatFields<'writer> + 'static,
S: Subscriber + for<'registry> LookupSpan<'registry>,
{
fn on_new_span(
&self,
attributes: &tracing::span::Attributes<'_>,
id: &tracing::span::Id,
context: tracing_subscriber::layer::Context<'_, S>,
) {
let Some(span) = context.span(id) else {
self.internal_message(Level::ERROR, format!("no matching span for ID: {id:#?}"));
return;
};
let mut extensions = span.extensions_mut();
if extensions.get_mut::<FormattedFields<F>>().is_none() {
let mut formatted_fields: FormattedFields<F> = FormattedFields::new(String::new());
let format_result = self
.field_formatter
.format_fields(formatted_fields.as_writer(), attributes);
match format_result {
Ok(()) => extensions.insert(formatted_fields),
Err(error) => {
self.internal_message(Level::ERROR, format!("failed to format fields: {error}"))
}
}
}
}
fn on_event(
&self,
event: &tracing::Event<'_>,
context: tracing_subscriber::layer::Context<'_, S>,
) {
let mut message = String::new();
if let Some(event_scope) = context.event_scope(event) {
for span in event_scope.from_root() {
message.push_str(span.name());
match span.extensions().get::<FormattedFields<F>>() {
Some(formatted_fields) => {
if !formatted_fields.is_empty() {
message.push('{');
message.push_str(formatted_fields);
message.push('}');
}
}
None => {
self.internal_message(
Level::WARN,
format!("no formatted fields stored for {:#?}", span.id()),
);
message.push_str("{missing fields}");
}
}
message.push(':');
}
}
match event.metadata().module_path() {
Some(module_path) => {
if !message.is_empty() {
message.push(' ');
}
message.push_str(module_path);
message.push_str(": ");
}
None => {
self.internal_message(
Level::WARN,
format!(
"missing module path for event at callsite {:#?}",
event.metadata().callsite()
),
);
}
}
let writer = tracing_subscriber::fmt::format::Writer::new(&mut message);
if let Err(error) = self.field_formatter.format_fields(writer, event) {
self.internal_message(
Level::ERROR,
format!("failed to write event fields: {error}"),
);
}
self.call_output_channel(*event.metadata().level(), message);
}
}
pub fn init<F>(env: &napi::Env, name: &str, field_formatter: F) -> Result<(), napi::Error>
where
F: for<'writer> FormatFields<'writer> + Send + Sync + 'static,
{
let vscode_layer = VscodeLayer::new(env, name, field_formatter)?;
let subscriber = tracing_subscriber::registry().with(vscode_layer);
tracing::subscriber::set_global_default(subscriber).map_err(|error| {
napi::Error::from_reason(format!("Failed to set global default subscriber: {error}"))
})?;
std::panic::set_hook(Box::new(|panic_hook_info| {
tracing::error!(message = "Extension panic", %panic_hook_info);
}));
Ok(())
}