futu_core/
log.rs

1use std::path::Path;
2
3use tracing_subscriber::{
4    filter::filter_fn, fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
5};
6
7/// 初始化日志系统
8///
9/// 优先级: RUST_LOG 环境变量 > level 参数 > 默认 info
10pub fn init_logging_with_level(level: &str) {
11    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
12
13    fmt()
14        .with_env_filter(filter)
15        .with_target(true)
16        .with_thread_ids(true)
17        .with_file(true)
18        .with_line_number(true)
19        .init();
20}
21
22/// 初始化日志系统(默认 info 级别)
23pub fn init_logging() {
24    init_logging_with_level("info");
25}
26
27/// 初始化 JSON 格式日志(适合生产环境)
28pub fn init_json_logging_with_level(level: &str) {
29    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
30
31    fmt()
32        .json()
33        .with_env_filter(filter)
34        .with_target(true)
35        .with_thread_ids(true)
36        .init();
37}
38
39/// 初始化 JSON 格式日志(默认 info 级别)
40pub fn init_json_logging() {
41    init_json_logging_with_level("info");
42}
43
44/// 初始化日志 + 可选的 audit JSONL 文件
45///
46/// - 常规事件走 stderr(与 [`init_logging_with_level`] 一致)
47/// - 如果 `audit_path` 传了,额外加一个 JSON 层,只捕获 `target = "futu_audit"` 的
48///   事件并写到文件 / 目录。返回的 `WorkerGuard` 必须保留到进程退出,否则
49///   tracing-appender 的后台线程可能丢事件。
50/// - 传 `None` 时返回 `Ok(None)`,等价于 `init_logging_with_level`。
51pub fn init_logging_with_audit(
52    level: &str,
53    audit_path: Option<&Path>,
54) -> std::io::Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
55    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
56
57    let fmt_layer = fmt::layer()
58        .with_target(true)
59        .with_thread_ids(true)
60        .with_file(true)
61        .with_line_number(true)
62        .with_writer(std::io::stderr);
63
64    let registry = tracing_subscriber::registry().with(filter).with(fmt_layer);
65
66    if let Some(path) = audit_path {
67        // 用 futu-auth 的 open_writer(单文件 or 每日滚动目录)
68        let (writer, guard) = open_audit_writer(path)?;
69        let audit_layer = fmt::layer()
70            .json()
71            .flatten_event(true)
72            .with_current_span(false)
73            .with_span_list(false)
74            .with_target(true)
75            .with_writer(writer)
76            .with_filter(filter_fn(|meta| meta.target() == "futu_audit"));
77        registry.with(audit_layer).init();
78        Ok(Some(guard))
79    } else {
80        registry.init();
81        Ok(None)
82    }
83}
84
85/// 打开 audit 日志 writer。为了避免 futu-core 反向依赖 futu-auth,
86/// 这里复制了 `futu_auth::audit::open_writer` 的启发式逻辑。两者行为必须一致,
87/// 任一改动都要同步对方。
88fn open_audit_writer(
89    path: &Path,
90) -> std::io::Result<(
91    tracing_appender::non_blocking::NonBlocking,
92    tracing_appender::non_blocking::WorkerGuard,
93)> {
94    let is_dir_hint = path.is_dir()
95        || path.as_os_str().to_string_lossy().ends_with('/')
96        || path.extension().is_none();
97    if is_dir_hint {
98        std::fs::create_dir_all(path)?;
99        let appender = tracing_appender::rolling::daily(path, "futu-audit.log");
100        Ok(tracing_appender::non_blocking(appender))
101    } else {
102        if let Some(parent) = path.parent() {
103            if !parent.as_os_str().is_empty() {
104                std::fs::create_dir_all(parent)?;
105            }
106        }
107        let file = std::fs::OpenOptions::new()
108            .append(true)
109            .create(true)
110            .open(path)?;
111        Ok(tracing_appender::non_blocking(file))
112    }
113}