Skip to main content

futu_core/
log.rs

1use std::path::Path;
2use std::sync::OnceLock;
3
4use crate::audit_log_writer;
5use tracing_subscriber::{
6    EnvFilter, Layer, filter::filter_fn, fmt, layer::SubscriberExt, reload, util::SubscriberInitExt,
7};
8
9/// 全局人类可读日志时间戳:使用系统本地时区并输出 RFC3339 offset。
10///
11/// 默认 tracing formatter 使用 UTC (`Z`);本项目排障通常发生在 Asia/Shanghai /
12/// Asia/Hong_Kong 时区,UTC 时间会让用户对照真机事件时多一层换算。这里不写死
13/// `+08:00`,而是读取 OS local timezone;部署到其他地区时仍按当地日志习惯输出。
14#[derive(Clone, Copy, Debug)]
15pub struct LocalRfc3339Timer;
16
17impl fmt::time::FormatTime for LocalRfc3339Timer {
18    fn format_time(&self, w: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
19        write!(
20            w,
21            "{}",
22            chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true)
23        )
24    }
25}
26
27/// **v1.4.106 codex 1110 F7 [P2]** — 全局 EnvFilter reload handle,
28/// 支持运行时通过 `RemoteCmd set_log_level` 修改 tracing filter。
29///
30/// 在 `init_logging_with_level` / `init_json_logging_with_level` /
31/// `init_logging_with_audit` 三个入口里 install reload layer 时写入;
32/// `set_runtime_log_level` 在外部业务 handler 里 reload。
33///
34/// **唯一性约束**:tracing global subscriber 只能 init 一次,所以同进程仅一份
35/// reload handle。重复 init 会被 `tracing_subscriber::registry().try_init()`
36/// 失败,此 handle 也只 set 一次(`OnceLock`)。
37static GLOBAL_RELOAD_HANDLE: OnceLock<reload::Handle<EnvFilter, tracing_subscriber::Registry>> =
38    OnceLock::new();
39
40/// 解析 RemoteCmd 风格的 level 字符串("no" / "debug" / "info" / ...)到
41/// EnvFilter 字符串("off" / "debug" / "info" / ...)。
42///
43/// 对齐 C++ RemoteCmd `set_log_level` 支持的 6 个 level 值。
44fn map_remote_level_to_env_filter(level: &str) -> Option<&'static str> {
45    match level.to_lowercase().as_str() {
46        "no" => Some("off"),
47        "debug" => Some("debug"),
48        "info" => Some("info"),
49        "warning" => Some("warn"),
50        "error" => Some("error"),
51        "fatal" => Some("error"), // tracing 没 fatal,等价 error
52        _ => None,
53    }
54}
55
56/// **v1.4.106 codex 1110 F7 [P2] Stable API** — 运行时修改 tracing filter level。
57///
58/// - 接受 RemoteCmd 风格的 level 值("no" / "debug" / "info" / "warning" /
59///   "error" / "fatal"),内部映射到 `EnvFilter` 接受的等价值。
60/// - 修改成功 → 返 `Ok(applied_filter_string)`,调用方可作 verify 提示。
61/// - 修改失败 → 返 `Err(reason)`:
62///   - "level invalid": level 字符串不在合法集合
63///   - "reload handle not initialized": init_logging 没用 reload-aware 入口
64///     (或 .init() 失败)
65///   - "reload failed: <e>": tracing_subscriber 内部 reload 错(一般极罕见)
66///
67/// 调用此函数后,所有现有 `tracing::*!` 宏的过滤器会立刻按新 filter 工作,
68/// 不需要重启进程。这是真"effect",配 `RemoteCmd set_log_level` 修复
69/// silent-success 反模式。
70pub fn set_runtime_log_level(level: &str) -> Result<String, String> {
71    let mapped = match map_remote_level_to_env_filter(level) {
72        Some(s) => s,
73        None => return Err("level invalid".to_string()),
74    };
75
76    let handle = match GLOBAL_RELOAD_HANDLE.get() {
77        Some(h) => h,
78        None => return Err("reload handle not initialized".to_string()),
79    };
80
81    let new_filter = EnvFilter::try_new(mapped)
82        .map_err(|e| format!("EnvFilter parse failed for '{mapped}': {e}"))?;
83
84    handle
85        .reload(new_filter)
86        .map_err(|e| format!("reload failed: {e}"))?;
87
88    Ok(mapped.to_string())
89}
90
91/// **v1.4.106 codex 1110 F7 [P2]** — query 当前 reload handle 是否已 install。
92/// handler 用来给用户 helpful error message。
93pub fn is_runtime_reload_available() -> bool {
94    GLOBAL_RELOAD_HANDLE.get().is_some()
95}
96
97/// **Stable API** — 初始化日志系统(stderr fmt layer)。
98///
99/// 优先级: RUST_LOG 环境变量 > level 参数 > 默认 info
100///
101/// v1.4.106 codex 1110 F7 [P2]:install runtime reload handle(用于
102/// `RemoteCmd set_log_level` 真切 filter)。
103pub fn init_logging_with_level(level: &str) {
104    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
105    let (filter_layer, reload_handle) = reload::Layer::new(filter);
106
107    let fmt_layer = fmt::layer()
108        .with_timer(LocalRfc3339Timer)
109        .with_target(true)
110        .with_thread_ids(true)
111        .with_file(true)
112        .with_line_number(true)
113        .with_writer(std::io::stderr);
114
115    let registry = tracing_subscriber::registry()
116        .with(filter_layer)
117        .with(fmt_layer);
118    if registry.try_init().is_ok() {
119        // 仅 init 成功时 set 全局 handle(重复 init 不覆盖)。
120        if GLOBAL_RELOAD_HANDLE.set(reload_handle).is_err() {
121            eprintln!(
122                "futu-opend: runtime log reload handle already installed; keeping existing handle"
123            );
124        }
125    }
126}
127
128/// **Stable API** — 初始化日志系统(默认 info 级别)。examples / 测试程序
129/// 常用入口。
130pub fn init_logging() {
131    init_logging_with_level("info");
132}
133
134/// **Stable API** — 初始化 JSON 格式日志(生产环境 / 日志聚合用)。
135///
136/// v1.4.106 codex 1110 F7 [P2]:install runtime reload handle。
137pub fn init_json_logging_with_level(level: &str) {
138    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
139    let (filter_layer, reload_handle) = reload::Layer::new(filter);
140
141    let fmt_layer = fmt::layer()
142        .json()
143        .with_timer(LocalRfc3339Timer)
144        .with_target(true)
145        .with_thread_ids(true);
146
147    let registry = tracing_subscriber::registry()
148        .with(filter_layer)
149        .with(fmt_layer);
150    if registry.try_init().is_ok() && GLOBAL_RELOAD_HANDLE.set(reload_handle).is_err() {
151        eprintln!(
152            "futu-opend: runtime log reload handle already installed; keeping existing handle"
153        );
154    }
155}
156
157/// **Stable API** — 初始化 JSON 格式日志(默认 info 级别)。
158pub fn init_json_logging() {
159    init_json_logging_with_level("info");
160}
161
162/// **Stable API** — 初始化日志 + 可选的 audit JSONL 文件。
163///
164/// - 常规事件走 stderr(与 [`init_logging_with_level`] 一致)
165/// - 如果 `audit_path` 传了,额外加一个 JSON 层,只捕获 `target = "futu_audit"` 的
166///   事件并写到文件 / 目录。返回的 `WorkerGuard` 必须保留到进程退出,否则
167///   tracing-appender 的后台线程可能丢事件。
168/// - 传 `None` 时返回 `Ok(None)`,等价于 `init_logging_with_level`。
169pub fn init_logging_with_audit(
170    level: &str,
171    audit_path: Option<&Path>,
172) -> std::io::Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
173    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
174    // v1.4.106 codex 1110 F7 [P2]: install reload handle for runtime set_log_level.
175    let (filter_layer, reload_handle) = reload::Layer::new(filter);
176
177    let fmt_layer = fmt::layer()
178        .with_timer(LocalRfc3339Timer)
179        .with_target(true)
180        .with_thread_ids(true)
181        .with_file(true)
182        .with_line_number(true)
183        .with_writer(std::io::stderr);
184
185    let registry = tracing_subscriber::registry()
186        .with(filter_layer)
187        .with(fmt_layer);
188
189    let _init_ok = if let Some(path) = audit_path {
190        // 用 shared audit writer(单文件 or 每日滚动目录,0600/0700 权限收紧)。
191        let (writer, guard) = audit_log_writer::open_writer(path)?;
192        let audit_layer = fmt::layer()
193            .json()
194            .flatten_event(true)
195            .with_current_span(false)
196            .with_span_list(false)
197            .with_target(true)
198            .with_writer(writer)
199            .with_filter(filter_fn(|meta| meta.target() == "futu_audit"));
200        let init_ok = registry.with(audit_layer).try_init().is_ok();
201        if init_ok && GLOBAL_RELOAD_HANDLE.set(reload_handle).is_err() {
202            eprintln!(
203                "futu-opend: runtime log reload handle already installed; keeping existing handle"
204            );
205        }
206        return Ok(if init_ok { Some(guard) } else { None });
207    } else {
208        let ok = registry.try_init().is_ok();
209        if ok && GLOBAL_RELOAD_HANDLE.set(reload_handle).is_err() {
210            eprintln!(
211                "futu-opend: runtime log reload handle already installed; keeping existing handle"
212            );
213        }
214        ok
215    };
216    Ok(None)
217}
218
219#[cfg(test)]
220mod tests;