Rust in Everything
基于 tracing-subscriber 0.3.23

Unable to install global subscriber 与 failed to set global default subscriber

主线位置:事件主线开始之前。init 把 subscriber 装成全局默认,之后每条事件在 get_default 那一步找到的就是它;这一步失败,事件要么去了别人装的 subscriber,要么哪儿都没去。

flowchart TB
  init["init()"] --> tryinit["try_init()"]
  tryinit --> sgd["set_global_default"]
  sgd -->|已经有了| e1["SetGlobalDefaultError"]
  sgd -->|成功| lt["LogTracer init"]
  lt -->|log 已被占| e2["SetLoggerError"]
  lt -->|成功| ok["Ok"]
  e1 --> wrap["包进 TryInitError"]
  e2 --> wrap
  wrap -->|init| pan["expect 时 panic"]
  wrap -->|"let _ ="| lost["两种都看不到"]

init 和 try_init 做的两件事、set_global_default 为什么只能成功一次,在 fmt 的「SubscriberBuilder、init 与 try_init」和 Dispatch:全局与线程局部 里讲过。这一页看它们在真实程序里的样子:panic 长什么样、出现在哪、try_init 的错误到底是哪一种。

本页的实验都在 M2 Max、rustc 1.99.0、--release 下运行,依赖:

tracing = "=0.1.44"
tracing-subscriber = "=0.3.23"
log = "0.4"

tracing-subscriber 用的是默认 feature,其中包括 fmt 和 tracing-log;后者决定了下面第三种 panic 会不会出现。

复现:三种 panic

use tracing_subscriber::prelude::*;

fn main() {
  let case = std::env::args().nth(1).unwrap_or_default();
  match case.as_str() {
    // SubscriberBuilder::init 两次
    "builder" => {
      tracing_subscriber::fmt().init();
      tracing_subscriber::fmt().init();
    }
    // SubscriberInitExt::init 两次
    "ext" => {
      tracing_subscriber::registry().with(tracing_subscriber::fmt::layer()).init();
      tracing_subscriber::registry().with(tracing_subscriber::fmt::layer()).init();
    }
    // 别人先占了 log 的 logger,再 init
    "logger" => {
      log::set_max_level(log::LevelFilter::Info);
      let _ = log::set_boxed_logger(Box::new(Other));
      tracing_subscriber::fmt().init();
    }
    _ => {}
  }
  println!("not reached");
}

struct Other;
impl log::Log for Other {
  fn enabled(&self, _: &log::Metadata<'_>) -> bool {
    true
  }
  fn log(&self, r: &log::Record<'_>) {
    println!("[other] {}", r.args());
  }
  fn flush(&self) {}
}

三个参数各跑一次,stdout、stderr 合在一起,连跑 3 遍,除线程号外输出相同:

== builder

thread 'main' (1568791) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/fmt/mod.rs:520:14:
Unable to install global subscriber: SetGlobalDefaultError("a global default trace dispatcher has already been set")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
== ext

thread 'main' (1568825) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/util.rs:94:14:
failed to set global default subscriber: SetGlobalDefaultError("a global default trace dispatcher has already been set")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
== logger

thread 'main' (1568863) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/fmt/mod.rs:520:14:
Unable to install global subscriber: SetLoggerError(())
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
  • 前缀:fmt().init() 是 Unable to install global subscriber,registry().with(..).init() 是 failed to set global default subscriber。同一个错误,两段文字,搜报错时两个都要搜。
  • 冒号后面:SetGlobalDefaultError(..) 说的是全局默认已经有了;SetLoggerError(()) 说的是 log crate 的 logger 已经有了,和 tracing 的全局默认无关。
  • 位置:panic 报在 tracing-subscriber 的 fmt/mod.rs:520 或 util.rs:94,不是你写 init() 的那一行。这两个 init 没有标 #[track_caller]。

源码:两个 expect,一个两步的 try_init

两个 init 都只是 try_init 加 expect。SubscriberBuilder::init:

    pub fn init(self) {
        self.try_init()
            .expect("Unable to install global subscriber")
    }

SubscriberInitExt::init:

    fn init(self) {
        self.try_init()
            .expect("failed to set global default subscriber")
    }

expect 打印的是 {msg}: {err:?},所以冒号后面是错误的 Debug。SubscriberBuilder::try_init 先 finish(),再交给 SubscriberInitExt::try_init:

    fn try_init(self) -> Result<(), TryInitError> {
        dispatcher::set_global_default(self.into()).map_err(TryInitError::new)?;

        // ...
        #[cfg(feature = "tracing-log")]
        tracing_log::LogTracer::builder()
            // Note that we must call this *after* setting the global default
            // subscriber, so that we get its max level hint.
            .with_max_level(tracing_core::LevelFilter::current().as_log())
            .init()
            .map_err(TryInitError::new)?;

        Ok(())
    }

两个 ?,两种错误:

