Repository: raphamorim/rio (language: rust)

Issue:
Improve logging and store logs in a file
### Discussed in https://github.com/raphamorim/rio/discussions/626

<div type='discussions-op-text'>

<sup>Originally posted by **andrewcrook** August 31, 2024</sup>
I have tried to setup logging 

```
 [developer]
#log-level= “ERROR"
 log-level = “DEBUG"
```

However, I have looked in the common folders and console app for the log file and I cannot find its location.
Any ideas?</div>

I will be working on it so feel free to assign :)


Discussion / hints:
Thanks for the heads up @orhun ! Just please in mind that ideally the logs to file should be optional (settled up via config)

Maybe it can also receive the path where the logs will be store but it can be a follow up change.

Fix patch:
diff --git a/.gitignore b/.gitignore
index c7a82cbde6..0a7bcd2f8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ NOTES
 *.wasm
 *.ts
 *.js
+*.log
diff --git a/Cargo.lock b/Cargo.lock
index f9f28f0380..1d2c900658 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -563,11 +563,11 @@ dependencies = [
  "fuchsia-zircon-sys",
  "iovec",
  "libc",
- "log 0.4.22",
  "miow 0.5.0",
  "net2",
  "slab",
  "tempdir",
+ "tracing",
  "windows 0.42.0",
  "windows-sys 0.42.0",
 ]
