debug.log 保留策略:只留最近3天+≤5MB,任一超标即清理到两条件同时满足

This commit is contained in:
ypsdf1
2026-10-05 13:35:39 +08:00
parent e040e8c994
commit ce489d1450
5 changed files with 190 additions and 2 deletions
+5
View File
@@ -45,6 +45,11 @@ function debugLog($msg, $ctx = []) {
if ($ctx) $entry .= ' | Context: ' . json_encode($ctx, JSON_UNESCAPED_UNICODE);
$entry .= "\n";
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
// ★ 保留策略(2026-10-05):只留最近3天 + ≤5MB,任一超标即清理到两条件同时满足
if (is_file(__DIR__ . '/../log_maintain.php')) {
@require_once __DIR__ . '/../log_maintain.php';
if (function_exists('logMaintain')) { @logMaintain($logFile); }
}
}
} // end if (!function_exists('debugLog'))
+7 -1
View File
@@ -119,7 +119,13 @@ function cgLog($msg) {
}
$dir = __DIR__ . '/db';
if (!is_dir($dir)) { @mkdir($dir, 0755, true); }
@file_put_contents($dir . '/debug.log', $line, FILE_APPEND | LOCK_EX);
$f = $dir . '/debug.log';
@file_put_contents($f, $line, FILE_APPEND | LOCK_EX);
// ★ 保留策略(2026-10-05):只留最近3天 + ≤5MB,任一超标即清理到两条件同时满足
if (is_file(__DIR__ . '/log_maintain.php')) {
@require_once __DIR__ . '/log_maintain.php';
if (function_exists('logMaintain')) { @logMaintain($f); }
}
}
// ====================================================================
+6
View File
@@ -26,6 +26,12 @@ function debugLog($message, $context = []) {
}
file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
// ★ 保留策略(2026-10-05):只留最近3天 + ≤5MB,任一超标即清理到两条件同时满足
if (is_file(__DIR__ . '/log_maintain.php')) {
@require_once __DIR__ . '/log_maintain.php';
if (function_exists('logMaintain')) { @logMaintain($logFile); }
}
}
// ===== 数据库初始化 =====
+165
View File
@@ -0,0 +1,165 @@
<?php
/**
* debug 日志保留策略(2026-10-05)
*
* 规则(两条同时满足才算干净,达成任一条超标即触发清理):
* 1. 只保留最近 3 天的日志行(按行首 [Y-m-d H:i:s] 时间戳判断);
* 2. 文件总大小 ≤ 5MB。
*
* 设计要点:
* - 自包含:不依赖 core.php / config.php(captcha_guard、poller 等独立入口都能用);
* - 廉价预检:每次写入后只做 1 次 stat + 读前几行找最旧时间戳,命中才做全量清理,
* 绝大多数调用在预检处直接返回,开销微秒级;
* - append-only 假设:日志按时间追加,文件开头即最旧行;
* - 并发安全:清理期间 flock 日志文件排他(写入方 file_put_contents(LOCK_EX) 会短暂
* 等待),锁内先复查,避免多个请求排队重复重建;
* - 流式处理:逐行读写、临时文件中转,60MB+ 大文件也不会撑爆 memory_limit;
* - 无时间戳前缀的行(多行消息的续行)跟随上一行去留,避免留下孤儿行;
* - 临时文件放在同目录(db/ 已被 nginx 整目录 404 屏蔽,外网不可见)。
*
* 接入点:core.php:debugLog / orders.php 内联 debugLog / captcha_guard.php:cgLog
* / api/poller_online.php:debugLog(api/pay_poller.php 为无引用模板,不接入)。
*/
if (!function_exists('logMaintain')) {
/** 从行首解析 [Y-m-d H:i:s] → unix 时间戳;解析失败返回 0 */
function logMaintainLineTs($line) {
if (preg_match('/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/', $line, $m)) {
$ts = strtotime($m[1]);
return ($ts === false) ? 0 : $ts;
}
return 0;
}
/**
* 读取文件最旧的可解析时间戳(最多扫前 maxLines 行找第一个带时间戳的行)。
* 找不到返回 0(调用方视为「无法判定年龄」,不触发超龄清理)。
* @param resource $fh 已打开的文件句柄(会被 rewind)
*/
function logMaintainOldestTs($fh, $maxLines = 10) {
if (rewind($fh) === false) return 0;
for ($i = 0; $i < $maxLines; $i++) {
$line = fgets($fh);
if ($line === false) break;
$ts = logMaintainLineTs($line);
if ($ts > 0) return $ts;
}
return 0;
}
/**
* 重建日志:过滤掉 cutoff 之前的行;若剩余仍 > maxBytes,再从最旧端按行丢弃到 ≤ maxBytes。
* 调用方必须已持有 $fh 的排他锁。
* @param resource $fh 日志文件句柄(读写)
* @param string $logFile 日志路径(临时文件建在同目录)
* @return bool 成功 true
*/
function logMaintainRebuild($fh, $logFile, $maxBytes, $cutoff) {
$tmp = $logFile . '.tmp';
// ===== 第一遍:读原文件,过滤超龄行,写入临时文件 =====
$out = @fopen($tmp, 'wb');
if (!$out) return false;
if (rewind($fh) === false) { fclose($out); @unlink($tmp); return false; }
$keepPrev = true; // 无时间戳的续行跟随上一行;首行无时间戳时保守保留
$kept = 0;
while (($line = fgets($fh)) !== false) {
$ts = logMaintainLineTs($line);
if ($ts > 0) {
$keepPrev = ($ts >= $cutoff);
}
if ($keepPrev) {
fwrite($out, $line);
$kept += strlen($line);
}
}
fflush($out);
fclose($out);
clearstatcache(true, $tmp);
// ===== 第二步:剩余仍超 5MB → 从最旧端按整行丢弃到 ≤ maxBytes =====
$skipBytes = ($kept > $maxBytes) ? ($kept - $maxBytes) : 0;
$in = @fopen($tmp, 'rb');
if (!$in) { @unlink($tmp); return false; }
if ($skipBytes > 0) {
$acc = 0;
// 逐行累计丢弃;按整行丢,最终大小 kept-acc 必 ≤ maxBytes
while ($acc < $skipBytes && ($line = fgets($in)) !== false) {
$acc += strlen($line);
}
}
// ===== 写回日志本体(仍持锁,写入方在锁外等待,不会丢)=====
if (ftruncate($fh, 0) === false) { fclose($in); @unlink($tmp); return false; }
rewind($fh);
while (!feof($in)) {
$chunk = fread($in, 1048576);
if ($chunk === false) break;
if ($chunk !== '') fwrite($fh, $chunk);
}
fflush($fh);
fclose($in);
@unlink($tmp);
return true;
}
/**
* 入口:日志写入后调用。任一条件超标(>5MB 或 含 3 天前的行)即清理,
* 清理到两个条件同时满足为止(最多 3 轮,正常第 1 轮后复查即通过)。
*
* @param string $logFile 日志文件绝对路径
* @param int $maxBytes 大小上限(默认 5MB)
* @param int $keepDays 保留天数(默认 3 天)
* @return bool true=干净(无需清理或已清理完成);false=清理失败
*/
function logMaintain($logFile, $maxBytes = 5242880, $keepDays = 3) {
if (!is_string($logFile) || $logFile === '') return true;
clearstatcache(true, $logFile);
if (!is_file($logFile)) return true;
$size = @filesize($logFile);
if ($size === false || $size <= 0) return true;
$cutoff = time() - $keepDays * 86400;
// ---- 廉价预检:两条都没超标就直接返回(绝大多数调用在这里结束)----
$need = ($size > $maxBytes);
if (!$need) {
$fh = @fopen($logFile, 'rb');
if ($fh) {
$oldest = logMaintainOldestTs($fh);
fclose($fh);
$need = ($oldest > 0 && $oldest < $cutoff);
}
}
if (!$need) return true;
// ---- 清理:排他锁内执行(写入方 file_put_contents(LOCK_EX) 短暂等待)----
$fh = @fopen($logFile, 'c+');
if (!$fh) return false;
if (!flock($fh, LOCK_EX)) { fclose($fh); return false; }
$ok = false;
for ($round = 0; $round < 3; $round++) {
// 锁内复查:可能别的进程刚清理完
$stat = fstat($fh);
$size = $stat ? (int) $stat['size'] : 0;
$oldest = logMaintainOldestTs($fh);
if ($size <= $maxBytes && !($oldest > 0 && $oldest < $cutoff)) {
$ok = true;
break;
}
if (!logMaintainRebuild($fh, $logFile, $maxBytes, $cutoff)) {
break; // 重建失败(如目录不可写)→ 解锁退出,下次写入重试
}
}
flock($fh, LOCK_UN);
fclose($fh);
clearstatcache(true, $logFile);
return $ok;
}
} // end if (!function_exists('logMaintain'))
+7 -1
View File
@@ -15,7 +15,13 @@ set_error_handler(function($errno, $errstr, $errfile, $errline) {
// core.php may not be loaded yet, define minimal debugLog
if (!function_exists('debugLog')) {
function debugLog($msg) {
@file_put_contents(__DIR__ . '/db/debug.log', "[" . date('Y-m-d H:i:s') . "] [orders.php] $msg\n", FILE_APPEND | LOCK_EX);
$lf = __DIR__ . '/db/debug.log';
@file_put_contents($lf, "[" . date('Y-m-d H:i:s') . "] [orders.php] $msg\n", FILE_APPEND | LOCK_EX);
// ★ 保留策略(2026-10-05):只留最近3天 + ≤5MB,任一超标即清理到两条件同时满足
if (is_file(__DIR__ . '/log_maintain.php')) {
@require_once __DIR__ . '/log_maintain.php';
if (function_exists('logMaintain')) { @logMaintain($lf); }
}
}
}
debugLog("[orders.php] PHP {$type}: {$errstr} in {$errfile}:{$errline}");