哪一步失败错误全局 subscriberlog::info! 去哪
set_global_defaultSetGlobalDefaultError没装上,还是之前那个第二步没执行,log 没有 logger
LogTracer 的 Builder::initlog::SetLoggerError已经装上先装的那个 logger

第二行就是第三种 panic 的情形:panic 的文字说「Unable to install global subscriber」,subscriber 其实已经装好了。在 init 外面套一个 catch_unwind 就能看到:

fn main() {
  log::set_logger(&Other).unwrap();
  log::set_max_level(log::LevelFilter::Info);
  let r = std::panic::catch_unwind(|| {
    tracing_subscriber::fmt().with_ansi(false).without_time().init();
  });
  println!("init panicked: {}", r.is_err());
  println!("has_been_set: {}", tracing::dispatcher::has_been_set());
  tracing::info!("tracing event after the panic");
}

(Other 同上。)连跑 3 次,除线程号外相同:

thread 'main' (1576874) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/fmt/mod.rs:520:14:
Unable to install global subscriber: SetLoggerError(())
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
init panicked: true
has_been_set: true
 INFO init_logger_panic: tracing event after the panic

try_init 加 let _ =:分不清是哪一种

为了不 panic,很多代码写成 let _ = tracing_subscriber::fmt().try_init();。这样两种失败都悄无声息。就算不扔掉错误,想在代码里分辨是哪一种也做不到:

fn main() {
  let case = std::env::args().nth(1).unwrap_or_default();
  match case.as_str() {
    // 别的代码先装了一个全局 subscriber(这里用一个什么都不收的)
    "global-taken" => {
      tracing::subscriber::set_global_default(tracing::subscriber::NoSubscriber::default()).unwrap();
    }
    // 别的代码先装了 log 的 logger
    "logger-taken" => {
      log::set_logger(&Other).unwrap();
      log::set_max_level(log::LevelFilter::Info);
    }
    _ => {}
  }

  let res = tracing_subscriber::fmt().with_ansi(false).without_time().try_init();
  match &res {
    Ok(()) => println!("try_init -> Ok"),
    Err(e) => {
      println!("try_init -> Err, Display = {e}");
      println!("  source() = {:?}", e.source());
      println!(
        "  downcast to SetGlobalDefaultError: {}",
        e.downcast_ref::<tracing::subscriber::SetGlobalDefaultError>().is_some()
      );
      println!(
        "  downcast to TryInitError: {}",
        e.downcast_ref::<tracing_subscriber::util::TryInitError>().is_some()
      );
    }
  }
  tracing::info!("tracing event");
  log::info!("log record");
}

(Other 同上,打印时加前缀 [other logger]。)三种情形连跑 3 遍,输出相同:

$ ./which_err none
try_init -> Ok
 INFO which_err: tracing event
 INFO which_err: log record
$ ./which_err global-taken
try_init -> Err, Display = a global default trace dispatcher has already been set
  source() = None
  downcast to SetGlobalDefaultError: false
  downcast to TryInitError: true
$ ./which_err logger-taken
try_init -> Err, Display = attempted to set a logger after the logging system was already initialized
  source() = None
  downcast to SetGlobalDefaultError: false
  downcast to TryInitError: true
 INFO which_err: tracing event
[other logger] log record
  • global-taken:两条日志都没了。tracing 事件去了先装的 NoSubscriber;log::info! 也没了,因为第一个 ? 返回后 LogTracer 根本没装,log 没有 logger。
  • logger-taken:Err,但 tracing 事件照常输出,log 记录走了先装的 logger。
  • 两种情形的错误类型一样,都是 TryInitError。它把真正的错误装在私有字段 inner: Box<dyn Error + ..> 里(TryInitError),没有取出它的方法;Error::source 的实现(第 152 行)返回的是 self.inner.source(),即内层错误的 source,而两种内层错误都没有 source,所以是 None。能区分它们的只剩 Display / Debug 的文字。
📝NOTE
没有蠢问题

问: 那就判断 is_err(),失败了再装一次?

答: 两种失败都不能靠「再装一次」解决。全局默认一旦设上就不能换(set_global_default 用 compare_exchange 只放一个线程过去,见 Dispatch:全局与线程局部);log 的 logger 也是只能设一次。而且 logger-taken 时你的 subscriber 已经装好,再装只会得到第一种错误。真正要做的是弄清楚是谁先装的:通常是一个库在初始化时顺手装了 subscriber 或 log logger,或者是框架:dioxus-server 0.7.9 的 launch.rs 里就调用了 dioxus_logger::initialize_default(),本站因此碰到的情形见 subscriber 的初始化。

测试里每个测试都 init

libtest 默认在同一个进程的多个线程里并行跑测试,全局默认是进程级的。每个测试都调用一次 setup():

fn setup() {
  tracing_subscriber::fmt().with_test_writer().init();
}

#[test]
fn first() {
  setup();
  tracing::info!("first");
}

#[test]
fn second() {
  setup();
  tracing::info!("second");
}

