feat(logging): tracing broadcast layer for real-time log streaming

This commit is contained in:
xiaoxixi 2026-07-26 21:50:03 +08:00
parent dff155a93c
commit bee11c7735
2 changed files with 73 additions and 7 deletions

View File

@ -23,6 +23,7 @@ prost = "0.14"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "local-time"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "local-time"] }
tracing-appender = "0.2" tracing-appender = "0.2"
time = { version = "0.3", features = ["formatting", "local-offset"] }
anyhow = "1.0" anyhow = "1.0"
mime_guess = "2.0" mime_guess = "2.0"
base64 = "0.22" base64 = "0.22"

View File

@ -1,27 +1,93 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::OnceLock;
use tokio::sync::broadcast;
use tracing::field::{Field, Visit};
use tracing::{Event, Subscriber};
use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;
use tracing_subscriber::{ use tracing_subscriber::{
EnvFilter, fmt, fmt::time::LocalTime, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, fmt, fmt::time::LocalTime, layer::SubscriberExt, util::SubscriberInitExt,
}; };
/// Get the default log directory path: ~/.picobot/logs #[derive(Clone, Debug)]
pub struct LogEvent {
pub ts: String,
pub level: String,
pub target: String,
pub message: String,
}
static LOG_TX: OnceLock<broadcast::Sender<LogEvent>> = OnceLock::new();
pub fn log_sender() -> Option<broadcast::Sender<LogEvent>> {
LOG_TX.get().cloned()
}
struct MessageVisitor(String);
impl Visit for MessageVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.0 = format!("{value:?}");
}
}
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "message" {
self.0 = value.to_string();
}
}
}
struct BroadcastLayer;
impl<S> Layer<S> for BroadcastLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let Some(tx) = LOG_TX.get() else {
return;
};
if tx.receiver_count() == 0 {
return;
}
let mut visitor = MessageVisitor(String::new());
event.record(&mut visitor);
let ts = time::OffsetDateTime::now_local()
.unwrap_or_else(|_| time::OffsetDateTime::now_utc())
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default();
let _ = tx.send(LogEvent {
ts,
level: event.metadata().level().to_string(),
target: event.metadata().target().to_string(),
message: visitor.0,
});
}
}
pub fn get_default_log_dir() -> PathBuf { pub fn get_default_log_dir() -> PathBuf {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
home.join(".picobot").join("logs") home.join(".picobot").join("logs")
} }
/// Get the default config file path: ~/.picobot/config.json
pub fn get_default_config_path() -> PathBuf { pub fn get_default_config_path() -> PathBuf {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
home.join(".picobot").join("config.json") home.join(".picobot").join("config.json")
} }
/// Initialize logging with file appender
/// Logs are written to ~/.picobot/logs/ with daily rotation
pub fn init_logging() { pub fn init_logging() {
let (tx, _) = broadcast::channel::<LogEvent>(1024);
let _ = LOG_TX.set(tx);
let log_dir = get_default_log_dir(); let log_dir = get_default_log_dir();
// Create log directory if it doesn't exist
if !log_dir.exists() if !log_dir.exists()
&& let Err(e) = std::fs::create_dir_all(&log_dir) && let Err(e) = std::fs::create_dir_all(&log_dir)
{ {
@ -32,10 +98,8 @@ pub fn init_logging() {
); );
} }
// Create file appender with daily rotation
let file_appender = RollingFileAppender::new(Rotation::DAILY, &log_dir, "picobot.log"); let file_appender = RollingFileAppender::new(Rotation::DAILY, &log_dir, "picobot.log");
// Build subscriber with both console and file output
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let file_layer = fmt::layer() let file_layer = fmt::layer()
@ -55,6 +119,7 @@ pub fn init_logging() {
.with(env_filter) .with(env_filter)
.with(console_layer) .with(console_layer)
.with(file_layer) .with(file_layer)
.with(BroadcastLayer)
.init(); .init();
tracing::info!("Logging initialized. Log directory: {}", log_dir.display()); tracing::info!("Logging initialized. Log directory: {}", log_dir.display());