PicoBot/src/logging.rs
oudecheng 29060315a3 feat(observability): 端到端可观测性整改,修复 trace_id 断链与指标配对
- 传播 trace_id:BusToolCallEmitter/SubAgentEmitter/processor 全链路设置

- AgentEnd 配对:补发 5 个 cancel/defensive 路径,闭合 AgentStart 指标

- LLM 计时修正:attempt_start 移入 retry 循环,排除退避等待时间

- /metrics auth:非 loopback 部署时纳入 Bearer token 校验

- recorder 复用:OnceLock 缓存 PrometheusHandle,热重启后不再返回 503

- 结构化日志:新增 tracing_ctx + JSON 日志格式支持
2026-08-12 08:26:42 +08:00

136 lines
4.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use chrono::Utc;
use chrono_tz::Tz;
use std::path::PathBuf;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{
fmt, fmt::time::FormatTime, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
};
use crate::config::LogFormat;
#[derive(Clone, Copy, Debug)]
struct ConfiguredTimestamp {
timezone: Tz,
}
impl FormatTime for ConfiguredTimestamp {
fn format_time(
&self,
writer: &mut tracing_subscriber::fmt::format::Writer<'_>,
) -> std::fmt::Result {
write!(
writer,
"{}",
Utc::now()
.with_timezone(&self.timezone)
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
)
}
}
/// Get the default log directory path: ~/.picobot/logs
pub fn get_default_log_dir() -> PathBuf {
let home = crate::platform::picobot_home_dir();
home.join(".picobot").join("logs")
}
/// Get the default config file path: ~/.picobot/config.json
pub fn get_default_config_path() -> PathBuf {
let home = crate::platform::picobot_home_dir();
home.join(".picobot").join("config.json")
}
/// Initialize logging with file appender
/// Logs are written to ~/.picobot/logs/ with daily rotation
///
/// `log_format` 控制文件日志格式Text默认或 Json便于日志聚合
/// 控制台始终使用文本格式(便于人读)。
pub fn init_logging(timezone: Tz, log_format: LogFormat) {
use std::sync::Once;
static INIT: Once = Once::new();
let mut initialized = false;
INIT.call_once(|| {
initialized = true;
});
if !initialized {
// Already initialized (e.g. after gateway restart), skip
return;
}
let log_dir = get_default_log_dir();
// Create log directory if it doesn't exist
if !log_dir.exists() {
if let Err(e) = std::fs::create_dir_all(&log_dir) {
eprintln!(
"Warning: Failed to create log directory {}: {}",
log_dir.display(),
e
);
}
}
// Create file appender with daily rotation
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"));
// 文件层:根据 log_format 选择 text 或 json
let file_layer = match log_format {
LogFormat::Json => fmt::layer()
.with_writer(file_appender)
.with_timer(ConfiguredTimestamp { timezone })
.with_ansi(false)
.with_target(true)
.with_level(true)
.with_thread_ids(true)
.json()
.boxed(),
LogFormat::Text => fmt::layer()
.with_writer(file_appender)
.with_timer(ConfiguredTimestamp { timezone })
.with_ansi(false)
.with_target(true)
.with_level(true)
.with_thread_ids(true)
.boxed(),
};
// 控制台层:始终文本格式
let console_layer = fmt::layer()
.with_timer(ConfiguredTimestamp { timezone })
.with_target(true)
.with_level(true);
tracing_subscriber::registry()
.with(env_filter)
.with(console_layer)
.with(file_layer)
.init();
tracing::info!(
log_format = ?log_format,
log_dir = %log_dir.display(),
"Logging initialized"
);
}
/// Initialize logging without file output (console only)
pub fn init_logging_console_only(timezone: Tz) {
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let console_layer = fmt::layer()
.with_timer(ConfiguredTimestamp { timezone })
.with_target(true)
.with_level(true);
tracing_subscriber::registry()
.with(env_filter)
.with(console_layer)
.init();
tracing::info!("Logging initialized (console only)");
}