fix(core): show complete instance startup errors (#2724)

Restore the CLI's default instance-manager diagnostics without enabling
unrelated module logs. Include failed instance IDs and their saved error
chains in the final exit error.

Attach the listener URL to required-listener failures and render full
error chains in startup logs, so bind failures expose their OS cause.

Extend listener regression coverage and document the default CLI targets.
This commit is contained in:
KKRainbow
2026-10-18 22:28:41 +08:00
committed by GitHub
parent 4ff27c7ef4
commit a144597d02
4 changed files with 37 additions and 12 deletions
+7
View File
@@ -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.
+5 -1
View File
@@ -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();
+7 -5
View File
@@ -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<Accepted, H>(
@@ -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!(
+18 -6
View File
@@ -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<std::ffi::OsString>) {
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;
}