summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBrendan Allan <[email protected]>2026-02-11 19:36:27 +0800
committerGitHub <[email protected]>2026-02-11 11:36:27 +0000
commita25b2af05aa4fe0cc2bc3474a0b5e23eca3efe4f (patch)
tree0097ed5f15eb009b3ab1f5d235884d4733283166
parent8bfd6fdba2490aa41ef9533e3301f0272711d489 (diff)
downloadopencode-a25b2af05aa4fe0cc2bc3474a0b5e23eca3efe4f.tar.gz
opencode-a25b2af05aa4fe0cc2bc3474a0b5e23eca3efe4f.zip
desktop: use tracing for logging (#13135)
-rw-r--r--packages/desktop/src-tauri/Cargo.lock102
-rw-r--r--packages/desktop/src-tauri/Cargo.toml4
-rw-r--r--packages/desktop/src-tauri/src/cli.rs104
-rw-r--r--packages/desktop/src-tauri/src/constants.rs1
-rw-r--r--packages/desktop/src-tauri/src/job_object.rs12
-rw-r--r--packages/desktop/src-tauri/src/lib.rs83
-rw-r--r--packages/desktop/src-tauri/src/linux_display.rs15
-rw-r--r--packages/desktop/src-tauri/src/logging.rs83
-rw-r--r--packages/desktop/src-tauri/src/main.rs4
-rw-r--r--packages/desktop/src-tauri/src/server.rs10
10 files changed, 264 insertions, 154 deletions
diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock
index 537a7c9c5..8ce97b2b7 100644
--- a/packages/desktop/src-tauri/Cargo.lock
+++ b/packages/desktop/src-tauri/Cargo.lock
@@ -535,8 +535,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
+ "js-sys",
"num-traits",
"serde",
+ "wasm-bindgen",
"windows-link 0.2.1",
]
@@ -2492,6 +2494,15 @@ dependencies = [
]
[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
name = "matches"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2692,6 +2703,15 @@ dependencies = [
]
[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3065,6 +3085,7 @@ dependencies = [
name = "opencode-desktop"
version = "0.0.0"
dependencies = [
+ "chrono",
"comrak",
"dirs",
"futures",
@@ -3096,6 +3117,9 @@ dependencies = [
"tauri-plugin-window-state",
"tauri-specta",
"tokio",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
"uuid",
"webkit2gtk",
"windows 0.61.3",
@@ -4413,6 +4437,15 @@ dependencies = [
]
[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
name = "shared_child"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5473,6 +5506,15 @@ dependencies = [
]
[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "tiff"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5745,9 +5787,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
-version = "0.1.41"
+version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@@ -5755,10 +5797,22 @@ dependencies = [
]
[[package]]
+name = "tracing-appender"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
+dependencies = [
+ "crossbeam-channel",
+ "thiserror 2.0.17",
+ "time",
+ "tracing-subscriber",
+]
+
+[[package]]
name = "tracing-attributes"
-version = "0.1.30"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -5767,11 +5821,41 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.34"
+version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
]
[[package]]
@@ -5965,6 +6049,12 @@ dependencies = [
]
[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
name = "version-compare"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml
index 2d6f13eca..e9ba55b03 100644
--- a/packages/desktop/src-tauri/Cargo.toml
+++ b/packages/desktop/src-tauri/Cargo.toml
@@ -47,6 +47,10 @@ specta = "=2.0.0-rc.22"
specta-typescript = "0.0.9"
tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] }
dirs = "6.0.0"
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+tracing-appender = "0.2"
+chrono = "0.4"
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18.2"
diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs
index 70ac973f0..b9e1ed4bd 100644
--- a/packages/desktop/src-tauri/src/cli.rs
+++ b/packages/desktop/src-tauri/src/cli.rs
@@ -6,10 +6,7 @@ use tauri_plugin_shell::{
use tauri_plugin_store::StoreExt;
use tokio::sync::oneshot;
-use crate::{
- LogState,
- constants::{MAX_LOG_ENTRIES, SETTINGS_STORE, WSL_ENABLED_KEY},
-};
+use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY};
const CLI_INSTALL_DIR: &str = ".opencode/bin";
const CLI_BINARY_NAME: &str = "opencode";
@@ -29,7 +26,7 @@ pub async fn get_config(app: &AppHandle) -> Option<Config> {
create_command(app, "debug config", &[])
.output()
.await
- .inspect_err(|e| eprintln!("Failed to read OC config: {e}"))
+ .inspect_err(|e| tracing::warn!("Failed to read OC config: {e}"))
.ok()
.and_then(|out| String::from_utf8(out.stdout.to_vec()).ok())
.and_then(|s| serde_json::from_str::<Config>(&s).ok())
@@ -104,12 +101,12 @@ pub fn install_cli(app: tauri::AppHandle) -> Result<String, String> {
pub fn sync_cli(app: tauri::AppHandle) -> Result<(), String> {
if cfg!(debug_assertions) {
- println!("Skipping CLI sync for debug build");
+ tracing::debug!("Skipping CLI sync for debug build");
return Ok(());
}
if !is_cli_installed() {
- println!("No CLI installation found, skipping sync");
+ tracing::info!("No CLI installation found, skipping sync");
return Ok(());
}
@@ -132,21 +129,21 @@ pub fn sync_cli(app: tauri::AppHandle) -> Result<(), String> {
let app_version = app.package_info().version.clone();
if cli_version >= app_version {
- println!(
- "CLI version {} is up to date (app version: {}), skipping sync",
- cli_version, app_version
+ tracing::info!(
+ %cli_version, %app_version,
+ "CLI is up to date, skipping sync"
);
return Ok(());
}
- println!(
- "CLI version {} is older than app version {}, syncing",
- cli_version, app_version
+ tracing::info!(
+ %cli_version, %app_version,
+ "CLI is older than app version, syncing"
);
install_cli(app)?;
- println!("Synced installed CLI");
+ tracing::info!("Synced installed CLI");
Ok(())
}
@@ -207,7 +204,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St
if cfg!(windows) {
if is_wsl_enabled(app) {
- println!("WSL is enabled, spawning CLI server in WSL.");
+ tracing::info!("WSL is enabled, spawning CLI server in WSL");
let version = app.package_info().version.to_string();
let mut script = vec![
"set -e".to_string(),
@@ -280,38 +277,9 @@ pub fn serve(
port: u32,
password: &str,
) -> (CommandChild, oneshot::Receiver<TerminatedPayload>) {
- let log_state = app.state::<LogState>();
- let log_state_clone = log_state.inner().clone();
-
let (exit_tx, exit_rx) = oneshot::channel::<TerminatedPayload>();
- println!("spawning sidecar on port {port}");
-
- if let Ok(mut logs) = log_state_clone.0.lock() {
- let args =
- format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}");
-
- #[cfg(target_os = "windows")]
- {
- logs.push_back(format!("[SPAWN] sidecar=opencode-cli args=\"{args}\"\n"));
- }
-
- #[cfg(not(target_os = "windows"))]
- {
- let sidecar = get_sidecar_path(app);
- let shell = get_user_shell();
- let cmd = if shell.ends_with("/nu") {
- format!("^\"{}\" {}", sidecar.display(), args)
- } else {
- format!("\"{}\" {}", sidecar.display(), args)
- };
- logs.push_back(format!("[SPAWN] shell=\"{shell}\" argv=\"-il -c {cmd}\"\n"));
- }
-
- while logs.len() > MAX_LOG_ENTRIES {
- logs.pop_front();
- }
- }
+ tracing::info!(port, "Spawning sidecar");
let envs = [
("OPENCODE_SERVER_USERNAME", "opencode".to_string()),
@@ -332,50 +300,22 @@ pub fn serve(
match event {
CommandEvent::Stdout(line_bytes) => {
let line = String::from_utf8_lossy(&line_bytes);
- print!("{line}");
-
- // Store log in shared state
- if let Ok(mut logs) = log_state_clone.0.lock() {
- logs.push_back(format!("[STDOUT] {}", line));
- // Keep only the last MAX_LOG_ENTRIES
- while logs.len() > MAX_LOG_ENTRIES {
- logs.pop_front();
- }
- }
+ tracing::info!(target: "sidecar", "{line}");
}
CommandEvent::Stderr(line_bytes) => {
let line = String::from_utf8_lossy(&line_bytes);
- eprint!("{line}");
-
- // Store log in shared state
- if let Ok(mut logs) = log_state_clone.0.lock() {
- logs.push_back(format!("[STDERR] {}", line));
- // Keep only the last MAX_LOG_ENTRIES
- while logs.len() > MAX_LOG_ENTRIES {
- logs.pop_front();
- }
- }
+ tracing::info!(target: "sidecar", "{line}");
}
CommandEvent::Error(err) => {
- eprintln!("{err}");
-
- if let Ok(mut logs) = log_state_clone.0.lock() {
- logs.push_back(format!("[ERROR] {err}\n"));
- while logs.len() > MAX_LOG_ENTRIES {
- logs.pop_front();
- }
- }
+ tracing::error!(target: "sidecar", "{err}");
}
CommandEvent::Terminated(payload) => {
- if let Ok(mut logs) = log_state_clone.0.lock() {
- logs.push_back(format!(
- "[EXIT] code={:?} signal={:?}\n",
- payload.code, payload.signal
- ));
- while logs.len() > MAX_LOG_ENTRIES {
- logs.pop_front();
- }
- }
+ tracing::info!(
+ target: "sidecar",
+ code = ?payload.code,
+ signal = ?payload.signal,
+ "Sidecar terminated"
+ );
if let Some(tx) = exit_tx.take() {
let _ = tx.send(payload);
diff --git a/packages/desktop/src-tauri/src/constants.rs b/packages/desktop/src-tauri/src/constants.rs
index cdf05fb45..9d50d00e2 100644
--- a/packages/desktop/src-tauri/src/constants.rs
+++ b/packages/desktop/src-tauri/src/constants.rs
@@ -4,7 +4,6 @@ pub const SETTINGS_STORE: &str = "opencode.settings.dat";
pub const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl";
pub const WSL_ENABLED_KEY: &str = "wslEnabled";
pub const UPDATER_ENABLED: bool = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
-pub const MAX_LOG_ENTRIES: usize = 200;
pub fn window_state_flags() -> StateFlags {
StateFlags::all() - StateFlags::DECORATIONS - StateFlags::VISIBLE
diff --git a/packages/desktop/src-tauri/src/job_object.rs b/packages/desktop/src-tauri/src/job_object.rs
index 220aa5db6..8d774b14c 100644
--- a/packages/desktop/src-tauri/src/job_object.rs
+++ b/packages/desktop/src-tauri/src/job_object.rs
@@ -15,9 +15,9 @@ use std::io::{Error, Result};
use std::sync::Mutex;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::JobObjects::{
- AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
- JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
- SetInformationJobObject,
+ AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
+ SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
+ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE};
@@ -111,7 +111,7 @@ impl JobObjectState {
error: Mutex::new(None),
},
Err(e) => {
- eprintln!("Failed to create job object: {e}");
+ tracing::error!("Failed to create job object: {e}");
Self {
job: Mutex::new(None),
error: Mutex::new(Some(format!("Failed to create job object: {e}"))),
@@ -123,11 +123,11 @@ impl JobObjectState {
pub fn assign_pid(&self, pid: u32) {
if let Some(job) = self.job.lock().unwrap().as_ref() {
if let Err(e) = job.assign_pid(pid) {
- eprintln!("Failed to assign process {pid} to job object: {e}");
+ tracing::error!(pid, "Failed to assign process to job object: {e}");
*self.error.lock().unwrap() =
Some(format!("Failed to assign process to job object: {e}"));
} else {
- println!("Assigned process {pid} to job object for automatic cleanup");
+ tracing::info!(pid, "Assigned process to job object for automatic cleanup");
}
}
}
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index 3e7902804..2c570b7a7 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/packages/desktop/src-tauri/src/lib.rs
@@ -4,6 +4,7 @@ mod constants;
mod job_object;
#[cfg(target_os = "linux")]
pub mod linux_display;
+mod logging;
mod markdown;
mod server;
mod window_customizer;
@@ -16,7 +17,6 @@ use futures::{
#[cfg(windows)]
use job_object::*;
use std::{
- collections::VecDeque,
env,
net::TcpListener,
path::PathBuf,
@@ -85,14 +85,11 @@ impl ServerState {
}
}
-#[derive(Clone)]
-struct LogState(Arc<Mutex<VecDeque<String>>>);
-
#[tauri::command]
#[specta::specta]
fn kill_sidecar(app: AppHandle) {
let Some(server_state) = app.try_state::<ServerState>() else {
- println!("Server not running");
+ tracing::info!("Server not running");
return;
};
@@ -102,24 +99,17 @@ fn kill_sidecar(app: AppHandle) {
.expect("Failed to acquire mutex lock")
.take()
else {
- println!("Server state missing");
+ tracing::info!("Server state missing");
return;
};
let _ = server_state.kill();
- println!("Killed server");
+ tracing::info!("Killed server");
}
-async fn get_logs(app: AppHandle) -> Result<String, String> {
- let log_state = app.try_state::<LogState>().ok_or("Log state not found")?;
-
- let logs = log_state
- .0
- .lock()
- .map_err(|_| "Failed to acquire log lock")?;
-
- Ok(logs.iter().cloned().collect::<Vec<_>>().join(""))
+fn get_logs() -> String {
+ logging::tail()
}
#[tauri::command]
@@ -715,10 +705,18 @@ pub fn run() {
.plugin(tauri_plugin_decorum::init())
.invoke_handler(builder.invoke_handler())
.setup(move |app| {
- let app = app.handle().clone();
+ let handle = app.handle().clone();
+
+ let log_dir = app
+ .path()
+ .app_log_dir()
+ .expect("failed to resolve app log dir");
+ // Hold the guard in managed state so it lives for the app's lifetime,
+ // ensuring all buffered logs are flushed on shutdown.
+ handle.manage(logging::init(&log_dir));
- builder.mount_events(&app);
- tauri::async_runtime::spawn(initialize(app));
+ builder.mount_events(&handle);
+ tauri::async_runtime::spawn(initialize(handle));
Ok(())
});
@@ -732,7 +730,7 @@ pub fn run() {
.expect("error while running tauri application")
.run(|app, event| {
if let RunEvent::Exit = event {
- println!("Received Exit");
+ tracing::info!("Received Exit");
kill_sidecar(app.clone());
}
@@ -780,9 +778,8 @@ fn test_export_types() {
#[derive(tauri_specta::Event, serde::Deserialize, specta::Type)]
struct LoadingWindowComplete;
-// #[tracing::instrument(skip_all)]
async fn initialize(app: AppHandle) {
- println!("Initializing app");
+ tracing::info!("Initializing app");
let (init_tx, init_rx) = watch::channel(InitStep::ServerWaiting);
@@ -795,7 +792,7 @@ async fn initialize(app: AppHandle) {
let loading_window_complete = event_once_fut::<LoadingWindowComplete>(&app);
- println!("Main and loading windows created");
+ tracing::info!("Main and loading windows created");
let sqlite_enabled = option_env!("OPENCODE_SQLITE").is_some();
@@ -806,7 +803,7 @@ async fn initialize(app: AppHandle) {
async move {
let mut sqlite_exists = sqlite_file_exists();
- println!("Setting up server connection");
+ tracing::info!("Setting up server connection");
let server_connection = setup_server_connection(app.clone()).await;
// we delay spawning this future so that the timeout is created lazily
@@ -831,16 +828,13 @@ async fn initialize(app: AppHandle) {
if let Some(err) = err {
let _ = child.kill();
- let logs = get_logs(app.clone())
- .await
- .unwrap_or_else(|e| format!("[DESKTOP] Failed to read sidecar logs: {e}\n"));
-
return Err(format!(
- "Failed to spawn OpenCode Server ({err}). Logs:\n{logs}"
+ "Failed to spawn OpenCode Server ({err}). Logs:\n{}",
+ get_logs()
));
}
- println!("CLI health check OK");
+ tracing::info!("CLI health check OK");
#[cfg(windows)]
{
@@ -868,11 +862,11 @@ async fn initialize(app: AppHandle) {
if let Some(cli_health_check) = cli_health_check {
if sqlite_enabled {
- println!("Does sqlite file exist: {sqlite_exists}");
+ tracing::debug!(sqlite_exists, "Checking sqlite file existence");
if !sqlite_exists {
- println!(
- "Sqlite file not found at {}, waiting for it to be generated",
- opencode_db_path().expect("failed to get db path").display()
+ tracing::info!(
+ path = %opencode_db_path().expect("failed to get db path").display(),
+ "Sqlite file not found, waiting for it to be generated"
);
let _ = init_tx.send(InitStep::SqliteWaiting);
@@ -897,7 +891,7 @@ async fn initialize(app: AppHandle) {
.await
.is_err()
{
- println!("Loading task timed out, showing loading window");
+ tracing::debug!("Loading task timed out, showing loading window");
let app = app.clone();
let loading_window = LoadingWindow::create(&app).expect("Failed to create loading window");
sleep(Duration::from_secs(1)).await;
@@ -910,14 +904,14 @@ async fn initialize(app: AppHandle) {
let _ = loading_task.await;
- println!("Loading done, completing initialisation");
+ tracing::info!("Loading done, completing initialisation");
let _ = init_tx.send(InitStep::Done);
if loading_window.is_some() {
loading_window_complete.await;
- println!("Loading window completed");
+ tracing::info!("Loading window completed");
}
MainWindow::create(&app).expect("Failed to create main window");
@@ -931,9 +925,6 @@ fn setup_app(app: &tauri::AppHandle, init_rx: watch::Receiver<InitStep>) {
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
app.deep_link().register_all().ok();
- // Initialize log state
- app.manage(LogState(Arc::new(Mutex::new(VecDeque::new()))));
-
#[cfg(windows)]
app.manage(JobObjectState::new());
@@ -943,7 +934,7 @@ fn setup_app(app: &tauri::AppHandle, init_rx: watch::Receiver<InitStep>) {
fn spawn_cli_sync_task(app: AppHandle) {
tokio::spawn(async move {
if let Err(e) = sync_cli(app) {
- eprintln!("Failed to sync CLI: {e}");
+ tracing::error!("Failed to sync CLI: {e}");
}
});
}
@@ -963,12 +954,12 @@ enum ServerConnection {
async fn setup_server_connection(app: AppHandle) -> ServerConnection {
let custom_url = get_saved_server_url(&app).await;
- println!("Attempting server connection to custom url: {custom_url:?}");
+ tracing::info!(?custom_url, "Attempting server connection");
if let Some(url) = custom_url
&& server::check_health_or_ask_retry(&app, &url).await
{
- println!("Connected to custom server: {}", url);
+ tracing::info!(%url, "Connected to custom server");
return ServerConnection::Existing { url: url.clone() };
}
@@ -976,15 +967,15 @@ async fn setup_server_connection(app: AppHandle) -> ServerConnection {
let hostname = "127.0.0.1";
let local_url = format!("http://{hostname}:{local_port}");
- println!("Checking health of server '{}'", local_url);
+ tracing::debug!(url = %local_url, "Checking health of local server");
if server::check_health(&local_url, None).await {
- println!("Health check OK, using existing server");
+ tracing::info!(url = %local_url, "Health check OK, using existing server");
return ServerConnection::Existing { url: local_url };
}
let password = uuid::Uuid::new_v4().to_string();
- println!("Spawning new local server");
+ tracing::info!("Spawning new local server");
let (child, health_check) =
server::spawn_local_server(app, hostname.to_string(), local_port, password.clone());
diff --git a/packages/desktop/src-tauri/src/linux_display.rs b/packages/desktop/src-tauri/src/linux_display.rs
index 24fb27a4b..0179cf8bb 100644
--- a/packages/desktop/src-tauri/src/linux_display.rs
+++ b/packages/desktop/src-tauri/src/linux_display.rs
@@ -14,7 +14,11 @@ struct DisplayConfig {
}
fn dir() -> Option<PathBuf> {
- Some(dirs::data_dir()?.join(if cfg!(debug_assertions) { "ai.opencode.desktop.dev" } else { "ai.opencode.desktop" }))
+ Some(dirs::data_dir()?.join(if cfg!(debug_assertions) {
+ "ai.opencode.desktop.dev"
+ } else {
+ "ai.opencode.desktop"
+ }))
}
fn path() -> Option<PathBuf> {
@@ -22,13 +26,12 @@ fn path() -> Option<PathBuf> {
}
pub fn read_wayland() -> Option<bool> {
- let raw = std::fs::read_to_string(dbg!(path()?)).ok()?;
+ let raw = std::fs::read_to_string(path()?).ok()?;
let root = serde_json::from_str::<serde_json::Value>(&raw)
.ok()?
- .get(LINUX_DISPLAY_CONFIG_KEY).cloned()?;
- serde_json::from_value::<DisplayConfig>(root)
- .ok()?
- .wayland
+ .get(LINUX_DISPLAY_CONFIG_KEY)
+ .cloned()?;
+ serde_json::from_value::<DisplayConfig>(root).ok()?.wayland
}
pub fn write_wayland(app: &AppHandle, value: bool) -> Result<(), String> {
diff --git a/packages/desktop/src-tauri/src/logging.rs b/packages/desktop/src-tauri/src/logging.rs
new file mode 100644
index 000000000..f794f9c1b
--- /dev/null
+++ b/packages/desktop/src-tauri/src/logging.rs
@@ -0,0 +1,83 @@
+use std::fs::File;
+use std::io::{BufRead, BufReader};
+use std::path::{Path, PathBuf};
+use tracing_appender::non_blocking::WorkerGuard;
+use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
+
+const MAX_LOG_AGE_DAYS: u64 = 7;
+const TAIL_LINES: usize = 1000;
+
+static LOG_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
+
+pub fn init(log_dir: &Path) -> WorkerGuard {
+ std::fs::create_dir_all(log_dir).expect("failed to create log directory");
+
+ cleanup(log_dir);
+
+ let timestamp = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S");
+ let filename = format!("opencode-desktop_{timestamp}.log");
+ let log_path = log_dir.join(&filename);
+
+ LOG_PATH
+ .set(log_path.clone())
+ .expect("logging already initialized");
+
+ let file = File::create(&log_path).expect("failed to create log file");
+ let (non_blocking, guard) = tracing_appender::non_blocking(file);
+
+ let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
+ if cfg!(debug_assertions) {
+ EnvFilter::new("opencode_lib=debug,opencode_desktop=debug,sidecar=debug")
+ } else {
+ EnvFilter::new("opencode_lib=info,opencode_desktop=info,sidecar=info")
+ }
+ });
+
+ tracing_subscriber::registry()
+ .with(filter)
+ .with(fmt::layer().with_writer(std::io::stderr))
+ .with(
+ fmt::layer()
+ .with_writer(non_blocking)
+ .with_ansi(false),
+ )
+ .init();
+
+ guard
+}
+
+pub fn tail() -> String {
+ let Some(path) = LOG_PATH.get() else {
+ return String::new();
+ };
+
+ let Ok(file) = File::open(path) else {
+ return String::new();
+ };
+
+ let lines: Vec<String> = BufReader::new(file)
+ .lines()
+ .map_while(Result::ok)
+ .collect();
+
+ let start = lines.len().saturating_sub(TAIL_LINES);
+ lines[start..].join("\n")
+}
+
+fn cleanup(log_dir: &Path) {
+ let cutoff = std::time::SystemTime::now()
+ - std::time::Duration::from_secs(MAX_LOG_AGE_DAYS * 24 * 60 * 60);
+
+ let Ok(entries) = std::fs::read_dir(log_dir) else {
+ return;
+ };
+
+ for entry in entries.flatten() {
+ if let Ok(meta) = entry.metadata()
+ && let Ok(modified) = meta.modified()
+ && modified < cutoff
+ {
+ let _ = std::fs::remove_file(entry.path());
+ }
+ }
+}
diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs
index af88342c3..9eb86cdac 100644
--- a/packages/desktop/src-tauri/src/main.rs
+++ b/packages/desktop/src-tauri/src/main.rs
@@ -43,7 +43,7 @@ fn configure_display_backend() -> Option<String> {
set_env_if_absent("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
return Some(
"Wayland session detected; forcing X11 backend to avoid compositor protocol errors. \
- Set OC_ALLOW_WAYLAND=1 to keep native Wayland."
+ Set OC_ALLOW_WAYLAND=1 to keep native Wayland."
.into(),
);
}
@@ -86,7 +86,7 @@ fn main() {
#[cfg(target_os = "linux")]
{
if let Some(backend_note) = configure_display_backend() {
- eprintln!("{backend_note:?}");
+ eprintln!("{backend_note}");
}
}
diff --git a/packages/desktop/src-tauri/src/server.rs b/packages/desktop/src-tauri/src/server.rs
index 9d09512ce..81e0595af 100644
--- a/packages/desktop/src-tauri/src/server.rs
+++ b/packages/desktop/src-tauri/src/server.rs
@@ -93,14 +93,14 @@ pub fn set_wsl_config(app: AppHandle, config: WslConfig) -> Result<(), String> {
pub async fn get_saved_server_url(app: &tauri::AppHandle) -> Option<String> {
if let Some(url) = get_default_server_url(app.clone()).ok().flatten() {
- println!("Using desktop-specific custom URL: {url}");
+ tracing::info!(%url, "Using desktop-specific custom URL");
return Some(url);
}
if let Some(cli_config) = cli::get_config(app).await
&& let Some(url) = get_server_url_from_config(&cli_config)
{
- println!("Using custom server URL from config: {url}");
+ tracing::info!(%url, "Using custom server URL from config");
return Some(url);
}
@@ -124,7 +124,7 @@ pub fn spawn_local_server(
tokio::time::sleep(Duration::from_millis(100)).await;
if check_health(&url, Some(&password)).await {
- println!("Server ready after {:?}", timestamp.elapsed());
+ tracing::info!(elapsed = ?timestamp.elapsed(), "Server ready");
return Ok(());
}
}
@@ -216,7 +216,7 @@ fn normalize_hostname_for_url(hostname: &str) -> String {
fn get_server_url_from_config(config: &cli::Config) -> Option<String> {
let server = config.server.as_ref()?;
let port = server.port?;
- println!("server.port found in OC config: {port}");
+ tracing::debug!(port, "server.port found in OC config");
let hostname = server
.hostname
.as_ref()
@@ -227,7 +227,7 @@ fn get_server_url_from_config(config: &cli::Config) -> Option<String> {
}
pub async fn check_health_or_ask_retry(app: &AppHandle, url: &str) -> bool {
- println!("Checking health for {url}");
+ tracing::debug!(%url, "Checking health");
loop {
if check_health(url, None).await {
return true;