如何在tracing-rs的Subscriber/Layer中按swap_id输出JSON格式日志
解决方案:基于swap_id分文件的JSON日志记录(tracing-rs)
以下是修改后的完整代码,复用tracing-subscriber内置的JSON格式化器,实现按swap_id将日志写入对应文件:
use anyhow::Result; use std::fmt::Debug; use std::fs::OpenOptions; use std::path::{Path, PathBuf}; use tracing::field::Field; use tracing::span::Attributes; use tracing::{Id, Level, Subscriber}; use tracing_subscriber::fmt::format::Json; use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::layer::{Context, SubscriberExt}; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, FmtSubscriber, Layer, Registry}; use tracing_subscriber::fmt::FormatEvent; struct SwapIdVisitor { swap_id: Option<String>, } pub struct FileLayer { dir: PathBuf, json_format: Json<UtcTime>, } impl FileLayer { pub fn new(dir: impl AsRef<Path>) -> Self { // 配置JSON格式化器,和控制台日志的时间格式、字段保持一致 let json_format = Json::new() .with_timer(UtcTime::rfc_3339()) .with_target(false); Self { dir: dir.as_ref().to_path_buf(), json_format, } } fn get_log_path(&self, swap_id: &str) -> PathBuf { self.dir.join(format!("swap-{}.log", swap_id)) } } impl<S> Layer<S> for FileLayer where S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, { fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { let mut visitor = SwapIdVisitor { swap_id: None }; attrs.record(&mut visitor); if let Some(swap_id) = visitor.swap_id { if let Some(span) = ctx.span(id) { span.extensions_mut().insert(swap_id); } } } fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) { // 找到当前span并获取swap_id if let Some(current_span) = ctx.current_span().id().and_then(|id| ctx.span(id)) { if let Some(swap_id) = current_span.extensions().get::<String>() { // 打开对应swap_id的日志文件,追加模式 let log_file = OpenOptions::new() .append(true) .create(true) .open(self.get_log_path(swap_id)) .expect("Failed to open swap log file"); // 使用内置JSON格式化器处理事件并写入文件 self.json_format .format_event(&ctx, event, &mut std::io::BufWriter::new(log_file)) .expect("Failed to format event to JSON"); } } } } impl tracing::field::Visit for SwapIdVisitor { // 优先处理字符串类型的swap_id,避免额外引号 fn record_str(&mut self, field: &Field, value: &str) { if field.name() == "swap_id" { self.swap_id = Some(value.to_string()); } } // 处理非字符串类型的swap_id(如数字) fn record_debug(&mut self, field: &Field, value: &dyn Debug) { if field.name() == "swap_id" { self.swap_id = Some(format!("{}", value)); } } } pub fn init(debug: bool, json: bool, dir: impl AsRef<Path>) -> Result<()> { let level = if debug { Level::DEBUG } else { Level::INFO }; let is_terminal = atty::is(atty::Stream::Stderr); let file_layer = FileLayer::new(dir.as_ref()); // 构建控制台订阅器,根据参数选择是否用JSON格式 let subscriber_builder = FmtSubscriber::builder() .with_env_filter(format!("swap={}", level)) .with_writer(std::io::stderr) .with_ansi(is_terminal) .with_timer(UtcTime::rfc_3339()) .with_target(false); let subscriber = if json { subscriber_builder.json().finish() } else { subscriber_builder.finish() }; subscriber.with(file_layer).init(); tracing::info!("Logging initialized to {}", dir.as_ref().display()); Ok(()) }
关键改动说明
- 集成内置JSON格式化器:在
FileLayer中持有预先配置好的Json<UtcTime>实例,复用tracing-subscriber的成熟格式化逻辑,避免手动拼接JSON字符串的错误。 - 动态文件写入:在
on_event中根据swap_id打开对应日志文件,使用BufWriter优化写入性能,直接调用format_event完成格式化与写入。 - 修正swap_id记录逻辑:新增
record_str方法处理字符串类型的swap_id,避免{:?}带来的额外引号;非字符串类型用format!("{}", value)保证输出格式简洁。 - 控制台日志可配置:利用
init函数的json参数,让控制台日志也可以选择是否输出JSON格式,和文件日志保持配置一致性。
内容的提问来源于stack exchange,提问作者binarybaron
相关产品推荐
相关产品推荐

