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())
}
}
两点和本站的写法不同:
- 它先看
dispatcher::has_been_set,已经有全局 dispatcher 就什么都不做。所以谁先调用谁赢,而它总是先被调用。 - 它装 subscriber 用的是
tracing::subscriber::set_global_default,不是try_init。只做 fmt 一页说的两件事里的第一件,不装LogTracer。
实验: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里用logcrate 打日志的依赖有 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。
问: 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_LOG | from_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 全部关闭;本站一侧照原样。
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下的格式随之改变),或删掉(行为不变)。由维护者决定。