@@ -2370,7 +2370,6 @@ dependencies = [
  "dirs",
  "image 0.25.2",
  "libc",
- "log 0.4.22",
  "parking_lot",
  "raw-window-handle",
  "regex",
@@ -2381,6 +2380,7 @@ dependencies = [
  "sugarloaf",
  "teletypewriter",
  "toml",
+ "tracing",
  "unicode-width",
  "wgpu",
 ]
@@ -2457,7 +2457,6 @@ dependencies = [
  "futures",
  "image 0.25.2",
  "libc",
- "log 0.4.22",
  "notify",
  "objc",
  "parking_lot",
@@ -2469,6 +2468,8 @@ dependencies = [
  "serde",
  "teletypewriter",
  "tinyvec",
+ "tracing",
+ "tracing-subscriber",
  "unicode-width",
  "url",
  "wgpu",
@@ -2795,7 +2796,6 @@ dependencies = [
  "image 0.25.2",
  "js-sys",
  "linked-hash-map",
- "log 0.4.22",
  "lru",
  "memmap2",
  "ordered-float",
@@ -2808,6 +2808,7 @@ dependencies = [
  "skrifa",
  "slotmap",
  "tinyvec",
+ "tracing",
  "ttf-parser 0.21.1",
  "twox-hash",
  "unicode-width",
@@ -2857,10 +2858,10 @@ dependencies = [
  "dirs",
  "iovec",
  "libc",
- "log 0.4.22",
  "miow 0.6.0",
  "parking_lot",
  "signal-hook",
+ "tracing",
  "windows-sys 0.48.0",
 ]
 
@@ -3029,9 +3030,21 @@ checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
 dependencies = [
  "log 0.4.22",
  "pin-project-lite",
+ "tracing-attributes",
  "tracing-core",
 ]
 
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.76",
+]
+
 [[package]]
 name = "tracing-core"
 version = "0.1.32"
@@ -3184,12 +3197,12 @@ dependencies = [
  "futures",
  "lazy_static",
  "libc",
- "log 0.4.22",
  "objc",
  "once_cell",
  "parking_lot",
  "raw-window-handle",
  "smol_str",
+ "tracing",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 7210e67265..6c54767079 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,7 +44,7 @@ unicode-width = "0.1.13"
 image_rs = { package = "image", version = "0.25.2", default-features = false, features = ["ico"] }
 regex = "1.10.5"
 bytemuck = { version = "1.17.0", features = [ "derive" ] }
-log = "0.4.22"
+swash = "0.1.18"
 serde = { version = "1.0.208", features = ["derive"] }
 wgpu = "22.1.0"
 libc = "0.2.158"
@@ -58,6 +58,7 @@ console_log = "0.2.2"
 bitflags = "2.6.0"
 iovec = "0.1.4"
 console_error_panic_hook = "0.1.7"
+tracing = "0.1.40"
 
 [profile.release]
 opt-level = 3
diff --git a/corcovado/Cargo.toml b/corcovado/Cargo.toml
index fb1aada256..e5130c32cf 100644
--- a/corcovado/Cargo.toml
+++ b/corcovado/Cargo.toml
@@ -17,7 +17,7 @@ include = [
 ]
 
 [dependencies]
-log = { workspace = true }
+tracing = { workspace = true }
 iovec = { workspace = true }
 slab = "0.4.9"
 net2 = "0.2.39"
diff --git a/corcovado/src/lib.rs b/corcovado/src/lib.rs
index d7ef464cda..7a51f44a93 100644
--- a/corcovado/src/lib.rs
+++ b/corcovado/src/lib.rs
@@ -116,7 +116,7 @@ extern crate miow;
 extern crate windows_sys;
 
 #[macro_use]
-extern crate log;
+extern crate tracing;
 
 mod event_imp;
 mod io;
diff --git a/docs/docs/command-line-interface.md b/docs/docs/command-line-interface.md
index 05e888f05a..a659d8afc5 100644
--- a/docs/docs/command-line-interface.md
+++ b/docs/docs/command-line-interface.md
@@ -15,6 +15,7 @@ Options:
   -e, --command <COMMAND>...       Command and args to execute (must be last argument)
   -w, --working-dir <WORKING_DIR>  Start the shell in the specified working directory
       --write-config [<PATH>]      Writes the config to a given path or the default location
+      --log-file                   Writes the logs to a file inside the config directory
   -h, --help                       Print help
   -V, --version                    Print version
 ```
diff --git a/docs/docs/config/log-level.md b/docs/docs/config/log-level.md
index 7bbe5e827f..515e0f199a 100644
--- a/docs/docs/config/log-level.md
+++ b/docs/docs/config/log-level.md
@@ -3,11 +3,12 @@ title: 'log-level'
 language: 'en'
 ---
 
-This property enables log level filter. Default is "OFF".
+This property enables log level filter and file. The default level is "OFF" and the logs are not logged to a file as default.
 
 ```toml
 [developer]
 log-level = "OFF"
+log-file = false
 ```
 
 If you have any suggestion of configuration ideas to Rio, please feel free to [open an issue](https://github.com/raphamorim/rio/issues/new).
diff --git a/frontends/rioterm/Cargo.toml b/frontends/rioterm/Cargo.toml
index f0ef37de6d..d4b9a2163f 100644
--- a/frontends/rioterm/Cargo.toml
+++ b/frontends/rioterm/Cargo.toml
@@ -16,7 +16,8 @@ name = "rio"
 path = "src/main.rs"
 
 [dependencies]
-log = { workspace = true }
+tracing = { workspace = true }
+tracing-subscriber = { version = "0.3.18", features = ["env-filter", "registry"] }
 wgpu = { workspace = true }
 rio-backend = { workspace = true }
 
diff --git a/frontends/rioterm/src/application.rs b/frontends/rioterm/src/application.rs
index 37a32b0d13..5a92afbecd 100644
--- a/frontends/rioterm/src/application.rs
+++ b/frontends/rioterm/src/application.rs
@@ -129,7 +129,7 @@ impl ApplicationHandler<EventPayload> for Application {
         window.is_focused = true;
         self.router.create_route_from_window(window);
 
-        log::info!("Initialisation complete");
+        tracing::info!("Initialisation complete");
     }
 
     fn user_event(&mut self, event_loop: &ActiveEventLoop, event: EventPayload) {
diff --git a/frontends/rioterm/src/bindings/mod.rs b/frontends/rioterm/src/bindings/mod.rs
index 7b5b9a5f12..1ea4d12e68 100644
--- a/frontends/rioterm/src/bindings/mod.rs
+++ b/frontends/rioterm/src/bindings/mod.rs
@@ -975,23 +975,23 @@ pub fn config_key_bindings(
 
                     if let Some(idx) = found_idx {
                         bindings.remove(idx);
-                        log::warn!(
+                        tracing::warn!(
                             "overwritten a previous key_binding with new one: {:?}",
                             key_binding
                         );
                     } else {
-                        log::info!("added a new key_binding: {:?}", key_binding);
+                        tracing::info!("added a new key_binding: {:?}", key_binding);
                     }
 
                     bindings.push(key_binding)
                 }
                 _ => {
-                    log::info!("added a new key_binding: {:?}", key_binding);
+                    tracing::info!("added a new key_binding: {:?}", key_binding);
                     bindings.push(key_binding)
                 }
             },
             Err(err_message) => {
-                log::error!("error loading a key binding: {:?}", err_message);
+                tracing::error!("error loading a key binding: {:?}", err_message);
             }
         }
     }
diff --git a/frontends/rioterm/src/cli.rs b/frontends/rioterm/src/cli.rs
index 2aba59f146..cc08c83221 100644
--- a/frontends/rioterm/src/cli.rs
+++ b/frontends/rioterm/src/cli.rs
@@ -34,6 +34,10 @@ pub struct TerminalOptions {
     /// Writes the config to a given path or the default location.
     #[clap(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
     pub write_config: Option<Option<PathBuf>>,
+
+    /// Writes the logs to a file inside the config directory.
+    #[clap(long)]
+    pub log_file: bool,
 }
 
 impl TerminalOptions {
diff --git a/frontends/rioterm/src/context.rs b/frontends/rioterm/src/context.rs
index cf4cf11804..a2b0443797 100644
--- a/frontends/rioterm/src/context.rs
+++ b/frontends/rioterm/src/context.rs
@@ -155,7 +155,7 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
         #[cfg(not(target_os = "windows"))]
         {
             if config.use_fork {
-                log::info!("rio -> teletypewriter: create_pty_with_fork");
+                tracing::info!("rio -> teletypewriter: create_pty_with_fork");
                 pty = match create_pty_with_fork(
                     &Cow::Borrowed(&config.shell.program),
                     cols,
@@ -163,12 +163,12 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
                 ) {
                     Ok(created_pty) => created_pty,
                     Err(err) => {
-                        log::error!("{err:?}");
+                        tracing::error!("{err:?}");
                         return Err(Box::new(err));
                     }
                 }
             } else {
-                log::info!("rio -> teletypewriter: create_pty_with_spawn");
+                tracing::info!("rio -> teletypewriter: create_pty_with_spawn");
                 pty = match create_pty_with_spawn(
                     &Cow::Borrowed(&config.shell.program),
                     config.shell.args.clone(),
@@ -178,7 +178,7 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
                 ) {
                     Ok(created_pty) => created_pty,
                     Err(err) => {
-                        log::error!("{err:?}");
+                        tracing::error!("{err:?}");
                         return Err(Box::new(err));
                     }
                 }
@@ -201,7 +201,7 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
             ) {
                 Ok(created_pty) => created_pty,
                 Err(err) => {
-                    log::error!("{err:?}");
+                    tracing::error!("{err:?}");
                     return Err(Box::new(err));
                 }
             }
@@ -252,7 +252,7 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
         ) {
             Ok(context) => context,
             Err(err_message) => {
-                log::error!("{:?}", err_message);
+                tracing::error!("{:?}", err_message);
 
                 event_proxy.send_event(
                     RioEvent::ReportToAssistant(RioError {
@@ -743,7 +743,7 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
                     }
                 }
                 Err(..) => {
-                    log::error!("not able to create a new context");
+                    tracing::error!("not able to create a new context");
                 }
             }
         }
diff --git a/frontends/rioterm/src/logger.rs b/frontends/rioterm/src/logger.rs
deleted file mode 100644
index 8704aa36fd..0000000000
--- a/frontends/rioterm/src/logger.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use log::{Metadata, Record};
-
-pub struct Logger;
-
-impl log::Log for Logger {
-    fn enabled(&self, _metadata: &Metadata) -> bool {
-        // If declarative wants to ignore trace
-        // metadata.level() <= log::Level::Debug
-
-        true
-    }
-
-    fn log(&self, record: &Record) {
-        // For cases where wants to validate if log is enabled
-        // if self.enabled(record.metadata()) {
-        //     println!("{}", record.level());
-        // }
-
-        let line = format!(
-            "\x1b[35m[{}]\x1b[0m \x1b[34m{}\x1b[0m {}\0",
-            record.level(),
-            record.target(),
-            record.args()
-        );
-        println!("{line}");
-    }
-
-    fn flush(&self) {}
-}
diff --git a/frontends/rioterm/src/main.rs b/frontends/rioterm/src/main.rs
index 78564f6c11..37908a9663 100644
--- a/frontends/rioterm/src/main.rs
+++ b/frontends/rioterm/src/main.rs
@@ -10,7 +10,6 @@ mod cli;
 mod constants;
 mod context;
 mod ime;
-mod logger;
 mod messenger;
 mod mouse;
 #[cfg(windows)]
@@ -24,17 +23,23 @@ mod screen;
 mod watcher;
 
 use clap::Parser;
-use log::{info, LevelFilter, SetLoggerError};
-use logger::Logger;
+use rio_backend::config::config_dir_path;
 use rio_backend::event::EventPayload;
 use rio_backend::{ansi, crosswords, event, performer, selection};
+use std::path::PathBuf;
 use std::str::FromStr;
+use tracing::level_filters::LevelFilter;
+use tracing_subscriber::{
+    self, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
+};
 
 #[cfg(windows)]
 use windows_sys::Win32::System::Console::{
     AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS,
 };
 
+const LOG_LEVEL_ENV: &str = "RIO_LOG_LEVEL";
+
 pub fn setup_environment_variables(config: &rio_backend::config::Config) {
     #[cfg(unix)]
     let terminfo = if teletypewriter::terminfo_exists("rio") {
@@ -45,7 +50,9 @@ pub fn setup_environment_variables(config: &rio_backend::config::Config) {
 
     #[cfg(unix)]
     {
-        info!("[setup_environment_variables] terminfo: {terminfo}");
+        let span = tracing::span!(tracing::Level::INFO, "setup_environment_variables");
+        let _guard = span.enter();
+        tracing::info!("terminfo: {terminfo}");
         std::env::set_var("TERM", terminfo);
     }
 
@@ -72,19 +79,50 @@ pub fn setup_environment_variables(config: &rio_backend::config::Config) {
     }
 }
 
-static LOGGER: Logger = Logger;
+fn setup_logs_by_filter_level(
+    log_level: &str,
+    log_file: bool,
+) -> Result<(), Box<dyn std::error::Error>> {
+    let mut filter_level = LevelFilter::from_str(log_level).unwrap_or(LevelFilter::OFF);
 
-fn setup_logs_by_filter_level(log_level: &str) -> Result<(), SetLoggerError> {
-    let mut filter_level = LevelFilter::from_str(log_level).unwrap_or(LevelFilter::Off);
-
-    if let Ok(data) = std::env::var("RIO_LOG_LEVEL") {
+    if let Ok(data) = std::env::var(LOG_LEVEL_ENV) {
         if !data.is_empty() {
             filter_level = LevelFilter::from_str(&data).unwrap_or(filter_level);
         }
     }
 
-    info!("[setup_logs_by_filter_level] log_level: {log_level}");
-    log::set_logger(&LOGGER).map(|()| log::set_max_level(filter_level))
+    let env_filter = EnvFilter::builder().with_default_directive(filter_level.into());
+    let stdout_subscriber = tracing_subscriber::fmt::layer()
+        .with_writer(std::io::stdout)
+        .with_ansi(true)
+        .with_filter(env_filter.parse("")?);
+    let subscriber = tracing_subscriber::registry().with(stdout_subscriber);
+
+    let mut log_file_path = PathBuf::new();
+    if log_file {
+        let log_dir_path = config_dir_path().join("log");
+        log_file_path = log_dir_path.join("rio.log");
+        std::fs::create_dir_all(&log_dir_path)?;
+        let log_file = std::fs::File::create(&log_file_path)?;
+        let file_subscriber = tracing_subscriber::fmt::layer()
+            .with_file(true)
+            .with_line_number(true)
+            .with_writer(log_file)
+            .with_target(false)
+            .with_ansi(false)
+            .with_filter(env_filter.parse("")?);
+        subscriber.with(file_subscriber).init();
+    } else {
+        subscriber.init();
+    }
+
+    let span = tracing::span!(tracing::Level::INFO, "logger");
+    let _guard = span.enter();
+    tracing::info!("logging level: {log_level}");
+    if log_file {
+        tracing::info!("logging to a file: {}", log_file_path.display());
+    }
+    Ok(())
 }
 
 fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -104,7 +142,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
 
     let write_config_path = args.window_options.terminal_options.write_config.clone();
     if let Some(config_path) = write_config_path {
-        let _ = setup_logs_by_filter_level("TRACE");
+        let _ = setup_logs_by_filter_level("TRACE", false);
         rio_backend::config::create_config_file(config_path);
         return Ok(());
     }
@@ -115,8 +153,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     };
 
     {
-        if setup_logs_by_filter_level(&config.developer.log_level).is_err() {
-            eprintln!("unable to configure log level");
+        let log_to_file = args.window_options.terminal_options.log_file.clone();
+        if let Err(e) = setup_logs_by_filter_level(
+            &config.developer.log_level,
+            log_to_file || config.developer.log_file,
+        ) {
+            eprintln!("unable to configure the logger: {e:?}");
         }
 
         if let Some(command) = args.window_options.terminal_options.command() {
diff --git a/frontends/rioterm/src/platform/macos/mod.rs b/frontends/rioterm/src/platform/macos/mod.rs
index 9387c5eba9..f1acfee13b 100644
--- a/frontends/rioterm/src/platform/macos/mod.rs
+++ b/frontends/rioterm/src/platform/macos/mod.rs
@@ -7,9 +7,9 @@ use std::os::raw::c_char;
 use std::{env, slice, str};
 
 use libc::{setlocale, LC_ALL, LC_CTYPE};
-use log::debug;
 use objc::runtime::{Class, Object};
 use objc::{msg_send, sel, sel_impl};
+use tracing::debug;
 const FALLBACK_LOCALE: &str = "UTF-8";
 
 pub fn set_locale_environment() {
diff --git a/frontends/rioterm/src/router/mod.rs b/frontends/rioterm/src/router/mod.rs
index 29e5bb8abc..e60be8b4b5 100644
--- a/frontends/rioterm/src/router/mod.rs
+++ b/frontends/rioterm/src/router/mod.rs
@@ -379,7 +379,7 @@ impl RouteWindow {
 
         #[cfg(not(any(target_os = "macos", windows)))]
         if let Some(token) = event_loop.read_token_from_env() {
-            log::debug!("Activating window with token: {token:?}");
+            tracing::debug!("Activating window with token: {token:?}");
             window_builder = window_builder.with_activation_token(token);
 
             // Remove the token from the env.
diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs
index 75424d0684..6135516acb 100644
--- a/frontends/rioterm/src/screen/mod.rs
+++ b/frontends/rioterm/src/screen/mod.rs
@@ -1279,16 +1279,20 @@ impl Screen<'_> {
             let main_fd = *self.ctx().current().main_fd;
             let shell_pid = &self.ctx().current().shell_pid;
             match teletypewriter::spawn_daemon(program, args, main_fd, *shell_pid) {
-                Ok(_) => log::debug!("Launched {} with args {:?}", program, args),
-                Err(_) => log::warn!("Unable to launch {} with args {:?}", program, args),
+                Ok(_) => tracing::debug!("Launched {} with args {:?}", program, args),
+                Err(_) => {
+                    tracing::warn!("Unable to launch {} with args {:?}", program, args)
+                }
             }
         }
 
         #[cfg(windows)]
         {
             match teletypewriter::spawn_daemon(program, args) {
-                Ok(_) => log::debug!("Launched {} with args {:?}", program, args),
-                Err(_) => log::warn!("Unable to launch {} with args {:?}", program, args),
+                Ok(_) => tracing::debug!("Launched {} with args {:?}", program, args),
+                Err(_) => {
+                    tracing::warn!("Unable to launch {} with args {:?}", program, args)
+                }
             }
         }
     }
diff --git a/frontends/rioterm/src/screen/touch.rs b/frontends/rioterm/src/screen/touch.rs
index e5610568da..ae6c76c7e0 100644
--- a/frontends/rioterm/src/screen/touch.rs
+++ b/frontends/rioterm/src/screen/touch.rs
@@ -127,7 +127,7 @@ fn on_touch_motion(route: &mut Route, touch: Touch) {
             let delta_x = touch.location.x - start.location.x;
             let delta_y = touch.location.y - start.location.y;
             if delta_x.abs() > MAX_TAP_DISTANCE {
-                log::info!("tap to select");
+                tracing::info!("tap to select");
                 // Update gesture state.
                 let start_location = start.location;
                 *touch_purpose = TouchPurpose::Select(*start);
@@ -155,13 +155,13 @@ fn on_touch_motion(route: &mut Route, touch: Touch) {
                 // Apply motion since touch start.
                 on_touch_motion(route, touch);
             } else if delta_y.abs() > MAX_TAP_DISTANCE {
-                log::info!("tap to scroll");
+                tracing::info!("tap to scroll");
                 // Update gesture state.
                 *touch_purpose = TouchPurpose::Scroll(*start);
                 // Apply motion since touch start.
                 on_touch_motion(route, touch);
             } else {
-                log::info!("tap normal");
+                tracing::info!("tap normal");
             }
         }
         TouchPurpose::Zoom(zoom) => {
@@ -177,14 +177,14 @@ fn on_touch_motion(route: &mut Route, touch: Touch) {
                     .screen
                     .change_font_size(FontSizeAction::Decrease);
             }
-            log::info!("zoom motion: {}", font_delta);
+            tracing::info!("zoom motion: {}", font_delta);
         }
         TouchPurpose::Scroll(last_touch) => {
             // Calculate delta and update last touch position.
             let delta_y = touch.location.y - last_touch.location.y;
             *touch_purpose = TouchPurpose::Scroll(touch);
             route.window.screen.scroll(0., delta_y);
-            log::info!("scroll motion: {}", delta_y);
+            tracing::info!("scroll motion: {}", delta_y);
         }
         TouchPurpose::Select(_) => {
             let layout = route.window.screen.sugarloaf.layout();
@@ -192,7 +192,7 @@ fn on_touch_motion(route: &mut Route, touch: Touch) {
             let y = touch.location.y.clamp(0.0, layout.height.into()) as usize;
             route.window.screen.mouse.x = x;
             route.window.screen.mouse.y = y;
-            log::info!("select motion");
+            tracing::info!("select motion");
         }
         TouchPurpose::Invalid(_) => (),
     }
@@ -229,14 +229,14 @@ fn on_touch_end(route: &mut Route, touch: Touch) {
                 .on_left_click(route.window.screen.mouse_position(0));
             route.window.screen.mouse.click_state = ClickState::None;
             route.window.screen.mouse.left_button_state = ElementState::Released;
-            log::info!("tap end");
+            tracing::info!("tap end");
         }
         // Invalidate zoom once a finger was released.
         TouchPurpose::Zoom(zoom) => {
             let mut slots = zoom.slots();
             slots.remove(&touch.id);
             *touch_purpose = TouchPurpose::Invalid(slots);
-            log::info!("zoom end");
+            tracing::info!("zoom end");
         }
         // Reset touch state once all slots were released.
         TouchPurpose::Invalid(slots) => {
@@ -250,12 +250,12 @@ fn on_touch_end(route: &mut Route, touch: Touch) {
             *touch_purpose = Default::default();
             route.window.screen.mouse.click_state = ClickState::None;
             route.window.screen.mouse.left_button_state = ElementState::Released;
-            log::info!("select end");
+            tracing::info!("select end");
         }
         // Reset touch state on scroll finish.
         TouchPurpose::Scroll(_) => {
             *touch_purpose = Default::default();
-            log::info!("scroll end");
+            tracing::info!("scroll end");
         }
         TouchPurpose::None => (),
     }
diff --git a/frontends/rioterm/src/watcher.rs b/frontends/rioterm/src/watcher.rs
index 1aff134220..3ff623956f 100644
--- a/frontends/rioterm/src/watcher.rs
+++ b/frontends/rioterm/src/watcher.rs
@@ -27,7 +27,7 @@ pub fn configuration_file_updates<
         if let Err(err_message) =
             watcher.watch(path.as_ref(), RecursiveMode::NonRecursive)
         {
-            log::warn!("unable to watch config directory {err_message:?}");
+            tracing::warn!("unable to watch config directory {err_message:?}");
         };
 
         for res in rx {
@@ -37,7 +37,9 @@ pub fn configuration_file_updates<
                     | EventKind::Create(_)
                     | EventKind::Modify(_)
                     | EventKind::Other => {
-                        log::info!("config directory has dispatched an event {event:?}");
+                        tracing::info!(
+                            "config directory has dispatched an event {event:?}"
+                        );
                         event_proxy.send_event(
                             RioEvent::UpdateConfig,
                             rio_backend::event::WindowId::from(0),
@@ -46,7 +48,7 @@ pub fn configuration_file_updates<
                     _ => (),
                 },
                 Err(err_message) => {
-                    log::error!("unable to watch config directory: {err_message:?}")
+                    tracing::error!("unable to watch config directory: {err_message:?}")
                 }
             }
         }
diff --git a/frontends/wasm/src/lib.rs b/frontends/wasm/src/lib.rs
index cafa6372af..49be22c900 100644
--- a/frontends/wasm/src/lib.rs
+++ b/frontends/wasm/src/lib.rs
@@ -70,7 +70,7 @@ mod wasm {
 
     #[wasm_bindgen(start)]
     pub fn run() {
-        console_log::init_with_level(log::Level::Debug)
+        console_tracing::init_with_level(tracing::Level::Debug)
             .expect("error initializing logger");
 
         #[allow(clippy::main_recursion)]
@@ -112,7 +112,7 @@ mod wasm {
     }
 
     pub fn log_event(log_list: &web_sys::Element, event: &Event<()>) {
-        log::debug!("{:?}", event);
+        tracing::debug!("{:?}", event);
 
         // Getting access to browser logs requires a lot of setup on mobile devices.
         // So we implement this basic logging system into the page to give developers an easy alternative.
diff --git a/rio-backend/Cargo.toml b/rio-backend/Cargo.toml
index bf60f2472f..02dd2691e5 100644
--- a/rio-backend/Cargo.toml
+++ b/rio-backend/Cargo.toml
@@ -14,7 +14,7 @@ path = "src/lib.rs"
 crate-type = ["cdylib", "rlib"]
 
 [dependencies]
-log = { workspace = true }
+tracing = { workspace = true }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 toml = "0.8.19"
diff --git a/rio-backend/src/ansi/mode.rs b/rio-backend/src/ansi/mode.rs
index 3d90fe429f..0777790614 100644
--- a/rio-backend/src/ansi/mode.rs
+++ b/rio-backend/src/ansi/mode.rs
@@ -1,4 +1,4 @@
-use log::warn;
+use tracing::warn;
 
 #[derive(Debug, Eq, PartialEq)]
 pub enum Mode {
diff --git a/rio-backend/src/ansi/sixel.rs b/rio-backend/src/ansi/sixel.rs
index 87250317ee..e18a1bea57 100644
--- a/rio-backend/src/ansi/sixel.rs
+++ b/rio-backend/src/ansi/sixel.rs
@@ -30,7 +30,7 @@ use crate::config::colors::ColorRgb;
 use sugarloaf::{ColorType, GraphicData, GraphicId};
 
 use copa::Params;
-use log::trace;
+use tracing::trace;
 
 /// Type for color registers.
 #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
diff --git a/rio-backend/src/clipboard.rs b/rio-backend/src/clipboard.rs
index db6e732dc3..0cd4f10683 100644
--- a/rio-backend/src/clipboard.rs
+++ b/rio-backend/src/clipboard.rs
@@ -1,8 +1,8 @@
 // clipboard.rs was retired originally from https://github.com/alacritty/alacritty/blob/e35e5ad14fce8456afdd89f2b392b9924bb27471/alacritty/src/clipboard.rs
 // which is licensed under Apache 2.0 license.
 
-use log::warn;
 use raw_window_handle::RawDisplayHandle;
+use tracing::warn;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum ClipboardType {
diff --git a/rio-backend/src/config/defaults.rs b/rio-backend/src/config/defaults.rs
index fac6745ae0..af798f6d2b 100644
--- a/rio-backend/src/config/defaults.rs
+++ b/rio-backend/src/config/defaults.rs
@@ -442,10 +442,11 @@ pub fn default_config_file_content() -> String {
 
 # Log level
 #
-# This property enables log level filter. Default is "OFF".
+# This property enables log level filter and file. The default level is "OFF" and the logs are not logged to a file as default.
 #
 # Example:
 # [developer]
 # log-level = "OFF"
+# # log-file = "rio.log"
 "#.to_string()
 }
diff --git a/rio-backend/src/config/mod.rs b/rio-backend/src/config/mod.rs
index efbc440c74..64fa9649b9 100644
--- a/rio-backend/src/config/mod.rs
+++ b/rio-backend/src/config/mod.rs
@@ -15,13 +15,13 @@ use crate::config::navigation::Navigation;
 use crate::config::renderer::Renderer;
 use crate::config::window::Window;
 use colors::Colors;
-use log::warn;
 use serde::{Deserialize, Serialize};
 use std::io::Write;
 use std::path::PathBuf;
 use std::{default::Default, fs::File};
 use sugarloaf::font::fonts::SugarloafFonts;
 use theme::{AdaptiveColors, AdaptiveTheme, Theme};
+use tracing::warn;
 
 #[derive(Clone, Debug)]
 pub enum ConfigError {
@@ -57,12 +57,15 @@ pub struct Developer {
     pub enable_fps_counter: bool,
     #[serde(default = "default_log_level", rename = "log-level")]
     pub log_level: String,
+    #[serde(rename = "log-file", default)]
+    pub log_file: bool,
 }
 
 impl Default for Developer {
     fn default() -> Developer {
         Developer {
             log_level: default_log_level(),
+            log_file: false,
             enable_fps_counter: false,
         }
     }
@@ -169,7 +172,7 @@ pub fn config_file_content() -> String {
 pub fn create_config_file(path: Option<PathBuf>) {
     let default_file_path = path.clone().unwrap_or(config_file_path());
     if default_file_path.exists() {
-        log::info!(
+        tracing::info!(
             "configuration file already exists at {}",
             default_file_path.display()
         );
@@ -180,27 +183,32 @@ pub fn create_config_file(path: Option<PathBuf>) {
         let default_dir_path = config_dir_path();
         match std::fs::create_dir_all(&default_dir_path) {
             Ok(_) => {
-                log::info!("configuration path created {}", default_dir_path.display());
+                tracing::info!(
+                    "configuration path created {}",
+                    default_dir_path.display()
+                );
             }
             Err(err_message) => {
-                log::error!("could not create config directory: {err_message}");
+                tracing::error!("could not create config directory: {err_message}");
             }
         }
     }
 
     match File::create(&default_file_path) {
         Err(err_message) => {
-            log::error!(
+            tracing::error!(
                 "could not create config file {}: {err_message}",
                 default_file_path.display()
             )
         }
         Ok(mut created_file) => {
-            log::info!("configuration file created {}", default_file_path.display());
+            tracing::info!("configuration file created {}", default_file_path.display());
 
             if let Err(err_message) = writeln!(created_file, "{}", config_file_content())
             {
-                log::error!("could not update config file with defaults: {err_message}")
+                tracing::error!(
+                    "could not update config file with defaults: {err_message}"
+                )
             }
         }
     }
diff --git a/rio-backend/src/crosswords/mod.rs b/rio-backend/src/crosswords/mod.rs
index ff0d003c08..3e0571bc50 100644
--- a/rio-backend/src/crosswords/mod.rs
+++ b/rio-backend/src/crosswords/mod.rs
@@ -47,7 +47,6 @@ use base64::{engine::general_purpose, Engine as _};
 use bitflags::bitflags;
 use copa::Params;
 use grid::row::Row;
-use log::{debug, info, warn};
 use pos::{
     Boundary, CharsetIndex, Column, Cursor, CursorState, Direction, Line, Pos, Side,
 };
@@ -59,6 +58,7 @@ use std::option::Option;
 use std::ptr;
 use std::sync::Arc;
 use sugarloaf::GraphicData;
+use tracing::{debug, info, warn};
 use unicode_width::UnicodeWidthChar;
 use vi_mode::{ViModeCursor, ViMotion};
 
@@ -1495,11 +1495,11 @@ impl<U: EventListener> Handler for Crosswords<U> {
 
     #[inline]
     fn push_title(&mut self) {
-        log::trace!("Pushing '{:?}' onto title stack", self.title);
+        tracing::trace!("Pushing '{:?}' onto title stack", self.title);
 
         if self.title_stack.len() >= TITLE_STACK_MAX_DEPTH {
             let removed = self.title_stack.remove(0);
-            log::trace!(
+            tracing::trace!(
                 "Removing '{:?}' from bottom of title stack that exceeds its maximum depth",
                 removed
             );
@@ -1510,10 +1510,10 @@ impl<U: EventListener> Handler for Crosswords<U> {
 
     #[inline]
     fn pop_title(&mut self) {
-        log::trace!("Attempting to pop title from stack...");
+        tracing::trace!("Attempting to pop title from stack...");
 
         if let Some(popped) = self.title_stack.pop() {
-            log::trace!("Title '{:?}' popped from stack", popped);
+            tracing::trace!("Title '{:?}' popped from stack", popped);
             self.set_title(Some(popped));
         }
     }
@@ -1739,13 +1739,13 @@ impl<U: EventListener> Handler for Crosswords<U> {
 
     #[inline]
     fn set_keypad_application_mode(&mut self) {
-        log::trace!("Setting keypad application mode");
+        tracing::trace!("Setting keypad application mode");
         self.mode.insert(Mode::APP_KEYPAD);
     }
 
     #[inline]
     fn unset_keypad_application_mode(&mut self) {
-        log::trace!("Unsetting keypad application mode");
+        tracing::trace!("Unsetting keypad application mode");
         self.mode.remove(Mode::APP_KEYPAD);
     }
 
@@ -1774,7 +1774,7 @@ impl<U: EventListener> Handler for Crosswords<U> {
         index: pos::CharsetIndex,
         charset: pos::StandardCharset,
     ) {
-        log::trace!("Configuring charset {:?} as {:?}", index, charset);
+        tracing::trace!("Configuring charset {:?} as {:?}", index, charset);
         self.grid.cursor.charsets[index] = charset;
     }
 
@@ -1885,14 +1885,14 @@ impl<U: EventListener> Handler for Crosswords<U> {
     fn identify_terminal(&mut self, intermediate: Option<char>) {
         match intermediate {
             None => {
-                log::trace!("Reporting primary device attributes");
+                tracing::trace!("Reporting primary device attributes");
                 let text = String::from("\x1b[?62;4;6;22c");
                 // let text = String::from("\x1b[?62;6;22c");
                 self.event_proxy
                     .send_event(RioEvent::PtyWrite(text), self.window_id);
             }
             Some('>') => {
-                log::trace!("Reporting secondary device attributes");
+                tracing::trace!("Reporting secondary device attributes");
                 let version = version_number(env!("CARGO_PKG_VERSION"));
                 let text = format!("\x1b[>0;{version};1c");
                 self.event_proxy
@@ -1952,7 +1952,7 @@ impl<U: EventListener> Handler for Crosswords<U> {
 
     #[inline]
     fn device_status(&mut self, arg: usize) {
-        log::trace!("Reporting device status: {}", arg);
+        tracing::trace!("Reporting device status: {}", arg);
         match arg {
             5 => {
                 let text = String::from("\x1b[0n");
@@ -2197,7 +2197,7 @@ impl<U: EventListener> Handler for Crosswords<U> {
 
     #[inline]
     fn move_forward_tabs(&mut self, count: u16) {
-        log::trace!("[unimplemented] Moving forward {} tabs", count);
+        tracing::trace!("[unimplemented] Moving forward {} tabs", count);
     }
 
     #[inline]
diff --git a/rio-backend/src/crosswords/search.rs b/rio-backend/src/crosswords/search.rs
index 5bf3cfac20..25c0d8f7cd 100644
--- a/rio-backend/src/crosswords/search.rs
+++ b/rio-backend/src/crosswords/search.rs
@@ -7,12 +7,12 @@ use std::error::Error;
 use std::mem;
 use std::ops::RangeInclusive;
 
-use log::{debug, warn};
 use regex_automata::hybrid::dfa::{Builder, Cache, Config, DFA};
 pub use regex_automata::hybrid::BuildError;
 use regex_automata::nfa::thompson::Config as ThompsonConfig;
 use regex_automata::util::syntax::Config as SyntaxConfig;
 use regex_automata::{Anchored, Input, MatchKind};
+use tracing::{debug, warn};
 
 use crate::crosswords::grid::{BidirectionalIterator, Dimensions, GridIterator, Indexed};
 use crate::crosswords::square::{Flags, Square};
diff --git a/rio-backend/src/performer/handler.rs b/rio-backend/src/performer/handler.rs
index d0605e6202..55175af7d4 100644
--- a/rio-backend/src/performer/handler.rs
+++ b/rio-backend/src/performer/handler.rs
@@ -4,11 +4,11 @@ use crate::config::colors::{AnsiColor, ColorRgb, NamedColor};
 use crate::crosswords::pos::{CharsetIndex, Column, Line, StandardCharset};
 use crate::crosswords::square::Hyperlink;
 use cursor_icon::CursorIcon;
-use log::{debug, warn};
 use std::str::FromStr;
 use std::time::Duration;
 use std::time::Instant;
 use sugarloaf::GraphicData;
+use tracing::{debug, warn};
 
 use crate::crosswords::attr::Attr;
 
@@ -544,7 +544,7 @@ impl<U: Handler> copa::Perform for Performer<'_, U> {
     }
 
     fn execute(&mut self, byte: u8) {
-        log::trace!("[execute] {byte:04x}");
+        tracing::trace!("[execute] {byte:04x}");
 
         match byte {
             C0::HT => self.handler.put_tab(1),
@@ -583,7 +583,7 @@ impl<U: Handler> copa::Perform for Performer<'_, U> {
         match self.state.dcs {
             Some(Dcs::SixelData(ref mut parser)) => {
                 if let Err(err) = parser.put(byte) {
-                    log::warn!("Failed to parse Sixel data: {}", err);
+                    tracing::warn!("Failed to parse Sixel data: {}", err);
                     self.state.dcs = None;
                 }
             }
@@ -604,7 +604,7 @@ impl<U: Handler> copa::Perform for Performer<'_, U> {
                 Ok((graphic, palette)) => {
                     self.handler.insert_graphic(graphic, Some(palette))
                 }
-                Err(err) => log::warn!("Failed to parse Sixel data: {}", err),
+                Err(err) => tracing::warn!("Failed to parse Sixel data: {}", err),
             },
             _ => debug!("[unhandled unhook]"),
         }
diff --git a/rio-backend/src/performer/mod.rs b/rio-backend/src/performer/mod.rs
index dc175b5499..3197c73d26 100644
--- a/rio-backend/src/performer/mod.rs
+++ b/rio-backend/src/performer/mod.rs
@@ -8,13 +8,13 @@ use corcovado::channel;
 #[cfg(unix)]
 use corcovado::unix::UnixReady;
 use corcovado::{self, Events, PollOpt, Ready};
-use log::error;
 use std::borrow::Cow;
 use std::collections::VecDeque;
 use std::io::{self, ErrorKind, Read, Write};
 use std::sync::Arc;
 use std::thread::{Builder, JoinHandle};
 use std::time::Instant;
+use tracing::error;
 
 /// Like `thread::spawn`, but with a `name` argument.
 pub fn spawn_named<F, T, S>(name: S, f: F) -> JoinHandle<T>
diff --git a/sugarloaf/Cargo.toml b/sugarloaf/Cargo.toml
index e77e0fb2f0..f499d20ed9 100644
--- a/sugarloaf/Cargo.toml
+++ b/sugarloaf/Cargo.toml
@@ -32,7 +32,7 @@ targets = [
 [dependencies]
 wgpu = { workspace = true }
 bytemuck = { workspace = true }
-log = { workspace = true }
+tracing = { workspace = true }
 serde = { workspace = true }
 twox-hash = { version = "1.6.3", default-features = false }
 image_rs = { workspace = true }
diff --git a/sugarloaf/src/components/layer/atlas.rs b/sugarloaf/src/components/layer/atlas.rs
index 9719d88da7..3a25ae48c8 100644
--- a/sugarloaf/src/components/layer/atlas.rs
+++ b/sugarloaf/src/components/layer/atlas.rs
@@ -86,7 +86,7 @@ impl Atlas {
             entry
         };
 
-        log::info!("Allocated atlas entry: {:?}", entry);
+        tracing::info!("Allocated atlas entry: {:?}", entry);
 
         // It is a webgpu requirement that:
         //   BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
@@ -135,13 +135,13 @@ impl Atlas {
             }
         }
 
-        log::info!("Current atlas: {:?}", self);
+        tracing::info!("Current atlas: {:?}", self);
 
         Some(entry)
     }
 
     pub fn remove(&mut self, entry: &Entry) {
-        log::info!("Removing atlas entry: {:?}", entry);
+        tracing::info!("Removing atlas entry: {:?}", entry);
 
         match entry {
             Entry::Contiguous(allocation) => {
@@ -254,7 +254,7 @@ impl Atlas {
     }
 
     fn deallocate(&mut self, allocation: &Allocation) {
-        log::info!("Deallocating atlas: {:?}", allocation);
+        tracing::info!("Deallocating atlas: {:?}", allocation);
 
         match allocation {
             Allocation::Full { layer } => {
diff --git a/sugarloaf/src/components/layer/mod.rs b/sugarloaf/src/components/layer/mod.rs
index 4b08d94a8e..3aacf18aed 100644
--- a/sugarloaf/src/components/layer/mod.rs
+++ b/sugarloaf/src/components/layer/mod.rs
@@ -341,7 +341,7 @@ impl LayerBrush {
         let texture_version = self.texture_atlas.layer_count();
 
         if self.texture_version != texture_version {
-            log::info!("Atlas has grown. Recreating bind group...");
+            tracing::info!("Atlas has grown. Recreating bind group...");
 
             self.texture = device.create_bind_group(&wgpu::BindGroupDescriptor {
                 label: Some("image texture atlas bind group"),
@@ -401,7 +401,7 @@ impl LayerBrush {
         let texture_version = self.texture_atlas.layer_count();
 
         if self.texture_version != texture_version {
-            log::info!("Atlas has grown. Recreating bind group...");
+            tracing::info!("Atlas has grown. Recreating bind group...");
 
             self.texture = device.create_bind_group(&wgpu::BindGroupDescriptor {
                 label: Some("image texture atlas bind group"),
diff --git a/sugarloaf/src/context/mod.rs b/sugarloaf/src/context/mod.rs
index 59d9d6a642..04c82e08c6 100644
--- a/sugarloaf/src/context/mod.rs
+++ b/sugarloaf/src/context/mod.rs
@@ -46,7 +46,7 @@ fn find_best_texture_format(formats: Vec<wgpu::TextureFormat>) -> wgpu::TextureF
         filtered_formats.first().unwrap().clone_into(&mut format);
     }
 
-    log::info!("Sugarloaf selected format: {format:?} from {:?}", formats);
+    tracing::info!("Sugarloaf selected format: {format:?} from {:?}", formats);
 
     format
 }
@@ -73,17 +73,17 @@ impl Context<'_> {
             ..Default::default()
         });
 
-        log::info!("selected instance: {instance:?}");
+        tracing::info!("selected instance: {instance:?}");
 
         #[cfg(not(target_arch = "wasm32"))]
         {
-            log::info!("Available adapters:");
+            tracing::info!("Available adapters:");
             for a in instance.enumerate_adapters(wgpu::Backends::all()) {
-                log::info!("    {:?}", a.get_info())
+                tracing::info!("    {:?}", a.get_info())
             }
         }
 
-        log::info!("initializing the surface");
+        tracing::info!("initializing the surface");
 
         let size = sugarloaf_window.size;
         let scale = sugarloaf_window.scale;
@@ -99,7 +99,7 @@ impl Context<'_> {
         ))
         .expect("Request adapter");
 
-        log::info!("Selected adapter: {:?}", adapter.get_info());
+        tracing::info!("Selected adapter: {:?}", adapter.get_info());
 
         let caps = surface.get_capabilities(&adapter);
 
diff --git a/sugarloaf/src/font/loader/mod.rs b/sugarloaf/src/font/loader/mod.rs
index 002ab3a12d..9a90916008 100644
--- a/sugarloaf/src/font/loader/mod.rs
+++ b/sugarloaf/src/font/loader/mod.rs
@@ -140,7 +140,7 @@ impl Database {
                         });
                         ids.push(ID(id));
                     }
-                    Err(e) => log::warn!(
+                    Err(e) => tracing::warn!(
                         "Failed to load a font face {} from source cause {}.",
                         index,
                         e
@@ -163,7 +163,7 @@ impl Database {
             match parse_face_info(source.clone(), data, index) {
                 Ok(info) => self.push_face_info(info),
                 Err(e) => {
-                    log::warn!(
+                    tracing::warn!(
                         "Failed to load a font face {} from '{}' cause {}.",
                         index,
                         path.display(),
@@ -223,7 +223,7 @@ impl Database {
                     Some("ttf") | Some("ttc") | Some("TTF") | Some("TTC")
                     | Some("otf") | Some("otc") | Some("OTF") | Some("OTC") => {
                         if let Err(e) = self.load_font_file(&path) {
-                            log::warn!(
+                            tracing::warn!(
                                 "Failed to load '{}' cause {}.",
                                 path.display(),
                                 e
diff --git a/sugarloaf/src/font/mod.rs b/sugarloaf/src/font/mod.rs
index 44c4c7472e..54642f93b2 100644
--- a/sugarloaf/src/font/mod.rs
+++ b/sugarloaf/src/font/mod.rs
@@ -317,7 +317,7 @@ impl FontLibraryData {
                 }
                 FindResult::NotFound(spec) => {
                     // Fallback should not add errors
-                    log::info!("{:?}", spec);
+                    tracing::info!("{:?}", spec);
                 }
             }
         }
@@ -556,7 +556,7 @@ pub type SugarloafFont = fonts::SugarloafFont;
 pub type SugarloafFonts = fonts::SugarloafFonts;
 
 #[cfg(not(target_arch = "wasm32"))]
-use log::{info, warn};
+use tracing::{info, warn};
 
 #[derive(Debug, Clone)]
 pub struct ComposedFontArc {
@@ -608,7 +608,7 @@ fn find_font(db: &crate::font::loader::Database, font_spec: SugarloafFont) -> Fi
                         if file.read_to_end(&mut font_data).is_ok() {
                             match FontData::from_data(font_data) {
                                 Ok(d) => {
-                                    log::info!(
+                                    tracing::info!(
                                         "Font '{}' found in {}",
                                         family,
                                         path.display()
@@ -616,7 +616,7 @@ fn find_font(db: &crate::font::loader::Database, font_spec: SugarloafFont) -> Fi
                                     return FindResult::Found(d);
                                 }
                                 Err(err_message) => {
-                                    log::info!(
+                                    tracing::info!(
                                         "Failed to load font '{query:?}', {err_message}"
                                     );
                                     return FindResult::NotFound(font_spec);
@@ -700,7 +700,7 @@ fn load_from_font_source(path: &PathBuf) -> Option<FontData> {
                     return Some(d);
                 }
                 Err(err_message) => {
-                    log::info!("Failed to load font from source {err_message}");
+                    tracing::info!("Failed to load font from source {err_message}");
                     return None;
                 }
             }
diff --git a/sugarloaf/src/layout/mod.rs b/sugarloaf/src/layout/mod.rs
index a489d55667..deaba9ac57 100644
--- a/sugarloaf/src/layout/mod.rs
+++ b/sugarloaf/src/layout/mod.rs
@@ -228,12 +228,12 @@ impl SugarloafLayout {
         let expected_stack_bound = (self.width / self.dimensions.scale)
             - (self.dimensions.width * self.dimensions.scale);
 
-        log::info!("expected columns {}", self.columns);
+        tracing::info!("expected columns {}", self.columns);
         if current_stack_bound < expected_stack_bound {
             let stack_difference = ((expected_stack_bound - current_stack_bound)
                 / (self.dimensions.width * self.dimensions.scale))
                 as usize;
-            log::info!("recalculating columns due to font width, adding more {stack_difference:?} columns");
+            tracing::info!("recalculating columns due to font width, adding more {stack_difference:?} columns");
             self.columns += stack_difference;
         }
 
@@ -241,7 +241,7 @@ impl SugarloafLayout {
             let stack_difference = ((current_stack_bound - expected_stack_bound)
                 / (self.dimensions.width * self.dimensions.scale))
                 as usize;
-            log::info!("recalculating columns due to font width, removing {stack_difference:?} columns");
+            tracing::info!("recalculating columns due to font width, removing {stack_difference:?} columns");
             self.columns -= stack_difference;
         }
     }
diff --git a/sugarloaf/src/sugarloaf.rs b/sugarloaf/src/sugarloaf.rs
index e69fc50500..74cec06b66 100644
--- a/sugarloaf/src/sugarloaf.rs
+++ b/sugarloaf/src/sugarloaf.rs
@@ -148,7 +148,7 @@ impl Sugarloaf<'_> {
 
     #[inline]
     pub fn update_font(&mut self, font_library: &FontLibrary) {
-        log::info!("requested a font change");
+        tracing::info!("requested a font change");
 
         self.state.reset_compositor();
         self.state.set_fonts(font_library);
diff --git a/sugarloaf/src/sugarloaf/state.rs b/sugarloaf/src/sugarloaf/state.rs
index 1cd064612d..6922754cfb 100644
--- a/sugarloaf/src/sugarloaf/state.rs
+++ b/sugarloaf/src/sugarloaf/state.rs
@@ -196,20 +196,20 @@ impl SugarState {
             let mut dimensions_changed = false;
             if dimension.height != self.current.layout.dimensions.height {
                 self.current.layout.dimensions.height = dimension.height;
-                log::info!("prepare_render: changed height... {}", dimension.height);
+                tracing::info!("prepare_render: changed height... {}", dimension.height);
                 dimensions_changed = true;
             }
 
             if dimension.width != self.current.layout.dimensions.width {
                 self.current.layout.dimensions.width = dimension.width;
                 self.current.layout.update_columns_per_font_width();
-                log::info!("prepare_render: changed width... {}", dimension.width);
+                tracing::info!("prepare_render: changed width... {}", dimension.width);
                 dimensions_changed = true;
             }
 
             if dimensions_changed {
                 self.current.layout.update();
-                log::info!("sugar_state: dimensions has changed");
+                tracing::info!("sugar_state: dimensions has changed");
             }
         }
     }
@@ -227,7 +227,7 @@ impl SugarState {
             self.compositors.advanced.update_layout(&self.current);
 
             self.latest_change = SugarTreeDiff::Repaint;
-            log::info!("has empty dimensions, will try to find...");
+            tracing::info!("has empty dimensions, will try to find...");
             return;
         }
 
@@ -247,7 +247,7 @@ impl SugarState {
             }
         }
 
-        log::info!("state compute_changes result: {:?}", self.latest_change);
+        tracing::info!("state compute_changes result: {:?}", self.latest_change);
 
         if should_update {
             if should_compute_dimensions {
diff --git a/teletypewriter/Cargo.toml b/teletypewriter/Cargo.toml
index b286fc39b2..4b7e9b0d06 100644
--- a/teletypewriter/Cargo.toml
+++ b/teletypewriter/Cargo.toml
@@ -12,7 +12,7 @@ authors = { workspace = true }
 libc = { workspace = true }
 dirs = "5.0.1"
 corcovado = { workspace = true }
-log = { workspace = true }
+tracing = { workspace = true }
 
 [target.'cfg(unix)'.dependencies]
 signal-hook = "0.3.17"
diff --git a/teletypewriter/src/unix/mod.rs b/teletypewriter/src/unix/mod.rs
index a8486bd311..2a45410115 100644
--- a/teletypewriter/src/unix/mod.rs
+++ b/teletypewriter/src/unix/mod.rs
@@ -445,7 +445,7 @@ pub fn create_pty_with_spawn(
         shell_program = &user.shell;
     }
 
-    log::info!("spawn {:?} {:?}", shell_program, args);
+    tracing::info!("spawn {:?} {:?}", shell_program, args);
 
     let mut builder = {
         let mut cmd = Command::new(shell_program);
@@ -602,11 +602,11 @@ pub fn create_pty_with_fork(shell: &str, columns: u16, rows: u16) -> Result<Pty,
     };
 
     if shell.is_empty() {
-        log::info!("shell configuration is empty, will retrieve from env");
+        tracing::info!("shell configuration is empty, will retrieve from env");
         shell_program = &user.shell;
     }
 
-    log::info!("fork {:?}", shell_program);
+    tracing::info!("fork {:?}", shell_program);
 
     match unsafe {
         forkpty(
diff --git a/teletypewriter/src/windows/conpty.rs b/teletypewriter/src/windows/conpty.rs
index b5262f25c6..004071ab9a 100644
--- a/teletypewriter/src/windows/conpty.rs
+++ b/teletypewriter/src/windows/conpty.rs
@@ -1,8 +1,8 @@
 use crate::Winsize;
-use log::*;
 use std::io::{Error, Result};
 use std::os::windows::io::IntoRawHandle;
 use std::{mem, ptr};
+use tracing::*;
 
 use crate::windows::pipes::{EventedAnonRead, EventedAnonWrite};
 
diff --git a/wa/Cargo.toml b/wa/Cargo.toml
index 290de0c08c..e043d09c14 100644
--- a/wa/Cargo.toml
+++ b/wa/Cargo.toml
@@ -10,7 +10,7 @@ description = "Cross-platform window assistant made primarily for Rio terminal"
 
 [dependencies]
 raw-window-handle = { workspace = true }
-log = { workspace = true }
+tracing = { workspace = true }
 bitflags = { workspace = true }
 parking_lot = { workspace = true }
 once_cell = "1.19.0"
diff --git a/wa/src/app/macos.rs b/wa/src/app/macos.rs
index cf7214e37f..221599ba81 100644
--- a/wa/src/app/macos.rs
+++ b/wa/src/app/macos.rs
@@ -57,7 +57,7 @@ impl Default for EventLoopWaker {
 impl EventLoopWaker {
     pub fn stop(&mut self) {
         if self.next_fire_date.is_some() {
-            log::info!("EventLoopWaker stop");
+            tracing::info!("EventLoopWaker stop");
             self.next_fire_date = None;
             unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) }
         }
@@ -65,7 +65,7 @@ impl EventLoopWaker {
 
     pub fn start(&mut self) {
         if self.next_fire_date != Some(self.start_instant) {
-            log::info!("EventLoopWaker start");
+            tracing::info!("EventLoopWaker start");
             self.next_fire_date = Some(self.start_instant);
             unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) }
         }
diff --git a/wa/src/native/macos.rs b/wa/src/native/macos.rs
index 07aeb25215..b663aaefe9 100644
--- a/wa/src/native/macos.rs
+++ b/wa/src/native/macos.rs
@@ -361,7 +361,7 @@ extern "C" fn rio_perform_key_assignment(
     let menu_item = MenuItem::with_menu_item(menu_item);
     // Safe because waPerformKeyAssignment: is only used with KeyAssignment
     let opt_action = menu_item.get_represented_item();
-    log::debug!("rio_perform_key_assignment {opt_action:?}",);
+    tracing::debug!("rio_perform_key_assignment {opt_action:?}",);
     if let Some(action) = opt_action {
         match action {
             RepresentedItem::KeyAssignment(KeyAssignment::SpawnWindow) => {
@@ -396,7 +396,7 @@ extern "C" fn application_did_finish_launching(
     _sel: Sel,
     _notif: *mut Object,
 ) {
-    log::debug!("application_did_finish_launching");
+    tracing::debug!("application_did_finish_launching");
     unsafe {
         let pool: ObjcId = msg_send![class!(NSAutoreleasePool), new];
         (*this).set_ivar("launched", YES);
@@ -552,7 +552,7 @@ fn send_resize_event(payload: &mut MacosDisplay, rescale: bool) {
 #[inline]
 unsafe fn view_base_decl(decl: &mut ClassDecl) {
     extern "C" fn mouse_moved(this: &Object, _sel: Sel, event: ObjcId) {
-        log::info!("mouse_moved");
+        tracing::info!("mouse_moved");
 
         if let Some(payload) = get_display_payload(this) {
             unsafe {
@@ -589,7 +589,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     fn fire_mouse_event(this: &Object, event: ObjcId, down: bool, btn: MouseButton) {
-        log::info!("fire_mouse_event");
+        tracing::info!("fire_mouse_event");
         if let Some(payload) = get_display_payload(this) {
             unsafe {
                 let point: NSPoint = msg_send!(event, locationInWindow);
@@ -619,7 +619,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, _a_selector: Sel) {
-        log::info!("do_command_by_selector");
+        tracing::info!("do_command_by_selector");
         if let Some(payload) = get_display_payload(this) {
             if payload.ime == ImeState::Commited {
                 return;
@@ -632,7 +632,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn has_marked_text(this: &Object, _sel: Sel) -> BOOL {
-        log::info!("has_marked_text");
+        tracing::info!("has_marked_text");
         if let Some(payload) = get_display_payload(this) {
             if !payload.marked_text.is_empty() {
                 YES
@@ -645,7 +645,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn marked_range(this: &Object, _sel: Sel) -> NSRange {
-        log::info!("marked_range");
+        tracing::info!("marked_range");
         if let Some(payload) = get_display_payload(this) {
             if !payload.marked_text.is_empty() {
                 NSRange::new(0, payload.marked_text.len() as u64)
@@ -658,7 +658,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn selected_range(_this: &Object, _sel: Sel) -> NSRange {
-        log::info!("selected_range");
+        tracing::info!("selected_range");
         NSRange {
             location: 0,
             length: 1,
@@ -672,7 +672,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
         astring: ObjcId,
         _replacement_range: NSRange,
     ) {
-        log::info!("insert_text_replacement_range");
+        tracing::info!("insert_text_replacement_range");
         let string = nsstring_to_string(astring);
         let is_control = string.chars().next().map_or(false, |c| c.is_control());
 
@@ -708,7 +708,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
         _selected_range: NSRange,
         _replacement_range: NSRange,
     ) {
-        log::info!("set_marked_text_selected_range_replacement_range");
+        tracing::info!("set_marked_text_selected_range_replacement_range");
         let s = nsstring_to_string(astring);
 
         if let Some(payload) = get_display_payload(this) {
@@ -750,7 +750,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn unmark_text(this: &Object, _sel: Sel) {
-        log::info!("unmark_text");
+        tracing::info!("unmark_text");
         if let Some(payload) = get_display_payload(this) {
             payload.marked_text.clear();
             payload.ime = ImeState::Ground;
@@ -767,7 +767,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
         _sel: Sel,
         _point: NSPoint,
     ) -> NSUInteger {
-        log::info!("character_index_for_point");
+        tracing::info!("character_index_for_point");
         NSNOT_FOUND as _
     }
 
@@ -777,7 +777,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
         _range: NSRange,
         _actual: *mut c_void,
     ) -> NSRect {
-        log::info!("first_rect_for_character_range");
+        tracing::info!("first_rect_for_character_range");
 
         // Returns a rect in screen coordinates; this is used to place
         // the input method editor
@@ -809,7 +809,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn valid_attributes_for_marked_text(_this: &Object, _sel: Sel) -> ObjcId {
-        log::info!("valid_attributes_for_marked_text");
+        tracing::info!("valid_attributes_for_marked_text");
         // FIXME: returns NSArray<NSAttributedStringKey> *
         let content: &[ObjcId; 0] = &[];
         unsafe {
@@ -830,31 +830,31 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn mouse_down(this: &Object, _sel: Sel, event: ObjcId) {
-        log::info!("mouse_down");
+        tracing::info!("mouse_down");
         fire_mouse_event(this, event, true, MouseButton::Left);
     }
     extern "C" fn mouse_up(this: &Object, _sel: Sel, event: ObjcId) {
-        log::info!("mouse_up");
+        tracing::info!("mouse_up");
         fire_mouse_event(this, event, false, MouseButton::Left);
     }
     extern "C" fn right_mouse_down(this: &Object, _sel: Sel, event: ObjcId) {
-        log::info!("right_mouse_down");
+        tracing::info!("right_mouse_down");
         fire_mouse_event(this, event, true, MouseButton::Right);
     }
     extern "C" fn right_mouse_up(this: &Object, _sel: Sel, event: ObjcId) {
-        log::info!("right_mouse_up");
+        tracing::info!("right_mouse_up");
         fire_mouse_event(this, event, false, MouseButton::Right);
     }
     extern "C" fn other_mouse_down(this: &Object, _sel: Sel, event: ObjcId) {
-        log::info!("other_mouse_down");
+        tracing::info!("other_mouse_down");
         fire_mouse_event(this, event, true, MouseButton::Middle);
     }
     extern "C" fn other_mouse_up(this: &Object, _sel: Sel, event: ObjcId) {
-        log::info!("other_mouse_up");
+        tracing::info!("other_mouse_up");
         fire_mouse_event(this, event, false, MouseButton::Middle);
     }
     extern "C" fn scroll_wheel(this: &Object, _sel: Sel, event: ObjcId) {
-        log::info!("scroll_wheel");
+        tracing::info!("scroll_wheel");
         if let Some(payload) = get_display_payload(this) {
             unsafe {
                 let mut dx: f64 = msg_send![event, scrollingDeltaX];
@@ -875,7 +875,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
         }
     }
     extern "C" fn window_did_become_key(this: &Object, _sel: Sel, _event: ObjcId) {
-        log::info!("window_did_become_key");
+        tracing::info!("window_did_become_key");
         if let Some(payload) = get_display_payload(this) {
             unsafe {
                 if let Some(app_state) = get_app_state(&*payload.app) {
@@ -892,7 +892,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
         }
     }
     extern "C" fn window_did_resign_key(this: &Object, _sel: Sel, _event: ObjcId) {
-        log::info!("window_did_resign_key");
+        tracing::info!("window_did_resign_key");
         if let Some(payload) = get_display_payload(this) {
             unsafe {
                 if let Some(app_state) = get_app_state(&*payload.app) {
@@ -909,7 +909,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
         }
     }
     extern "C" fn reset_cursor_rects(this: &Object, _sel: Sel) {
-        log::info!("reset_cursor_rects");
+        tracing::info!("reset_cursor_rects");
         if let Some(payload) = get_display_payload(this) {
             unsafe {
                 let cursor_id = {
@@ -933,7 +933,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn dragging_entered(this: &Object, _: Sel, sender: ObjcId) -> BOOL {
-        log::info!("dragging_entered");
+        tracing::info!("dragging_entered");
         if let Some(payload) = get_display_payload(this) {
             unsafe {
                 let pboard: ObjcId = msg_send![sender, draggingPasteboard];
@@ -965,7 +965,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn dragging_exited(this: &Object, _: Sel, _sender: ObjcId) {
-        log::info!("dragging_exited");
+        tracing::info!("dragging_exited");
         if let Some(payload) = get_display_payload(this) {
             if let Some(&mut HandlerState::Running {
                 ref mut handler, ..
@@ -977,7 +977,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn perform_drag_operation(this: &Object, _: Sel, sender: ObjcId) -> BOOL {
-        log::info!("perform_drag_operation");
+        tracing::info!("perform_drag_operation");
         if let Some(payload) = get_display_payload(this) {
             unsafe {
                 let pboard: ObjcId = msg_send![sender, draggingPasteboard];
@@ -1005,7 +1005,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn window_should_close(this: &Object, _: Sel, _: ObjcId) -> BOOL {
-        log::info!("window_should_close");
+        tracing::info!("window_should_close");
         let payload = get_display_payload(this);
 
         if payload.is_none() {
@@ -1054,39 +1054,39 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn window_did_resize(this: &Object, _: Sel, _: ObjcId) {
-        log::info!("window_did_resize");
+        tracing::info!("window_did_resize");
         if let Some(payload) = get_display_payload(this) {
             send_resize_event(payload, false);
         }
     }
     extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: ObjcId) {
-        log::info!("window_did_change_screen");
+        tracing::info!("window_did_change_screen");
         if let Some(payload) = get_display_payload(this) {
             send_resize_event(payload, true);
         }
     }
     extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: ObjcId) {
-        log::info!("window_did_enter_fullscreen");
+        tracing::info!("window_did_enter_fullscreen");
         if let Some(payload) = get_display_payload(this) {
             payload.fullscreen = true;
         }
     }
     extern "C" fn window_did_exit_fullscreen(this: &Object, _: Sel, _: ObjcId) {
-        log::info!("window_did_exit_fullscreen");
+        tracing::info!("window_did_exit_fullscreen");
         if let Some(payload) = get_display_payload(this) {
             payload.fullscreen = false;
         }
     }
 
     extern "C" fn key_down(this: &Object, _sel: Sel, event: ObjcId) {
-        log::info!("key_down");
+        tracing::info!("key_down");
         if let Some(payload) = get_display_payload(this) {
             let repeat: bool = unsafe { msg_send!(event, isARepeat) };
             let unmod = unsafe { msg_send!(event, charactersIgnoringModifiers) };
             let unmod = nsstring_to_string(unmod);
             let chars = get_event_char(event);
 
-            log::info!("KEY_DOWN (chars={:?} unmod={:?}", chars, unmod,);
+            tracing::info!("KEY_DOWN (chars={:?} unmod={:?}", chars, unmod,);
 
             let old_ime = &payload.ime;
             // unmod is differently depending of the keymap used, for example if you
@@ -1136,7 +1136,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     }
 
     extern "C" fn appearance_did_change(this: &Object, _sel: Sel, _app: ObjcId) {
-        log::info!("appearance_did_change");
+        tracing::info!("appearance_did_change");
         if let Some(payload) = get_display_payload(this) {
             if let Some(&mut HandlerState::Running {
                 ref mut handler, ..
@@ -1150,7 +1150,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
     extern "C" fn key_up(this: &Object, _sel: Sel, event: ObjcId) {
         if let Some(payload) = get_display_payload(this) {
             if let Some(key) = get_event_keycode(event) {
-                log::info!("KEY_UP (key={:?}", key);
+                tracing::info!("KEY_UP (key={:?}", key);
                 if let Some(&mut HandlerState::Running {
                     ref mut handler, ..
                 }) = get_app_handler(&Some(payload.app))
@@ -1418,7 +1418,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) {
 
 #[inline]
 extern "C" fn draw_rect(this: &Object, _sel: Sel, _rect: NSRect) {
-    log::info!("draw_rect");
+    tracing::info!("draw_rect");
     if let Some(payload) = get_display_payload(this) {
         if !payload.has_initialized {
             unsafe { payload.update_dimensions() };
@@ -1491,7 +1491,7 @@ pub fn define_metal_view_class(
     // }
 
     // extern "C" fn update_layer(_this: &mut Object, _sel: Sel) {
-    //     log::trace!("update_layer called");
+    //     tracing::trace!("update_layer called");
     // }
 
     // extern "C" fn timer_fired(_this: &Object, _sel: Sel, _: ObjcId) {
@@ -1519,7 +1519,7 @@ pub fn define_metal_view_class(
     }
 
     extern "C" fn make_backing_layer(this: &mut Object, _: Sel) -> ObjcId {
-        log::trace!("make_backing_layer");
+        tracing::trace!("make_backing_layer");
         let class = class!(CAMetalLayer);
         unsafe {
             let layer: ObjcId = msg_send![class, new];
@@ -1999,7 +1999,7 @@ impl App {
                 let appearance: ObjcId = msg_send![*app.inner, effectiveAppearance];
                 nsstring_to_string(msg_send![appearance, name])
             };
-            log::info!("App Appearance is {name}");
+            tracing::info!("App Appearance is {name}");
             match name.as_str() {
                 "NSAppearanceNameVibrantDark" | "NSAppearanceNameDarkAqua" => {
                     Appearance::Dark
@@ -2016,7 +2016,7 @@ impl App {
                     Appearance::DarkHighContrast
                 }
                 _ => {
-                    log::warn!("Unknown NSAppearanceName {name}, assume Light");
+                    tracing::warn!("Unknown NSAppearanceName {name}, assume Light");
                     Appearance::Light
                 }
             }


Test patch:
diff --git a/corcovado/test/mod.rs b/corcovado/test/mod.rs
index 6af9ddfd9a..6defc62795 100644
--- a/corcovado/test/mod.rs
+++ b/corcovado/test/mod.rs
@@ -2,10 +2,10 @@ extern crate bytes;
 extern crate corcovado;
 extern crate env_logger;
 extern crate iovec;
-extern crate log;
 extern crate net2;
 extern crate slab;
 extern crate tempdir;
+extern crate tracing;
 
 #[cfg(target_os = "fuchsia")]
 extern crate fuchsia_zircon as zircon;
diff --git a/sugarloaf/tests/test_example_text.rs b/sugarloaf/tests/test_example_text.rs
index 9668cab473..76eafc5a41 100644
--- a/sugarloaf/tests/test_example_text.rs
+++ b/sugarloaf/tests/test_example_text.rs
@@ -329,7 +329,7 @@ async fn pass() {
     create_html_canvas();
 
     std::panic::set_hook(Box::new(console_error_panic_hook::hook));
-    console_log::init().expect("could not initialize logger");
+    console_tracing::init().expect("could not initialize logger");
     wasm_bindgen_futures::spawn_local(run());
 
     // assert_eq!(1 + 1, 2);
diff --git a/sugarloaf/tests/util/image.rs b/sugarloaf/tests/util/image.rs
index 18784a01c3..15581ed853 100644
--- a/sugarloaf/tests/util/image.rs
+++ b/sugarloaf/tests/util/image.rs
@@ -8,7 +8,7 @@ fn read_png(path: impl AsRef<Path>, width: u32, height: u32) -> Option<Vec<u8>>
     let data = match std::fs::read(&path) {
         Ok(f) => f,
         Err(e) => {
-            log::warn!(
+            tracing::warn!(
                 "image comparison invalid: file io error when comparing {}: {}",
                 path.as_ref().display(),
                 e
@@ -22,19 +22,19 @@ fn read_png(path: impl AsRef<Path>, width: u32, height: u32) -> Option<Vec<u8>>
     let mut buffer = vec![0; reader.output_buffer_size()];
     let info = reader.next_frame(&mut buffer).ok()?;
     if info.width != width {
-        log::warn!("image comparison invalid: size mismatch");
+        tracing::warn!("image comparison invalid: size mismatch");
         return None;
     }
     if info.height != height {
-        log::warn!("image comparison invalid: size mismatch");
+        tracing::warn!("image comparison invalid: size mismatch");
         return None;
     }
     if info.color_type != png::ColorType::Rgba {
-        log::warn!("image comparison invalid: color type mismatch");
+        tracing::warn!("image comparison invalid: color type mismatch");
         return None;
     }
     if info.bit_depth != png::BitDepth::Eight {
-        log::warn!("image comparison invalid: bit depth mismatch");
+        tracing::warn!("image comparison invalid: bit depth mismatch");
         return None;
     }