cargo test --release 连跑 3 次,失败的测试第 1、3 次是 second,第 2 次是 first,取决于谁先抢到。第 1 次:

running 2 tests
. 1/2
second --- FAILED

failures:

---- second stdout ----

thread 'second' (1572368) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/fmt/mod.rs:520:14:
Unable to install global subscriber: SetGlobalDefaultError("a global default trace dispatcher has already been set")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    second

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

只跑其中一个测试时它会通过,于是这个问题常常在加第二个测试时才出现。改法是让每个测试装一个线程默认,用守卫限定在这个测试里:

use tracing_subscriber::util::SubscriberInitExt;

fn setup() -> tracing::subscriber::DefaultGuard {
  tracing_subscriber::fmt().with_test_writer().set_default()
}

#[test]
fn first() {
  let _guard = setup();
  tracing::info!("first");
}

#[test]
fn second() {
  let _guard = setup();
  tracing::info!("second");
}

连跑 3 次都是 test result: ok. 2 passed; 0 failed。libtest 每个测试跑在自己的线程上,线程默认互不干扰;守卫要绑到 _guard 这样的变量上,不能写 let _ =(原因见 一条日志都没有)。测试里的输出为什么要 with_test_writer(),见 测试里的日志。

修法

情形怎么改
自己的代码 init 了两次只在 main 开头初始化一次;库代码不要装全局 subscriber,交给二进制决定
测试里每个测试都 init每个测试 let _guard = ...set_default();;或者只要求「装上就行」时用 let _ = try_init(),这是 let _ = 唯一说得过去的地方
依赖或框架先装了全局 subscriber找到它(见下一节),用它提供的配置入口,或者关掉它的初始化
只是 log 的 logger 被占了不需要 log 转发就关掉 tracing-subscriber 的 tracing-log feature(default-features = false 再列出要的 feature);需要的话把两步拆开写,见下面

想分清两种失败,不要用 try_init,自己分两步做,每一步的错误类型都是具体的:

fn main() {
  log::set_logger(&Other).unwrap(); // 别人先占了 log
  log::set_max_level(log::LevelFilter::Info);

  let subscriber = tracing_subscriber::fmt().with_ansi(false).without_time().finish();
  // 第一件事:装全局 subscriber,错误类型是 SetGlobalDefaultError
  if let Err(e) = tracing::subscriber::set_global_default(subscriber) {
    eprintln!("tracing subscriber 没装上: {e}");
  }
  // 第二件事:把 log 转进 tracing,错误类型是 log::SetLoggerError
  if let Err(e) = tracing_log::LogTracer::init() {
    eprintln!("LogTracer 没装上(tracing 本身已装好): {e}");
  }
  tracing::info!("tracing event");
  log::info!("log record");
}

(另加 tracing-log = "=0.2.0"。)连跑 3 次输出相同:

LogTracer 没装上(tracing 本身已装好): attempted to set a logger after the logging system was already initialized
 INFO typed_init: tracing event
[other logger] log record

LogTracer::init() 不像 try_init 那样把 log 的最大级别同步成 tracing 的;要同步,用 LogTracer::builder().with_max_level(..).init(),和上面源码里的写法一样。

怎么确认自己没踩

  • 不确定谁先装了 subscriber 时,在你的 init 之前打印 tracing::dispatcher::has_been_set()。它在任何 dispatcher 被设过(全局或线程默认)后返回 true(has_been_set)。

  • panic 的位置在 tracing-subscriber 里,要找你自己的那一行,用 debug 构建加 RUST_BACKTRACE=1。上面 builder 那个例子在 debug 构建下的回溯里有一帧:

       5: init_twice::main
                 at ./src/bin/init_twice.rs:9:33
    

    第 9 行正是第二个 fmt().init()。release 构建里这一帧只剩 init_twice::main,没有行号。

  • 用了 try_init 的地方,至少把错误打出来(if let Err(e) = ... { eprintln!("{e}") }),不要 let _ =。看到 a global default trace dispatcher has already been set 就是没装上;看到 attempted to set a logger ... 就是装上了、只差 log 转发。

小结

  • 两段 panic 文字对应两个 init:SubscriberBuilder::init(fmt().init())是 Unable to install global subscriber,SubscriberInitExt::init(registry().with(..).init())是 failed to set global default subscriber;冒号后是内层错误的 Debug。
  • try_init 有两步:set_global_default 失败时什么都没装;LogTracer 失败时 subscriber 已经装上。第二种情况 panic 文字仍然说「Unable to install」。
  • TryInitError 不暴露内层错误,source() 是 None,代码里只能靠文字区分;要区分就自己分两步装。
  • libtest 并行跑测试,全局默认是进程级的,每个测试都 init 必有一个 panic;测试里用 set_default 加守卫。
  • panic 位置在 tracing-subscriber 内部,用 debug 构建的回溯找自己的调用行。

💬 关联讨论

发起讨论
加载中...