Rust in Everything
基于 tracing 0.1.44

subscriber 的初始化

主线位置:事件主线的终点。每条 info! 最后交给哪个 subscriber、按什么规则过滤、写成什么格式,由进程里第一个 set_global_default 成功的那一方决定。本站这一方不是 main.rs,而是 dioxus-logger。

sequenceDiagram
  participant M as main
  participant S as serve_router
  participant L as dioxus-logger
  participant C as 本站回调
  participant G as 全局 dispatcher
  M->>S: dioxus::serve(回调)
  S->>L: initialize_default()
  L->>G: set_global_default
  G-->>L: Ok
  S->>C: serve_callback()
  C->>G: try_init()
  G-->>C: SetGlobalDefaultError
  Note over C: let _ = 吞掉

本页的站点源码链接都指向本站仓库的提交 89a61b1。那个提交的 Cargo.lock 锁的是 dioxus、dioxus-server、dioxus-logger、dioxus-cli-config 0.7.9,tracing-subscriber 0.3.23。dioxus 的源码按「crate + 版本 + 文件」引用。

本站写了什么

crates/app/src/main.rs 第 186 行起,服务端的入口是 dioxus::serve 加一个返回 Router 的异步闭包。闭包开头先读 .env,第 199 行装 subscriber:

  #[cfg(feature = "server")]
  dioxus::serve(|| async move {
    use axum::routing::get;

    // 加载 .env 环境变量
    dotenvy::dotenv().ok();

    // Phase 7.5:初始化 tracing 订阅。日志级别由 `RUST_LOG` 控制
    // (默认 `info`),格式为人可读 + 含 target。
    //
    // 用 `try_init`(而非 `init`):在 `dx serve` 等开发运行时下,
    // dioxus-devtools 可能已安装全局 subscriber,`init` 会 panic
    // ("a global default trace dispatcher has already been set")。
    // 已存在时静默跳过,生产 `dx bundle` 产物里没有冲突 subscriber,正常生效。
    let _ = tracing_subscriber::fmt()
      .with_env_filter(
        tracing_subscriber::EnvFilter::try_from_default_env()
          .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
      )
      .with_target(true)
      .try_init();

注释里有两个判断:开发时可能有别人先装了 subscriber,所以用 try_init;生产构建里没有,这段代码会生效。第一个判断里「别人」是谁说错了,第二个判断不成立。

dioxus-server 先装了一个

dioxus::serve 就是 dioxus_server::serve。dioxus-server 0.7.9 的 src/launch.rs:

pub fn serve<F>(mut serve_it: impl FnMut() -> F) -> !
where
    F: Future<Output = Result<Router, anyhow::Error>> + 'static,
{
    let cb = move || Box::pin(serve_it()) as _;

    block_on(
        async move { serve_router(cb, dioxus_cli_config::fullstack_address_or_localhost()).await },
    );

    unreachable!("Serving a fullstack app should never return")
}

pub async fn serve_router(
    mut serve_callback: impl FnMut() -> Pin<Box<dyn Future<Output = Result<Router, anyhow::Error>>>>,
    addr: SocketAddr,
) {
    dioxus_logger::initialize_default();

    let listener = TcpListener::bind(addr)
    // ...

    // If we're not in debug mode, just serve the app normally
    if !cfg!(debug_assertions) {
        axum::serve(listener, serve_callback().await.unwrap())
            .await
            .unwrap();
        return;
    }

    // ... 开发模式:连上 devtools,经 HotFn 调用 serve_callback

serve_router 的第一行就是 dioxus_logger::initialize_default(),之后才 serve_callback().await。release 和 debug 两条路径都在这一行之后才调用回调。dioxus-server 0.7.9 对 dioxus-logger 的依赖不是可选的,没有 feature 能把这一行关掉。dioxus-devtools 0.7.9 的源码里没有 set_global_default 或 try_init,注释里说的 devtools 并不装 subscriber;先装的是 dioxus-logger,开发和生产都一样。

dioxus-logger 0.7.9 的 src/lib.rs:

pub fn initialize_default() {
    if tracing::dispatcher::has_been_set() {
        return;
    }

    if cfg!(debug_assertions) {
        _ = init(Level::DEBUG);
    } else {
        _ = init(Level::INFO);
    }
}

pub fn init(level: Level) -> Result<(), SetGlobalDefaultError> {
    // ... wasm32 分支用 tracing-wasm,略

    #[cfg(not(target_arch = "wasm32"))]
    {
        let sub = tracing_subscriber::FmtSubscriber::builder()
            .with_max_level(level)
            .with_env_filter(
                tracing_subscriber::EnvFilter::builder()
                    .with_default_directive(level.into())
                    .from_env_lossy()
                    .add_directive("hyper_util=warn".parse().unwrap()), // hyper has `debug!` sitting around in some places that are spammy
            );

        if !dioxus_cli_config::is_cli_enabled() {
            return set_global_default(sub.finish());
        }

        // todo(jon): this is a small hack to clean up logging when running under the CLI
        // eventually we want to emit everything as json and let the CLI manage the parsing + display
        set_global_default(sub.without_time().with_target(false).finish())
    }
}

两点和本站的写法不同:

实验:try_init 返回哪一种错误

fmt 的「SubscriberBuilder、init 与 try_init」讲过,try_init 有两个 ?:第一个是 set_global_default 失败,什么都没装上,错误是 SetGlobalDefaultError;第二个是 LogTracer 装不上,这时 subscriber 已经装好了,错误是 SetLoggerError。两种都被包进 TryInitError,let _ = 把它们一起吞掉(见 Unable to install global subscriber)。要知道本站是哪一种,得把它打印出来。

实验不构建整个站点,只依赖 dioxus-logger 本身,按 serve_router 的顺序调用:先 initialize_default(),再原样执行本站的链式调用。M2 Max、rustc 1.99.0,依赖:

dioxus-logger = "=0.7.9"
dioxus-cli-config = "=0.7.9" # 和本站 Cargo.lock 一致;不写会解析到更新的 0.7.10
tracing = "=0.1.44"
tracing-subscriber = { version = "=0.3.23", features = ["env-filter", "fmt"] }
log = "0.4"

tracing-subscriber 的 feature 和本站 Cargo.toml 一样:默认 feature(含 ansi、tracing-log)加 env-filter、fmt。

fn main() {
  // dioxus-server 0.7.9 的 serve_router 第一行
  dioxus_logger::initialize_default();

  // 本站 crates/app/src/main.rs 回调里的原样写法,只是不再 `let _ =`
  let res = tracing_subscriber::fmt()
    .with_env_filter(
      tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
    )
    .with_target(true)
    .try_init();
  println!("try_init -> {res:?}");
  println!("log::max_level() = {}", log::max_level());

  tracing::info!(target: "app", "app info");
  tracing::debug!(target: "app", "app debug");
  tracing::debug!(target: "x", "x debug");
  tracing::info!(target: "hyper_util::client", "hyper_util info");
  tracing::warn!(target: "hyper_util::client", "hyper_util warn");
  log::warn!(target: "app", "a log::warn! record");
}

--release,RUST_LOG=info(和生产一样),另设 NO_COLOR=1 去掉颜色(原因见 日志行)。跑 3 次,除时间戳外输出相同:

try_init -> Err(SetGlobalDefaultError("a global default trace dispatcher has already been set"))
log::max_level() = OFF
2026-10-23T20:34:54.634106Z  INFO app: app info
2026-10-23T20:34:54.634175Z  WARN hyper_util::client: hyper_util warn
  • 错误是 SetGlobalDefaultError,第一个 ? 就失败了。本站构造的 subscriber 整个被丢掉,连同它本该装上的 LogTracer。
  • log::max_level() 是 OFF:dioxus-logger 不装 LogTracer,本站的 try_init 也没走到那一步,进程里没有 log 的 logger。最后那条 log::warn! 没有输出。本站 app 的源码里没有别处调用 log::set_logger 或 LogTracer;Cargo.lock 里用 log crate 打日志的依赖有 tantivy 0.26.1、sqlx-postgres 0.8.6、sea-orm 1.1.20、reqwest 0.12.28 等,按这个初始化顺序,它们的 log 记录没有去处。log 和 tracing 怎么互通见 和 log 互通。
  • hyper_util info 没有出现,hyper_util warn 出现了:生效的过滤器里有 dioxus-logger 加的 hyper_util=warn。
📝NOTE
没有蠢问题

问: try_init 一直失败,本站的日志为什么照样有,格式也和注释里写的「人可读 + 含 target」一样?

答: 因为 dioxus-logger 装的也是一个 FmtSubscriber,默认的 Full 格式。生产构建是 release、容器里没有 DIOXUS_CLI_ENABLED,它走 set_global_default(sub.finish()) 那一支:时间、级别、target 都在,和本站想要的一样。RUST_LOG=info 时两边的默认级别也一样。差别只在 hyper_util 的 INFO 和 log 记录这些平时不看的地方,所以从输出上看不出本站的初始化没有生效。

两份配置逐项对比

项dioxus-logger 0.7.9(实际生效)本站 main.rs(没有生效)
没有 RUST_LOG 时的级别release 构建 info,debug 构建 debug(看 cfg!(debug_assertions))info
读 RUST_LOGfrom_env_lossy:坏的一段打印 ignoring ... 后丢掉,其余照用try_from_default_env:变量不存在或任何一段解析失败都是 Err,整体换成 EnvFilter::new("info")
RUST_LOG=(空串)用默认指令Ok(空过滤器),什么都不输出(见 RUST_LOG 不匹配)
额外指令最后 add_directive("hyper_util=warn")无
with_max_level(level)被随后的 with_env_filter 整个替换,不起作用—
时间、target不在 dx 下:都有;在 dx 下:without_time().with_target(false)都有;with_target(true) 本来就是默认值
ANSI 颜色默认:开了 ansi feature 且 NO_COLOR 为空就开同左
LogTracer不装try_init 成功时会装

with_max_level 那一行不起作用,是因为 SubscriberBuilder::with_env_filter 返回一个新的 builder,filter 字段直接换成 EnvFilter,前面的 LevelFilter 被丢掉:

        let filter = filter.into();
        SubscriberBuilder {
            filter,
            inner: self.inner,
        }

with_max_level 也是同样的写法,两者谁在后谁生效。level 真正起作用的地方是 with_default_directive(level.into())。

hyper_util=warn 是在 from_env_lossy 之后用 EnvFilter::add_directive 加的。默认指令只在一条指令都没有时由 from_directives 加(见 EnvFilter),这时已经加完了,所以 RUST_LOG 为空也照样有 info。add_directive 把指令交给 DirectiveSet::add,遇到排序上相等的已有指令(同一个 target、没有 span 和字段条件)就覆盖:

        match self.directives.binary_search(&directive) {
            Ok(i) => self.directives[i] = directive,
            Err(i) => self.directives.insert(i, directive),
        }

所以 RUST_LOG 里写的 hyper_util=debug 会被它换成 hyper_util=warn。

把两边的过滤器在同一组 RUST_LOG 下构造出来,打印 Display。dioxus 一侧照抄 init 里的表达式(release 构建的 level):

use tracing::Level;
use tracing_subscriber::EnvFilter;

fn set(v: Option<&str>) {
  // edition 2024 里改环境变量是 unsafe;这里是单线程
  unsafe {
    match v {
      Some(v) => std::env::set_var("RUST_LOG", v),
      None => std::env::remove_var("RUST_LOG"),
    }
  }
}

fn main() {
  for v in [None, Some(""), Some("info"), Some("oops=loud"), Some("info,oops=loud"), Some("info, x=debug"), Some("info,app=debug,oops=loud"), Some("hyper_util=debug")] {
    set(v);
    println!("== RUST_LOG={v:?}");
    // dioxus-logger 0.7.9 init() 里的过滤器(release 构建 level = INFO)
    let level = Level::INFO;
    let dioxus = EnvFilter::builder()
      .with_default_directive(level.into())
      .from_env_lossy()
      .add_directive("hyper_util=warn".parse().unwrap());
    println!("  dioxus: {dioxus}");
    // 本站 main.rs 里的过滤器
    let site = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
    println!("  site:   {site}");
  }
}

运行时 2>&1,跑 3 次,输出相同:

== RUST_LOG=None
  dioxus: hyper_util=warn,info
  site:   info
== RUST_LOG=Some("")
  dioxus: hyper_util=warn,info
  site:   
== RUST_LOG=Some("info")
  dioxus: hyper_util=warn,info
  site:   info
== RUST_LOG=Some("oops=loud")
ignoring `oops=loud`: error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
  dioxus: hyper_util=warn,info
  site:   info
== RUST_LOG=Some("info,oops=loud")
ignoring `oops=loud`: error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
  dioxus: hyper_util=warn,info
  site:   info
== RUST_LOG=Some("info, x=debug")
ignoring ` x=debug`: error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
  dioxus: hyper_util=warn,info
  site:   info
== RUST_LOG=Some("info,app=debug,oops=loud")
ignoring `oops=loud`: error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
  dioxus: hyper_util=warn,app=debug,info
  site:   info
== RUST_LOG=Some("hyper_util=debug")
  dioxus: hyper_util=warn
  site:   hyper_util=debug

逐行看:

  • 没设、info:两边只差 hyper_util=warn。生产就是 RUST_LOG=info 这一行。
  • 空串:本站的写法得到空过滤器,Display 是空的,什么都不放行;unwrap_or_else 的兜底只在 Err 时触发,空串不是 Err。dioxus 一侧退回默认指令。
  • oops=loud、info,oops=loud:两边结果碰巧一样,但原因不同。dioxus 一侧丢掉坏的一段;本站一侧整个 RUST_LOG 作废,换成 info,而且不打印任何提示。ignoring 那一行来自 dioxus 一侧的 parse_lossy。
  • info, x=debug:逗号后的空格让 x=debug 这一段解析失败,原因见 EnvFilter 的「指令语法」(Directive::parse 对 trim 过的字符串取下标,却去切没 trim 的原串)。dioxus 一侧丢掉这一段,x 的 debug 不会开;本站一侧整体回退成 info。两边都开不了 x=debug。
  • info,app=debug,oops=loud:真正分出差别的一行。dioxus 一侧保留 app=debug;本站一侧因为一个坏段把 app=debug 也丢了。
  • hyper_util=debug:dioxus 一侧被 add_directive 覆盖成 hyper_util=warn,而且没有裸级别,其余 target 全部关闭;本站一侧照原样。
❗IMPORTANT
动脑筋:RUST_LOG=hyper_util=info,release 构建,上面那个 try_init 实验会输出哪几条日志?

想一想:app info 还在不在?hyper_util info 呢?

dx serve 与生产

dioxus-logger 的格式分支看 dioxus-cli-config 0.7.9 的 is_cli_enabled:

pub const CLI_ENABLED_ENV: &str = "DIOXUS_CLI_ENABLED";

/// Check if the CLI is enabled when running the application.
///
/// The CLI *always* sets this value to true when running the application.
// ...
pub fn is_cli_enabled() -> bool {
    // todo: (jon) - on android and web we should fix this...
    std::env::var(CLI_ENABLED_ENV).is_ok()
}

只看变量存不存在,不看值。文档注释说 dx 运行程序时总是设它。于是有两个开关:构建是不是 debug(决定默认级别),有没有 DIOXUS_CLI_ENABLED(决定格式)。还是上面的 try_init 实验,换几种组合:

== release, RUST_LOG=info, DIOXUS_CLI_ENABLED=1
try_init -> Err(SetGlobalDefaultError("a global default trace dispatcher has already been set"))
log::max_level() = OFF
 INFO app info
 WARN hyper_util warn
== debug, RUST_LOG unset
try_init -> Err(SetGlobalDefaultError("a global default trace dispatcher has already been set"))
log::max_level() = OFF
2026-10-23T20:34:58.008156Z  INFO app: app info
2026-10-23T20:34:58.008378Z DEBUG app: app debug
2026-10-23T20:34:58.008385Z DEBUG x: x debug
2026-10-23T20:34:58.008394Z  WARN hyper_util::client: hyper_util warn
== debug, RUST_LOG unset, DIOXUS_CLI_ENABLED=1
try_init -> Err(SetGlobalDefaultError("a global default trace dispatcher has already been set"))
log::max_level() = OFF
 INFO app info
DEBUG app debug
DEBUG x debug
 WARN hyper_util warn
  • dx serve 是 debug 构建加 DIOXUS_CLI_ENABLED,对应最后一组:默认 debug,没有时间也没有 target。DEBUG x debug 里的 x debug 是消息本身,不是 target 加消息。本站注释说的「含 target」在 dx serve 下也不成立。
  • 生产镜像由 Dockerfile 里的 dx bundle --platform web --release 构建,是 release;Dockerfile 第 157 行的 ENV 和 docker-compose.yml 的 environment 里都没有 DIOXUS_CLI_ENABLED,所以是完整格式、默认 info。RUST_LOG 由 docker-compose.yml 第 107 行的 RUST_LOG: ${RUST_LOG:-info} 给出(Dockerfile 的 ENV 里也有一份 RUST_LOG=info)。生产实际生效的过滤器就是 hyper_util=warn,info。

开发模式下 serve_router 经 HotFn 调用回调,热重载时回调可能被再次调用;每一次 try_init 都是同样的 SetGlobalDefaultError。

.env 里的 RUST_LOG

还有一个顺序问题。本站在回调里才执行 dotenvy::dotenv(),而 dioxus-logger 在那之前就读过 RUST_LOG 了。.env.example 第 112 行是 RUST_LOG=info,上一行的注释举例 RUST_LOG=info,rustineverything_core=debug,sqlx=warn。如果启动进程的环境里本来没有 RUST_LOG,.env 里这一行进不了过滤器:

fn main() {
  // serve_router 第一行,此时环境里还没有 RUST_LOG
  dioxus_logger::initialize_default();

  // 回调开头:本站 main.rs 用 dotenvy::dotenv() 读 .env,这里换成指定文件
  dotenvy::from_filename("dot.env").ok();
  println!("RUST_LOG now = {:?}", std::env::var("RUST_LOG"));

  tracing::info!(target: "app", "app info");
  tracing::debug!(target: "app", "app debug");
}

dot.env 里只有一行 RUST_LOG=debug,另加依赖 dotenvy = "=0.15.7"。--release,不设 RUST_LOG,跑 3 次,输出相同:

RUST_LOG now = Ok("debug")
2026-10-23T20:35:29.083327Z  INFO app: app info

环境变量已经是 debug,app debug 仍然没有出现:EnvFilter 在构造时读一次环境变量,之后不再看。生产不受影响,容器的环境变量由 compose 直接给出,进程启动时就在。

可能的改法

改不改、怎么改由本站维护者决定。这里只列出两种做法各自会带来什么,都用上面的实验验证过。

一、把初始化挪到 dioxus::serve 之前。 放在 main 里、#[cfg(feature = "server")] 下,initialize_default 看到 has_been_set() 为真就直接返回,本站的配置生效。实验(同样的依赖,先执行本站的链式调用,再调用 initialize_default()):

fn main() {
  // 假设把本站的初始化挪到 dioxus::serve 之前
  let res = tracing_subscriber::fmt()
    .with_env_filter(
      tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
    )
    .with_target(true)
    .try_init();
  println!("try_init -> {res:?}");

  // serve_router 第一行:has_been_set() 为 true,直接返回
  dioxus_logger::initialize_default();
  println!("log::max_level() = {}", log::max_level());

  tracing::info!(target: "app", "app info");
  tracing::debug!(target: "app", "app debug");
  tracing::debug!(target: "x", "x debug");
  tracing::info!(target: "hyper_util::client", "hyper_util info");
  tracing::warn!(target: "hyper_util::client", "hyper_util warn");
  log::warn!(target: "app", "a log::warn! record");
}

RUST_LOG=info 跑 3 次,除时间戳外相同;另两组各跑一次:

== RUST_LOG=info
try_init -> Ok(())
log::max_level() = INFO
2026-10-23T20:35:04.938555Z  INFO app: app info
2026-10-23T20:35:04.938654Z  INFO hyper_util::client: hyper_util info
2026-10-23T20:35:04.938659Z  WARN hyper_util::client: hyper_util warn
2026-10-23T20:35:04.938663Z  WARN app: a log::warn! record
== RUST_LOG=info,oops=loud
try_init -> Ok(())
log::max_level() = INFO
2026-10-23T20:35:05.210186Z  INFO app: app info
2026-10-23T20:35:05.210287Z  INFO hyper_util::client: hyper_util info
2026-10-23T20:35:05.210292Z  WARN hyper_util::client: hyper_util warn
2026-10-23T20:35:05.210297Z  WARN app: a log::warn! record
== debug, unset, DIOXUS_CLI_ENABLED=1
try_init -> Ok(())
log::max_level() = INFO
2026-10-23T20:35:05.422620Z  INFO app: app info
2026-10-23T20:35:05.422742Z  INFO hyper_util::client: hyper_util info
2026-10-23T20:35:05.422751Z  WARN hyper_util::client: hyper_util warn
2026-10-23T20:35:05.422760Z  WARN app: a log::warn! record

随之改变的东西:LogTracer 装上了,依赖里的 log 记录开始出现;没有了 hyper_util=warn,hyper-util 的 INFO 会出现;过滤器换成上一节表格右栏的规则(严格解析、整体回退,RUST_LOG= 时什么都没有);dx serve 下也是带时间和 target 的完整格式,默认级别是 info 而不是 debug。如果还想让 .env 里的 RUST_LOG 生效,dotenvy::dotenv() 也要一起挪到前面。

二、删掉这段代码。 它现在每次都失败,删掉后行为不变:仍是 dioxus-logger 的配置,app crate 对 tracing-subscriber 的直接依赖(crates/app/Cargo.toml 第 42 行,server feature 打开)也就没有用处了。需要调整的是注释和文档里关于「本站自己初始化 tracing」的说法。

动脑筋答案

RUST_LOG=hyper_util=info,--release,跑 3 次,除时间戳外相同:

try_init -> Err(SetGlobalDefaultError("a global default trace dispatcher has already been set"))
log::max_level() = OFF
2026-10-23T20:40:49.654782Z  WARN hyper_util::client: hyper_util warn

只剩一行。

  • app info 没了:RUST_LOG 有一条指令,from_directives 就不加默认指令;过滤器里没有裸级别,hyper_util 以外的 target 一律关闭。
  • hyper_util info 也没了:RUST_LOG 里的 hyper_util=info 被随后的 add_directive("hyper_util=warn") 覆盖。
  • try_init 还是 SetGlobalDefaultError,log::max_level() 还是 OFF,和 RUST_LOG 无关。

小结

  • dioxus-server 0.7.9 的 serve_router 第一行调用 dioxus_logger::initialize_default(),然后才调用本站传给 dioxus::serve 的回调;本站回调里的 try_init 总是返回 SetGlobalDefaultError,被 let _ = 吞掉。开发和生产都是这样。
  • 生效的是 dioxus-logger 的 FmtSubscriber:from_env_lossy 读 RUST_LOG,默认级别 release 为 info、debug 为 debug,最后加 hyper_util=warn(会覆盖 RUST_LOG 里同名的指令),with_max_level 被 with_env_filter 替换掉。不装 LogTracer,log 记录没有去处。
  • 生产(release、无 DIOXUS_CLI_ENABLED、RUST_LOG=info)下,它的输出格式和本站想要的一样,过滤器是 hyper_util=warn,info;dx serve 下没有时间和 target,默认 debug。
  • .env 在回调里才读,晚于 dioxus-logger 读 RUST_LOG。
  • 两种改法:挪到 dioxus::serve 之前(本站配置生效,过滤规则、log 转发、dx serve 下的格式随之改变),或删掉(行为不变)。由维护者决定。

💬 关联讨论

发起讨论
加载中...