app_logger.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. //! 应用全局日志模块
  2. //! 日志文件放在 exe 同目录下,每天一个日志文件
  3. //! 格式:[时间戳] [LEVEL] 消息内容
  4. use chrono::Local;
  5. use std::fs::{self, OpenOptions};
  6. use std::io::Write;
  7. use std::path::PathBuf;
  8. use std::sync::Mutex;
  9. #[cfg(target_os = "windows")]
  10. use std::sync::atomic::{AtomicBool, Ordering};
  11. /// 全局日志写入锁
  12. static LOG_FILE: Mutex<Option<PathBuf>> = Mutex::new(None);
  13. /// VEH 是否已注册(防止重复注册)
  14. #[cfg(target_os = "windows")]
  15. static VEH_REGISTERED: AtomicBool = AtomicBool::new(false);
  16. /// 进程启动时间(用于计算运行时间)
  17. static PROCESS_START_TIME: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
  18. /// 获取 exe 所在目录
  19. fn get_exe_dir() -> PathBuf {
  20. if let Ok(exe_path) = std::env::current_exe() {
  21. if let Some(parent) = exe_path.parent() {
  22. return parent.to_path_buf();
  23. }
  24. }
  25. std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
  26. }
  27. /// 获取日志目录
  28. fn get_log_dir() -> PathBuf {
  29. get_exe_dir().join("logs")
  30. }
  31. /// 获取今天的日志文件路径
  32. fn get_today_log_path() -> Result<PathBuf, String> {
  33. let log_dir = get_log_dir();
  34. fs::create_dir_all(&log_dir)
  35. .map_err(|e| format!("创建日志目录失败: {}", e))?;
  36. let today = Local::now().format("%Y-%m-%d").to_string();
  37. Ok(log_dir.join(format!("app_{}.log", today)))
  38. }
  39. /// 初始化日志系统
  40. pub fn init_app_logger() -> Result<(), String> {
  41. let log_path = get_today_log_path()?;
  42. if !log_path.exists() {
  43. fs::write(&log_path, "").map_err(|e| format!("创建日志文件失败: {}", e))?;
  44. }
  45. let mut guard = LOG_FILE.lock().map_err(|e| format!("获取日志锁失败: {}", e))?;
  46. *guard = Some(log_path.clone());
  47. let start_msg = format!(
  48. "[{}] [INFO] ==================== 应用启动 ====================\n",
  49. Local::now().format("%Y-%m-%d %H:%M:%S")
  50. );
  51. write_to_log_file(&log_path, &start_msg);
  52. Ok(())
  53. }
  54. /// 获取当前日志文件路径
  55. pub fn get_current_log_path() -> Option<String> {
  56. LOG_FILE
  57. .lock()
  58. .ok()
  59. .and_then(|guard| guard.clone())
  60. .map(|p| p.to_string_lossy().to_string())
  61. }
  62. /// 写入日志到文件(不带时间戳前缀,供内部/panic 使用)
  63. fn write_to_log_file(path: &PathBuf, content: &str) {
  64. if let Ok(mut file) = OpenOptions::new()
  65. .create(true)
  66. .append(true)
  67. .open(path)
  68. {
  69. let _ = file.write_all(content.as_bytes());
  70. let _ = file.flush();
  71. }
  72. }
  73. /// 写入带时间戳的日志行
  74. fn write_log_line(level: &str, message: &str) {
  75. let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
  76. let log_line = format!("[{}] [{}] {}\n", timestamp, level, message);
  77. if let Ok(guard) = LOG_FILE.lock() {
  78. if let Some(path) = guard.as_ref() {
  79. write_to_log_file(path, &log_line);
  80. }
  81. }
  82. eprintln!("{}", log_line.trim());
  83. }
  84. /// Info 级别日志
  85. pub fn info(msg: &str) {
  86. write_log_line("INFO", msg);
  87. }
  88. /// Warning 级别日志
  89. pub fn warn(msg: &str) {
  90. write_log_line("WARN", msg);
  91. }
  92. /// Error 级别日志
  93. pub fn error(msg: &str) {
  94. write_log_line("ERROR", msg);
  95. }
  96. /// Debug 级别日志
  97. pub fn debug(msg: &str) {
  98. write_log_line("DEBUG", msg);
  99. }
  100. /// 记录 panic 信息
  101. pub fn log_panic(panic_info: &std::panic::PanicHookInfo) {
  102. let location = panic_info
  103. .location()
  104. .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
  105. .unwrap_or_else(|| "unknown location".to_string());
  106. let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
  107. s.to_string()
  108. } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
  109. s.clone()
  110. } else {
  111. "Unknown panic payload".to_string()
  112. };
  113. let full_msg = format!(
  114. "PANIC at {}: {}\nBacktrace:\n{:?}",
  115. location,
  116. message,
  117. std::backtrace::Backtrace::capture()
  118. );
  119. write_log_line("PANIC", &full_msg);
  120. }
  121. /// 设置 panic hook
  122. pub fn setup_panic_hook() {
  123. std::panic::set_hook(Box::new(|panic_info| {
  124. log_panic(&panic_info);
  125. }));
  126. }
  127. // ============================================================
  128. // Windows 顶层异常处理(Vectored Exception Handler)
  129. // 能捕获 Rust panic 无法覆盖的崩溃,如访问违规、栈溢出等
  130. // ============================================================
  131. #[cfg(target_os = "windows")]
  132. const EXCEPTION_ACCESS_VIOLATION: u32 = 0xC0000005;
  133. #[cfg(target_os = "windows")]
  134. const EXCEPTION_STACK_OVERFLOW: u32 = 0xC00000FD;
  135. #[cfg(target_os = "windows")]
  136. const EXCEPTION_INT_DIVIDE_BY_ZERO: u32 = 0xC0000094;
  137. #[cfg(target_os = "windows")]
  138. const EXCEPTION_ILLEGAL_INSTRUCTION: u32 = 0xC000001D;
  139. #[cfg(target_os = "windows")]
  140. const EXCEPTION_BREAKPOINT: u32 = 0x80000003;
  141. #[cfg(target_os = "windows")]
  142. fn exception_code_to_name(code: u32) -> &'static str {
  143. match code {
  144. 0xC0000005 => "ACCESS_VIOLATION",
  145. 0xC00000FD => "STACK_OVERFLOW",
  146. 0xC0000094 => "INT_DIVIDE_BY_ZERO",
  147. 0xC000001D => "ILLEGAL_INSTRUCTION",
  148. 0x80000003 => "BREAKPOINT",
  149. 0xC0000008 => "INVALID_HANDLE",
  150. 0xC000001E => "LOCK_VIOLATION",
  151. 0xC0000025 => "NOT_MAPPED_VIEW",
  152. 0xC000000D => "DATA_TYPE_MISALIGNMENT",
  153. 0xC0000409 => "STACK_BUFFER_OVERRUN",
  154. 0xE0000001 => "GUARD_PAGE_VIOLATION",
  155. 0xC0000374 => "HEAP_CORRUPTION",
  156. 0xC0000142 => "DLL_NOT_FOUND",
  157. 0xC0000135 => "ENTRYPOINT_NOT_FOUND",
  158. _ => "UNKNOWN",
  159. }
  160. }
  161. #[cfg(target_os = "windows")]
  162. fn format_windows_exception_record(exception_pointers: *mut std::ffi::c_void) -> String {
  163. use std::ffi::c_void;
  164. // LPEXCEPTION_POINTERS 结构:
  165. // typedef struct _EXCEPTION_POINTERS {
  166. // PEXCEPTION_RECORD ExceptionRecord; // 偏移 0
  167. // PCONTEXT ContextRecord; // 偏移 8 (64位)
  168. // } EXCEPTION_POINTERS;
  169. let exc_record_ptr = unsafe { *(exception_pointers as *const *const c_void) };
  170. if exc_record_ptr.is_null() {
  171. return "ExceptionRecord is null".to_string();
  172. }
  173. // EXCEPTION_RECORD 结构 (前几个字段):
  174. // DWORD ExceptionCode; // 偏移 0, 4字节
  175. // DWORD ExceptionFlags; // 偏移 4, 4字节
  176. // struct _EXCEPTION_RECORD *ExceptionRecord; // 偏移 8, 8字节 (64位)
  177. // PVOID ExceptionAddress; // 偏移 16, 8字节 (64位)
  178. // DWORD NumberParameters; // 偏移 24, 4字节
  179. // ULONG_PTR ExceptionInformation[15]; // 偏移 32
  180. let exc_code = unsafe { *(exc_record_ptr as *const u32) };
  181. let exc_flags = unsafe { *((exc_record_ptr as *const u32).offset(1)) };
  182. let exc_addr = unsafe { *((exc_record_ptr as *const *const c_void).offset(2)) };
  183. let mut info = format!(
  184. "ExceptionCode=0x{:08X} ({})\n ExceptionFlags=0x{:08X}\n ExceptionAddress={:?}",
  185. exc_code,
  186. exception_code_to_name(exc_code),
  187. exc_flags,
  188. exc_addr
  189. );
  190. if exc_code == EXCEPTION_ACCESS_VIOLATION {
  191. // ExceptionInformation[0]: 0 = read, 1 = write
  192. // ExceptionInformation[1]: 访问的虚拟地址
  193. let read_write = unsafe { *((exc_record_ptr as *const u64).offset(4)) };
  194. let virt_addr = unsafe { *((exc_record_ptr as *const *const c_void).offset(5)) };
  195. info.push_str(&format!(
  196. "\n Attempted {} at address {:?}",
  197. if read_write == 0 { "read" } else { "write" },
  198. virt_addr
  199. ));
  200. }
  201. info
  202. }
  203. #[cfg(target_os = "windows")]
  204. extern "system" fn veh_exception_handler(
  205. exception_pointers: *mut std::ffi::c_void,
  206. ) -> i32 {
  207. use std::ffi::c_void;
  208. // LPEXCEPTION_POINTERS 结构
  209. let exc_record_ptr = unsafe { *(exception_pointers as *const *const c_void) };
  210. if exc_record_ptr.is_null() {
  211. // 记录基本信息
  212. let msg = format!(
  213. "==================== WINDOWS CRASH ====================\n\
  214. ExceptionRecord is null\n\
  215. ProcessInfo:\n\
  216. PID={}\n\
  217. ThreadId={:?}\n\
  218. ========================================================\n",
  219. std::process::id(),
  220. std::thread::current().id()
  221. );
  222. eprintln!("{}", msg);
  223. return 0; // EXCEPTION_CONTINUE_SEARCH
  224. }
  225. let exc_code = unsafe { *(exc_record_ptr as *const u32) };
  226. let exc_addr = unsafe { *((exc_record_ptr as *const *const c_void).offset(2)) };
  227. let record_str = format_windows_exception_record(exception_pointers);
  228. let backtrace = format!("{:?}", std::backtrace::Backtrace::capture());
  229. let msg = format!(
  230. "==================== WINDOWS CRASH ====================\n\
  231. ExceptionCode=0x{:08X} ({})\n\
  232. ExceptionAddress={:?}\n\
  233. RecordInfo:\n{}\n\
  234. Backtrace:\n{}\n\
  235. ProcessInfo:\n\
  236. PID={}\n\
  237. ThreadId={:?}\n\
  238. ========================================================\n",
  239. exc_code,
  240. exception_code_to_name(exc_code),
  241. exc_addr,
  242. record_str,
  243. backtrace,
  244. std::process::id(),
  245. std::thread::current().id()
  246. );
  247. if let Ok(guard) = LOG_FILE.lock() {
  248. if let Some(path) = guard.as_ref() {
  249. let _ = write_to_log_file(path, &msg);
  250. }
  251. }
  252. eprintln!("{}", msg);
  253. // 继续执行,让 Rust 的 panic hook 也有机会处理
  254. // EXCEPTION_CONTINUE_SEARCH = 0(交给其他处理器)
  255. // EXCEPTION_EXECUTE_HANDLER = 1(执行我们的处理然后终止)
  256. // 这里返回 CONTINUE_SEARCH,让其他处理器(包括 Rust)处理
  257. // 但如果是非常严重的错误,可以返回 EXECUTE_HANDLER
  258. if exc_code == EXCEPTION_STACK_OVERFLOW || exc_code == EXCEPTION_ACCESS_VIOLATION {
  259. // 栈溢出和访问违规非常危险,返回 EXECUTE_HANDLER
  260. // 让程序优雅退出,而不是继续执行导致不可预测行为
  261. 1 // EXCEPTION_EXECUTE_HANDLER
  262. } else {
  263. 0 // EXCEPTION_CONTINUE_SEARCH
  264. }
  265. }
  266. /// 注册 Windows 顶层异常处理器(Vectored Exception Handler)
  267. /// 这能捕获一些 Rust panic 无法覆盖的崩溃,如:
  268. /// - Access Violation(访问违规)
  269. /// - Stack Overflow(栈溢出)
  270. /// - 非法指令
  271. /// - 除零错误
  272. #[cfg(target_os = "windows")]
  273. pub fn setup_windows_exception_handler() {
  274. if VEH_REGISTERED.swap(true, Ordering::SeqCst) {
  275. return; // 已经注册过了
  276. }
  277. use std::ffi::c_void;
  278. // AddVectoredExceptionHandler 来自 winapi
  279. // RUSTSEC-2023-0071: 使用 winapi 而非 libc
  280. // 函数签名:unsafe extern "system" fn(LPEXCEPTION_POINTERS) -> LONG
  281. // 返回值:EXCEPTION_CONTINUE_SEARCH(0) 或 EXCEPTION_EXECUTE_HANDLER(1)
  282. #[link(name = "kernel32")]
  283. extern "system" {
  284. fn AddVectoredExceptionHandler(
  285. first_handler: u32, // 1 = CALL_FIRST, 0 = CALL_LAST
  286. handler: *mut std::ffi::c_void,
  287. ) -> *mut std::ffi::c_void;
  288. }
  289. unsafe {
  290. let result = AddVectoredExceptionHandler(1, veh_exception_handler as *mut c_void);
  291. if result.is_null() {
  292. if let Ok(guard) = LOG_FILE.lock() {
  293. if let Some(path) = guard.as_ref() {
  294. let msg = "[VEH] 注册 Windows 顶层异常处理器失败\n";
  295. let _ = write_to_log_file(path, msg);
  296. }
  297. }
  298. } else {
  299. if let Ok(guard) = LOG_FILE.lock() {
  300. if let Some(path) = guard.as_ref() {
  301. let msg = "[VEH] Windows 顶层异常处理器注册成功\n";
  302. let _ = write_to_log_file(path, msg);
  303. }
  304. }
  305. }
  306. }
  307. }
  308. /// 非 Windows 平台的空实现
  309. #[cfg(not(target_os = "windows"))]
  310. pub fn setup_windows_exception_handler() {
  311. // no-op on non-Windows
  312. }
  313. /// 设置所有崩溃处理机制
  314. pub fn setup_all_crash_handlers() {
  315. setup_panic_hook();
  316. setup_windows_exception_handler();
  317. }
  318. // ============================================================
  319. // 扫货相关日志
  320. // ============================================================
  321. /// 扫货任务开始日志
  322. pub fn log_sweep_start(sweep_request_id: i64, selection_request_id: i64, worker_count: u32) {
  323. info(&format!(
  324. "[SWEEP-START] sweep_request_id={} selection_request_id={} worker_count={}",
  325. sweep_request_id, selection_request_id, worker_count
  326. ));
  327. }
  328. /// 扫货任务结束日志
  329. pub fn log_sweep_end(sweep_request_id: i64, status: &str) {
  330. info(&format!(
  331. "[SWEEP-END] sweep_request_id={} status={}",
  332. sweep_request_id, status
  333. ));
  334. }
  335. /// 扫货轮次日志
  336. pub fn log_sweep_round(sweep_request_id: i64, round: u64, items_count: usize) {
  337. info(&format!(
  338. "[SWEEP-ROUND] sweep_request_id={} round={} items_count={}",
  339. sweep_request_id, round, items_count
  340. ));
  341. }
  342. /// API 调用开始日志
  343. pub fn log_api_call_start(api_name: &str, url: &str) {
  344. debug(&format!("[API-START] {} {}", api_name, url));
  345. }
  346. /// API 调用结束日志
  347. pub fn log_api_call_end(api_name: &str, status: u16, elapsed_ms: u64) {
  348. debug(&format!(
  349. "[API-END] {} status={} elapsed={}ms",
  350. api_name, status, elapsed_ms
  351. ));
  352. }
  353. /// API 调用错误日志
  354. pub fn log_api_error(api_name: &str, err_msg: &str) {
  355. write_log_line("ERROR", &format!("[API-ERROR] {}: {}", api_name, err_msg));
  356. }
  357. /// Steam 账号操作日志
  358. pub fn log_steam_account_op(account: &str, op: &str, result: &str) {
  359. info(&format!(
  360. "[STEAM-ACCOUNT] account={} op={} result={}",
  361. account, op, result
  362. ));
  363. }
  364. /// 购买成功日志
  365. pub fn log_purchase_success(
  366. goods_id: &str,
  367. goods_name: &str,
  368. listing_id: &str,
  369. price_usd: f64,
  370. account: &str,
  371. ) {
  372. info(&format!(
  373. "[PURCHASE-SUCCESS] goods_id={} goods_name={} listing_id={} price=${} account={}",
  374. goods_id, goods_name, listing_id, price_usd, account
  375. ));
  376. }
  377. /// 购买失败日志
  378. pub fn log_purchase_failure(
  379. goods_id: &str,
  380. goods_name: &str,
  381. listing_id: &str,
  382. reason: &str,
  383. ) {
  384. warn(&format!(
  385. "[PURCHASE-FAIL] goods_id={} goods_name={} listing_id={} reason={}",
  386. goods_id, goods_name, listing_id, reason
  387. ));
  388. }
  389. /// 代理切换日志
  390. pub fn log_proxy_change(proxy_endpoint: &str, reason: &str) {
  391. debug(&format!(
  392. "[PROXY-CHANGE] endpoint={} reason={}",
  393. proxy_endpoint, reason
  394. ));
  395. }
  396. /// 心跳日志
  397. pub fn log_heartbeat(status: &str) {
  398. debug(&format!("[HEARTBEAT] status={}", status));
  399. }
  400. /// 线程/任务开始
  401. pub fn log_task_start(task_name: &str) {
  402. debug(&format!("[TASK-START] {}", task_name));
  403. }
  404. /// 线程/任务结束
  405. pub fn log_task_end(task_name: &str) {
  406. debug(&format!("[TASK-END] {}", task_name));
  407. }
  408. // ============================================================
  409. // 内存和资源监控诊断日志
  410. // ============================================================
  411. /// 获取当前进程的内存使用情况(Windows)
  412. #[cfg(target_os = "windows")]
  413. pub fn get_memory_usage_mb() -> f64 {
  414. use std::mem::MaybeUninit;
  415. #[repr(C)]
  416. struct MEMORYSTATUSEX {
  417. dw_length: u32,
  418. dw_memory_load: u32,
  419. ull_total_phys: u64,
  420. ull_avail_phys: u64,
  421. ull_total_page_file: u64,
  422. ull_avail_page_file: u64,
  423. ull_total_virtual: u64,
  424. ull_avail_virtual: u64,
  425. ull_avail_extended_virtual: u64,
  426. }
  427. #[link(name = "kernel32")]
  428. extern "system" {
  429. fn GlobalMemoryStatusEx(lpBuffer: *mut MEMORYSTATUSEX) -> i32;
  430. }
  431. let mut mem_status = MaybeUninit::<MEMORYSTATUSEX>::uninit();
  432. let mem_status_ptr = mem_status.as_mut_ptr();
  433. // 初始化 dw_length 字段
  434. unsafe {
  435. (*mem_status_ptr).dw_length = std::mem::size_of::<MEMORYSTATUSEX>() as u32;
  436. }
  437. unsafe {
  438. if GlobalMemoryStatusEx(mem_status_ptr) != 0 {
  439. let status = mem_status.assume_init();
  440. // 返回已使用的物理内存(MB)
  441. ((status.ull_total_phys - status.ull_avail_phys) as f64) / (1024.0 * 1024.0)
  442. } else {
  443. 0.0
  444. }
  445. }
  446. }
  447. /// 获取当前进程的内存使用情况(非 Windows 平台)
  448. #[cfg(not(target_os = "windows"))]
  449. pub fn get_memory_usage_mb() -> f64 {
  450. 0.0
  451. }
  452. /// 获取当前进程的总内存(MB)
  453. #[cfg(target_os = "windows")]
  454. pub fn get_total_memory_mb() -> f64 {
  455. use std::mem::MaybeUninit;
  456. #[repr(C)]
  457. struct MEMORYSTATUSEX {
  458. dw_length: u32,
  459. dw_memory_load: u32,
  460. ull_total_phys: u64,
  461. ull_avail_phys: u64,
  462. ull_total_page_file: u64,
  463. ull_avail_page_file: u64,
  464. ull_total_virtual: u64,
  465. ull_avail_virtual: u64,
  466. ull_avail_extended_virtual: u64,
  467. }
  468. #[link(name = "kernel32")]
  469. extern "system" {
  470. fn GlobalMemoryStatusEx(lpBuffer: *mut MEMORYSTATUSEX) -> i32;
  471. }
  472. let mut mem_status = MaybeUninit::<MEMORYSTATUSEX>::uninit();
  473. let mem_status_ptr = mem_status.as_mut_ptr();
  474. unsafe {
  475. (*mem_status_ptr).dw_length = std::mem::size_of::<MEMORYSTATUSEX>() as u32;
  476. }
  477. unsafe {
  478. if GlobalMemoryStatusEx(mem_status_ptr) != 0 {
  479. let status = mem_status.assume_init();
  480. (status.ull_total_phys as f64) / (1024.0 * 1024.0)
  481. } else {
  482. 0.0
  483. }
  484. }
  485. }
  486. #[cfg(not(target_os = "windows"))]
  487. pub fn get_total_memory_mb() -> f64 {
  488. 0.0
  489. }
  490. /// 获取当前活跃的线程数量
  491. pub fn get_thread_count() -> usize {
  492. std::thread::available_parallelism()
  493. .map(|p| p.get())
  494. .unwrap_or(0)
  495. }
  496. /// 获取当前进程运行时间(秒)
  497. pub fn get_process_uptime_secs() -> u64 {
  498. let start = PROCESS_START_TIME.get_or_init(|| std::time::Instant::now());
  499. start.elapsed().as_secs()
  500. }
  501. /// 记录完整的系统资源诊断日志
  502. pub fn log_resource_diagnostics(tag: &str) {
  503. let mem_used = get_memory_usage_mb();
  504. let mem_total = get_total_memory_mb();
  505. let mem_percent = if mem_total > 0.0 {
  506. (mem_used / mem_total * 100.0).min(100.0)
  507. } else {
  508. 0.0
  509. };
  510. let thread_count = get_thread_count();
  511. let uptime = get_process_uptime_secs();
  512. let uptime_formatted = format_uptime(uptime);
  513. info(&format!(
  514. "[DIAGNOSTIC][{}] 内存: {:.1}/{:.0}MB ({:.1}%), 线程: {}, 运行时间: {}",
  515. tag, mem_used, mem_total, mem_percent, thread_count, uptime_formatted
  516. ));
  517. }
  518. /// 格式化运行时间
  519. fn format_uptime(secs: u64) -> String {
  520. let hours = secs / 3600;
  521. let minutes = (secs % 3600) / 60;
  522. let seconds = secs % 60;
  523. if hours > 0 {
  524. format!("{}h{}m{}s", hours, minutes, seconds)
  525. } else if minutes > 0 {
  526. format!("{}m{}s", minutes, seconds)
  527. } else {
  528. format!("{}s", seconds)
  529. }
  530. }
  531. /// 记录代理池状态诊断日志
  532. pub fn log_proxy_pool_status(tag: &str, pool_size: usize, in_use: usize) {
  533. info(&format!(
  534. "[DIAGNOSTIC][{}] 代理池: 可用={}, 使用中={}, 空闲={}",
  535. tag, pool_size, in_use, pool_size.saturating_sub(in_use)
  536. ));
  537. }
  538. /// 记录 Worker 状态诊断日志
  539. pub fn log_worker_status(tag: &str, worker_index: u32, active_tasks: usize) {
  540. info(&format!(
  541. "[DIAGNOSTIC][{}] Worker[{}]: 活跃任务={}",
  542. tag, worker_index, active_tasks
  543. ));
  544. }
  545. /// 记录扫货进度诊断日志(包含详细统计)
  546. pub fn log_sweep_progress_diagnostic(
  547. sweep_request_id: i64,
  548. round: u64,
  549. items_processed: u64,
  550. purchases_success: u64,
  551. purchases_failed: u64,
  552. proxy_pool_available: usize,
  553. proxy_pool_in_use: usize,
  554. ) {
  555. let mem_used = get_memory_usage_mb();
  556. let thread_count = get_thread_count();
  557. info(&format!(
  558. "[DIAGNOSTIC][PROGRESS] sweep={} round={} processed={} success={} failed={} mem={:.0}MB threads={} proxies=可用{}/使用中{}",
  559. sweep_request_id,
  560. round,
  561. items_processed,
  562. purchases_success,
  563. purchases_failed,
  564. mem_used,
  565. thread_count,
  566. proxy_pool_available,
  567. proxy_pool_in_use
  568. ));
  569. }
  570. #[cfg(test)]
  571. mod tests {
  572. use super::*;
  573. #[test]
  574. fn test_log_dir_creation() {
  575. let log_dir = get_log_dir();
  576. assert!(log_dir.ends_with("logs"));
  577. }
  578. #[test]
  579. fn test_memory_functions() {
  580. let mem_used = get_memory_usage_mb();
  581. let mem_total = get_total_memory_mb();
  582. assert!(mem_used >= 0.0);
  583. assert!(mem_total >= 0.0);
  584. assert!(mem_used <= mem_total);
  585. }
  586. #[test]
  587. fn test_uptime_format() {
  588. assert_eq!(format_uptime(30), "30s");
  589. assert_eq!(format_uptime(90), "1m30s");
  590. assert_eq!(format_uptime(3660), "1h1m0s");
  591. }
  592. }