diff --git a/devtools/src/layer.rs b/devtools/src/layer.rs index 167353fc..52ad5c78 100644 --- a/devtools/src/layer.rs +++ b/devtools/src/layer.rs @@ -98,10 +98,13 @@ where fn on_event(&self, event: &tracing_core::Event<'_>, ctx: Context<'_, S>) { let at = Instant::now(); + let metadata = event.metadata(); self.send_event(&self.shared.dropped_log_events, || { - let metadata = event.metadata(); + Event::Metadata(metadata) + }); + self.send_event(&self.shared.dropped_log_events, || { let mut visitor = EventVisitor::new(metadata as *const _ as u64); event.record(&mut visitor); let (message, fields) = visitor.result(); diff --git a/devtools/src/tauri_plugin.rs b/devtools/src/tauri_plugin.rs index cd1e54b2..fd438c05 100644 --- a/devtools/src/tauri_plugin.rs +++ b/devtools/src/tauri_plugin.rs @@ -1,19 +1,60 @@ use crate::aggregator::Aggregator; use crate::server::Server; use crate::Command; +use std::collections::HashMap; use std::net::SocketAddr; use std::thread; use std::time::Duration; use tauri::Runtime; use tokio::sync::mpsc; +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +struct Key { + level: u16, + file: Option, + line: Option, +} + +#[allow(clippy::needless_pass_by_value)] +#[tauri::command] +fn log( + level: u16, + message: &str, + location: Option<&str>, + file: Option<&str>, + line: Option, + _key_values: Option>, +) { + match level { + 1 => { + tracing::trace!(target: "log", message, log.target = "webview", log.module_path = location, log.file = file, log.line = line); + } + 2 => { + tracing::debug!(target: "log", message, log.target = "webview", log.module_path = location, log.file = file, log.line = line); + } + 3 => { + tracing::info!(target: "log", message, log.target = "webview", log.module_path = location, log.file = file, log.line = line); + } + 4 => { + tracing::warn!(target: "log", message, log.target = "webview", log.module_path = location, log.file = file, log.line = line); + } + 5 => { + tracing::error!(target: "log", message, log.target = "webview", log.module_path = location, log.file = file, log.line = line); + } + _ => {} + } +} + pub(crate) fn init( addr: SocketAddr, publish_interval: Duration, aggregator: Aggregator, cmd_tx: mpsc::Sender, ) -> tauri::plugin::TauriPlugin { - tauri::plugin::Builder::new("probe") + // we pretend to be the log plugin so we can intercept the commands + // this plugin is incompatible with the log plugin anyway + tauri::plugin::Builder::new("log") + .invoke_handler(tauri::generate_handler![log]) .setup(move |app_handle| { let server = Server::new(cmd_tx, app_handle.clone()); diff --git a/devtools/src/visitors.rs b/devtools/src/visitors.rs index 7f54b226..4d2be4bd 100644 --- a/devtools/src/visitors.rs +++ b/devtools/src/visitors.rs @@ -96,11 +96,54 @@ impl Visit for FieldVisitor { } impl Visit for EventVisitor { - /// Visit a value that implements `Debug`. + /// Visit a double-precision floating point value. + fn record_f64(&mut self, field: &tracing_core::Field, value: f64) { + match field.name() { + // skip fields that are `log` metadata that have already been handled + "message" if self.message.is_none() => self.message = Some(value.to_string()), + _ => self.field_visitor.record_f64(field, value), + } + } + + /// Visit a signed 64-bit integer value. + fn record_i64(&mut self, field: &tracing_core::Field, value: i64) { + match field.name() { + // skip fields that are `log` metadata that have already been handled + "message" if self.message.is_none() => self.message = Some(value.to_string()), + _ => self.field_visitor.record_i64(field, value), + } + } + + /// Visit an unsigned 64-bit integer value. + fn record_u64(&mut self, field: &tracing_core::Field, value: u64) { + match field.name() { + // skip fields that are `log` metadata that have already been handled + "message" if self.message.is_none() => self.message = Some(value.to_string()), + _ => self.field_visitor.record_u64(field, value), + } + } + + /// Visit a boolean value. + fn record_bool(&mut self, field: &tracing_core::Field, value: bool) { + match field.name() { + // skip fields that are `log` metadata that have already been handled + "message" if self.message.is_none() => self.message = Some(value.to_string()), + _ => self.field_visitor.record_bool(field, value), + } + } + + /// Visit a string value. + fn record_str(&mut self, field: &tracing_core::Field, value: &str) { + match field.name() { + // skip fields that are `log` metadata that have already been handled + "message" if self.message.is_none() => self.message = Some(value.to_string()), + _ => self.field_visitor.record_str(field, value), + } + } + fn record_debug(&mut self, field: &tracing_core::Field, value: &dyn Debug) { match field.name() { // skip fields that are `log` metadata that have already been handled - name if name.starts_with("log.") => (), "message" if self.message.is_none() => self.message = Some(format!("{value:?}")), _ => self.field_visitor.record_debug(field, value), } diff --git a/web-client/src/views/dashboard/console.tsx b/web-client/src/views/dashboard/console.tsx index d1b880b6..20350c25 100644 --- a/web-client/src/views/dashboard/console.tsx +++ b/web-client/src/views/dashboard/console.tsx @@ -12,6 +12,7 @@ import { getLevelClasses } from "~/lib/console/get-level-classes"; import { LogLevelFilter } from "~/components/console/log-level-filter"; import { NoLogs } from "~/components/console/no-logs"; import { getFileNameFromPath } from "~/lib/console/get-file-name-from-path"; +import { processFieldValue } from "~/lib/span/process-field-value.ts"; export default function Console() { const { monitorData } = useMonitor(); @@ -69,6 +70,43 @@ export default function Console() { const timeDate = timestampToDate(at); const levelStyle = getLevelClasses(metadata?.level); + let target = metadata?.target; + let location = metadata?.location; + if (target === "log") { + const field = logEvent.fields.find( + (field) => field.name === "log.target" + ); + if (field) { + target = processFieldValue(field.value); + } + } + if (target === "webview") { + const file = logEvent.fields.find( + (field) => field.name === "log.file" + ); + const line = logEvent.fields.find( + (field) => field.name === "log.line" + ); + const mod = logEvent.fields.find( + (field) => field.name === "log.module_path" + ); + + let mod_file, mod_line; + if (mod) { + const str = processFieldValue(mod.value).replace("log@", ""); + const [http, url, port, line, col] = str.split(":"); + + mod_file = http + url + port; + mod_line = parseInt(line); + console.log(http, url, port, line, col); + } + + location = { + file: file ? processFieldValue(file.value) : mod_file, + line: line ? parseInt(processFieldValue(line.value)) : mod_line, + }; + } + return (
  • {message} - - {metadata!.target} + + {target} + + + {getFileNameFromPath(location!.file!)}:{location!.line} - - {getFileNameFromPath(metadata!.location!.file!)}: + + {getFileNameFromPath(metadata!.location!.modulePath!)}: {metadata!.location!.line}