如何修改Rust tracing_subscriber配置实现日志定向到stdout?
问题描述
我已经将Rust的tracing_subscriber配置为默认将所有日志消息输出到stderr(自定义配置如下),符合常规需求。现在希望在部分场景中,通过类似tracing::info!(to: "stdout", "This message goes to stdout");的写法将日志输出到stdout,请问如何修改setup_logging函数实现该需求?
原配置代码:
use std::fmt; use std::io; use tracing::{Event, Level, Subscriber}; use tracing_subscriber::fmt::format; use tracing_subscriber::{ fmt::{FmtContext, FormatEvent, FormatFields}, registry::LookupSpan, }; /// A [tracing_subscriber] event formatter that suppresses everything but the /// log message. /// /// Level/target are displayed for non-INFO messages. struct BareFormatter; impl<S, N> FormatEvent<S, N> for BareFormatter where S: Subscriber + for<'a> LookupSpan<'a>, N: for<'a> FormatFields<'a> + 'static, { fn format_event( &self, ctx: &FmtContext<'_, S, N>, mut writer: format::Writer<'_>, event: &Event<'_>, ) -> fmt::Result { let metadata = event.metadata(); if metadata.level() != &Level::INFO { write!(&mut writer, "{} {}: ", metadata.level(), metadata.target())?; } ctx.field_format().format_fields(writer.by_ref(), event)?; writeln!(writer) } } pub fn setup_logging(verbose: bool) { let env_filter = if verbose { "nixci=debug,nix_rs=debug" } else { "nixci=info,nix_rs=info" }; let builder = tracing_subscriber::fmt() .with_writer(io::stderr) .with_max_level(Level::INFO) .with_env_filter(env_filter); if !verbose { builder.event_format(BareFormatter).init(); } else { builder.init() } }
解决方案
要实现按日志字段动态选择输出流,需完成以下修改:
- 自定义动态Writer:创建能根据日志事件的
to字段,自动切换stdout/stderr输出的Writer类型,默认输出到stderr。 - 调整日志格式化逻辑:确保自定义的
BareFormatter不会打印to字段本身,避免日志冗余。 - 替换默认Writer:在
setup_logging中用自定义Writer替代原有固定的io::stderr。
修改后的完整代码如下:
use std::fmt; use std::io::{self, Stderr, Stdout}; use std::sync::Arc; use tracing::{Event, Level, Subscriber, field::Visit}; use tracing_subscriber::fmt::format; use tracing_subscriber::{ fmt::{FmtContext, FormatEvent, FormatFields}, registry::LookupSpan, }; /// 自定义Writer,根据日志事件的`to`字段选择输出到stdout或stderr struct DynamicWriter { stdout: Arc<Stdout>, stderr: Arc<Stderr>, } impl DynamicWriter { fn new() -> Self { Self { stdout: Arc::new(io::stdout()), stderr: Arc::new(io::stderr()), } } // 从事件中提取`to`字段的值,判断输出流 fn get_writer(&self, event: &Event<'_>) -> Arc<dyn io::Write + Send + Sync + 'static> { let mut visitor = ToFieldVisitor { target: None }; event.record(&mut visitor); match visitor.target.as_deref() { Some("stdout") => Arc::clone(&self.stdout) as _, _ => Arc::clone(&self.stderr) as _, } } } // 用于提取`to`字段的Visitor struct ToFieldVisitor { target: Option<String>, } impl Visit for ToFieldVisitor { fn record_str(&mut self, field: &tracing::field::Field, value: &str) { if field.name() == "to" { self.target = Some(value.to_string()); } } } // 实现tracing_subscriber的MakeWriter trait,支持按事件选择Writer impl tracing_subscriber::fmt::MakeWriter<'_> for DynamicWriter { type Writer = Arc<dyn io::Write + Send + Sync + 'static>; fn make_writer(&self) -> Self::Writer { Arc::clone(&self.stderr) } // 关键:根据事件动态返回Writer fn make_writer_for(&self, event: &Event<'_>) -> Self::Writer { self.get_writer(event) } } /// A [tracing_subscriber] event formatter that suppresses everything but the /// log message. /// /// Level/target are displayed for non-INFO messages. struct BareFormatter; impl<S, N> FormatEvent<S, N> for BareFormatter where S: Subscriber + for<'a> LookupSpan<'a>, N: for<'a> FormatFields<'a> + 'static, { fn format_event( &self, ctx: &FmtContext<'_, S, N>, mut writer: format::Writer<'_>, event: &Event<'_>, ) -> fmt::Result { let metadata = event.metadata(); if metadata.level() != &Level::INFO { write!(&mut writer, "{} {}: ", metadata.level(), metadata.target())?; } // 自定义字段格式化,跳过`to`字段 let mut visitor = SkipToFieldVisitor { writer: &mut writer, formatter: ctx.field_format() }; event.record(&mut visitor); writeln!(writer) } } // 用于跳过`to`字段的Visitor struct SkipToFieldVisitor<'a, N> { writer: format::Writer<'a>, formatter: &'a N, } impl<'a, N> Visit for SkipToFieldVisitor<'a, N> where N: FormatFields<'a>, { fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { if field.name() != "to" { let _ = self.formatter.format_field(self.writer.by_ref(), field, value); } } fn record_str(&mut self, field: &tracing::field::Field, value: &str) { if field.name() != "to" { let _ = self.formatter.format_field(self.writer.by_ref(), field, value); } } } pub fn setup_logging(verbose: bool) { let env_filter = if verbose { "nixci=debug,nix_rs=debug" } else { "nixci=info,nix_rs=info" }; let builder = tracing_subscriber::fmt() .with_writer(DynamicWriter::new()) // 使用自定义动态Writer .with_max_level(Level::INFO) .with_env_filter(env_filter); if !verbose { builder.event_format(BareFormatter).init(); } else { builder.init() } }
代码说明
- DynamicWriter:实现
MakeWritertrait,通过make_writer_for方法解析日志事件的to字段,动态选择输出流,默认输出到stderr。 - ToFieldVisitor:负责从日志事件中提取
to字段的值,供DynamicWriter判断输出目标。 - SkipToFieldVisitor:在格式化日志时跳过
to字段,避免日志内容中出现多余的to="stdout"标识。 - setup_logging修改:将原本固定的
io::stderr替换为自定义的DynamicWriter::new(),实现动态输出逻辑。
修改完成后,即可通过tracing::info!(to: "stdout", "This message goes to stdout");指定日志输出到stdout,未指定to字段的日志默认输出到stderr。
内容的提问来源于stack exchange,提问作者Sridhar Ratnakumar
相关产品推荐
相关产品推荐

