mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 19:45:34 +08:00
feat(logging): persist diagnostics across restarts and redact secrets
Backend half of the logging overhaul. Retention: - Keep the last 4 rotated files at 20MB each and stop deleting logs on startup, so a crash's prior-run logs survive a restart. - Apply the persisted log level right after plugin registration instead of at the end of setup. - Bound crash.log with size-based rotation (5MB x 2). Secret redaction on every log path: - Proxy: strip userinfo/query from upstream URLs (keep path for diagnostics), exact-match redact known auth.api_key/access_token, classify request/response bodies instead of logging them, header allowlist. - Redact the Gemini `?key=` in cache-trace endpoints. - Omit MCP custom-field values (headers may carry tokens). - Redact deeplink and model-fetch URLs before logging. Docs: - FAQ points users to the persistent crash.log for support workflows.
This commit is contained in:
+103
-8
@@ -3,16 +3,19 @@
|
||||
//! 在应用崩溃时捕获 panic 信息并记录到 `<app_config_dir>/crash.log` 文件中(默认 `~/.cc-switch/crash.log`)。
|
||||
//! 便于用户和开发者诊断闪退问题。
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::panic;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
/// 应用版本号(从 Cargo.toml 读取)
|
||||
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const CRASH_LOG_MAX_SIZE: u64 = 5 * 1024 * 1024;
|
||||
const CRASH_LOG_ARCHIVES_TO_KEEP: usize = 2;
|
||||
|
||||
static APP_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
static CRASH_LOG_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
pub fn init_app_config_dir(dir: PathBuf) {
|
||||
let _ = APP_CONFIG_DIR.set(dir);
|
||||
@@ -38,6 +41,50 @@ fn get_crash_log_path() -> PathBuf {
|
||||
get_app_config_dir().join("crash.log")
|
||||
}
|
||||
|
||||
fn rotated_crash_log_path(path: &Path, index: usize) -> PathBuf {
|
||||
let mut rotated = path.as_os_str().to_os_string();
|
||||
rotated.push(format!(".{index}"));
|
||||
PathBuf::from(rotated)
|
||||
}
|
||||
|
||||
fn rotate_crash_log_if_needed_with_limit(
|
||||
path: &Path,
|
||||
max_size: u64,
|
||||
archives_to_keep: usize,
|
||||
) -> std::io::Result<()> {
|
||||
let size = match fs::metadata(path) {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
if size < max_size || archives_to_keep == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for index in (1..=archives_to_keep).rev() {
|
||||
let source = if index == 1 {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
rotated_crash_log_path(path, index - 1)
|
||||
};
|
||||
if !source.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let destination = rotated_crash_log_path(path, index);
|
||||
if destination.exists() {
|
||||
fs::remove_file(&destination)?;
|
||||
}
|
||||
fs::rename(source, destination)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rotate_crash_log_if_needed(path: &Path) -> std::io::Result<()> {
|
||||
rotate_crash_log_if_needed_with_limit(path, CRASH_LOG_MAX_SIZE, CRASH_LOG_ARCHIVES_TO_KEEP)
|
||||
}
|
||||
|
||||
/// 获取日志目录路径
|
||||
pub fn get_log_dir() -> PathBuf {
|
||||
get_app_config_dir().join("logs")
|
||||
@@ -167,12 +214,23 @@ Stack Trace (Backtrace)
|
||||
"#
|
||||
);
|
||||
|
||||
// 写入文件(追加模式)
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
|
||||
let _ = file.write_all(crash_entry.as_bytes());
|
||||
let _ = file.flush();
|
||||
// 将 size check、轮转和追加合成同一个临界区,避免多线程同时 panic
|
||||
// 时两个 hook 竞争 rename 而丢失归档。
|
||||
let crash_log_guard = CRASH_LOG_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
let _ = rotate_crash_log_if_needed(&log_path);
|
||||
let saved =
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
|
||||
let _ = file.write_all(crash_entry.as_bytes());
|
||||
let _ = file.flush();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
drop(crash_log_guard);
|
||||
|
||||
// 记录日志文件位置到 stderr
|
||||
if saved {
|
||||
eprintln!("\n[CC-Switch] Crash log saved to: {}", log_path.display());
|
||||
}
|
||||
|
||||
@@ -202,4 +260,41 @@ mod tests {
|
||||
assert!(info.contains("Arch:"));
|
||||
assert!(info.contains("App Version:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crash_log_rotation_keeps_bounded_archives() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("crash.log");
|
||||
|
||||
fs::write(&path, b"first").unwrap();
|
||||
rotate_crash_log_if_needed_with_limit(&path, 4, 2).unwrap();
|
||||
assert!(!path.exists());
|
||||
assert_eq!(
|
||||
fs::read(rotated_crash_log_path(&path, 1)).unwrap(),
|
||||
b"first"
|
||||
);
|
||||
|
||||
fs::write(&path, b"second").unwrap();
|
||||
rotate_crash_log_if_needed_with_limit(&path, 4, 2).unwrap();
|
||||
assert_eq!(
|
||||
fs::read(rotated_crash_log_path(&path, 1)).unwrap(),
|
||||
b"second"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(rotated_crash_log_path(&path, 2)).unwrap(),
|
||||
b"first"
|
||||
);
|
||||
|
||||
fs::write(&path, b"third").unwrap();
|
||||
rotate_crash_log_if_needed_with_limit(&path, 4, 2).unwrap();
|
||||
assert_eq!(
|
||||
fs::read(rotated_crash_log_path(&path, 1)).unwrap(),
|
||||
b"third"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(rotated_crash_log_path(&path, 2)).unwrap(),
|
||||
b"second"
|
||||
);
|
||||
assert!(!rotated_crash_log_path(&path, 3).exists());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user