diff --git a/docs/core-architecture.md b/docs/core-architecture.md index 0b0abf17..ecf07bf5 100644 --- a/docs/core-architecture.md +++ b/docs/core-architecture.md @@ -552,6 +552,13 @@ optional `tracing` feature selects the tokio-console subscriber integration; only that diagnostic profile pulls the main crate's `tracing-subscriber` and `console-subscriber` dependencies. +The native CLI enables the `CORE` and `easytier_core::instance::manager` console +targets by default. Instance startup failures are reported immediately with the +instance ID and complete error chain, including the listener URL for bind +failures. When all instances stop, the CLI's failure summary retains each failed +instance's ID and saved error. Explicit console levels and `RUST_LOG` directives +still control which records are emitted. + Contrib applications and platform integrations may have independent logging requirements and are not implicitly wired to the native process logger. diff --git a/easytier-core/src/instance/manager.rs b/easytier-core/src/instance/manager.rs index 64b47900..d6a936ea 100644 --- a/easytier-core/src/instance/manager.rs +++ b/easytier-core/src/instance/manager.rs @@ -387,7 +387,11 @@ where let instance_state_changes = self.instance_state_changes.clone(); runtime.spawn(async move { if let Err(error) = instance.start().await { - tracing::error!(%error, %instance_id, "instance failed to start"); + tracing::error!( + error = %format_args!("{error:#}"), + %instance_id, + "instance failed to start" + ); } instance_state_changes.mark_changed(); notifier.notify_one(); diff --git a/easytier-core/src/listener/mod.rs b/easytier-core/src/listener/mod.rs index e59247bf..1d1007bf 100644 --- a/easytier-core/src/listener/mod.rs +++ b/easytier-core/src/listener/mod.rs @@ -342,10 +342,11 @@ where Accepted: Send + 'static, { let mut listener = creator(); - match listener.listen().await { - Ok(()) => Ok(listener), - Err(error) => Err(error), - } + listener + .listen() + .await + .with_context(|| format!("failed to listen on {}", listener.local_url()))?; + Ok(listener) } async fn run_listener( @@ -782,7 +783,8 @@ mod tests { true, ); - assert!(manager.run().await.is_err()); + let error = manager.run().await.unwrap_err(); + assert!(format!("{error:#}").contains("mock://second")); crate::foundation::time::sleep(Duration::from_millis(20)).await; assert!(handler.accepted.lock().unwrap().is_empty()); assert!( diff --git a/easytier/src/core.rs b/easytier/src/core.rs index af96ca95..fde9fa58 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -29,6 +29,7 @@ use rust_i18n::t; use serde_json::{Value, json}; use std::str::FromStr; use std::{ + fmt::Write as _, net::{IpAddr, SocketAddr}, path::PathBuf, process::ExitCode, @@ -1211,7 +1212,11 @@ fn win_service_main(arg: Vec) { async fn run_main(cli: Cli) -> anyhow::Result<()> { defer!(dump_profile(0);); - log::init(&cli.logging_options, true)?; + log::init_with_default_console_targets( + &cli.logging_options, + true, + &["CORE", "easytier_core::instance::manager"], + )?; let manager = Arc::new(native_cli_instance_manager().with_config_path(cli.config_dir.clone())); @@ -1360,10 +1365,17 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { tokio::select! { _ = manager.wait() => { let infos = manager.collect_network_infos().await?; - if infos - .into_values() - .filter_map(|info| info.error_msg).next().is_some() { - return Err(anyhow::anyhow!("some instances stopped with errors")); + let mut errors = String::new(); + for (instance_id, info) in infos { + if let Some(error) = info.error_msg { + if errors.is_empty() { + errors.push_str("some instances stopped with errors:"); + } + write!(errors, "\n{instance_id}: {error}").unwrap(); + } + } + if !errors.is_empty() { + return Err(anyhow::anyhow!(errors)); } } _ = tokio::signal::ctrl_c() => { @@ -1489,7 +1501,7 @@ pub async fn main() -> ExitCode { let mut ret_code = 0; if let Err(error) = run_main(cli).await { - log::error!(%error); + log::error!(error = %format_args!("{error:#}")); ret_code = 1; }