fix: use second-level log timestamps

This commit is contained in:
2026-07-14 08:50:44 +08:00
parent 62ed176bdd
commit b41d744965
3 changed files with 25 additions and 15 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ MyLogger 是用于 Qt 程序的进程级日志模块,接管 `qDebug()`、`qInf
## 行为 ## 行为
- 默认写入 `{applicationDirPath}/logs`,日志文件名为 `yyyy-MM-dd_进程号_分卷号.log` - 默认写入 `{applicationDirPath}/logs`,日志文件名为 `yyyy-MM-dd_hhmmss_分卷号.log`,例如 `2026-07-14_153045_001.log`
- 单个文件最大 10 MiB,达到上限自动创建下一个分卷;仅清理本模块生成且超过 30 天的日志。 - 单个文件最大 10 MiB,达到上限自动创建下一个分卷;仅清理本模块生成且超过 30 天的日志。
- 普通日志最多每秒 flush 一次;`qCritical()``qFatal()` 会立即 flush。 - 普通日志最多每秒 flush 一次;`qCritical()``qFatal()` 会立即 flush。
- 安装时保存此前的 Qt 消息处理器,卸载时恢复它。Qt 每个进程只能有一个全局消息处理器,安装期间不要由其他模块再次调用 `qInstallMessageHandler()` - 安装时保存此前的 Qt 消息处理器,卸载时恢复它。Qt 每个进程只能有一个全局消息处理器,安装期间不要由其他模块再次调用 `qInstallMessageHandler()`
+20 -10
View File
@@ -45,6 +45,7 @@ struct LogState
qint64 currentFileBytes = 0; qint64 currentFileBytes = 0;
int currentPart = 0; int currentPart = 0;
QDate activeDate; QDate activeDate;
QString currentFilePrefix;
bool installed = false; bool installed = false;
#if defined(MYLOGGER_TESTING) #if defined(MYLOGGER_TESTING)
// 测试编译时将文件系统副作用收敛到临时目录,并用小阈值快速覆盖分卷逻辑。 // 测试编译时将文件系统副作用收敛到临时目录,并用小阈值快速覆盖分卷逻辑。
@@ -174,11 +175,18 @@ QString shortFileName(const QMessageLogContext &context)
return separatorIndex >= 0 ? fileName.mid(separatorIndex + 1) : fileName; return separatorIndex >= 0 ? fileName.mid(separatorIndex + 1) : fileName;
} }
// 仅清理本模块按日期、进程号和分卷号命名的日志,避免公共 logs 目录中的宿主业务文件被误删。 // 日志文件以一次打开日志时的本地秒级时间作为会话标识;分卷只追加序号,
// 既能让现场直接看出开始时间,也避免把进程 ID 暴露到文件名中。
QString createLogFilePrefix(const QDateTime &dateTime)
{
return dateTime.toString(QStringLiteral("yyyy-MM-dd_hhmmss"));
}
// 仅清理本模块按“日期_时分秒_分卷”命名的日志,避免公共 logs 目录中的宿主业务文件被误删。
void removeExpiredLogs(const QDir &logDir, const QDate &today) void removeExpiredLogs(const QDir &logDir, const QDate &today)
{ {
const QRegularExpression logNamePattern( const QRegularExpression logNamePattern(
QStringLiteral("^\\d{4}-\\d{2}-\\d{2}_\\d+_\\d{3}\\.log$")); QStringLiteral("^\\d{4}-\\d{2}-\\d{2}_\\d{6}_\\d{3}\\.log$"));
const QDate oldestDate = today.addDays(-kLogRetentionDays); const QDate oldestDate = today.addDays(-kLogRetentionDays);
const QFileInfoList files = logDir.entryInfoList(QStringList() << QStringLiteral("*.log"), const QFileInfoList files = logDir.entryInfoList(QStringList() << QStringLiteral("*.log"),
QDir::Files | QDir::Readable); QDir::Files | QDir::Readable);
@@ -197,12 +205,15 @@ void removeExpiredLogs(const QDir &logDir, const QDate &today)
} }
} }
// 在锁保护下选择当前进程当天可继续写入的分卷;已满分卷永不复用,防止重启后突破大小上限。 // 在锁保护下为当前日期选择可继续写入的分卷;同一会话始终复用首次生成的详细时间前缀,
// 已满分卷永不复用,防止重启后突破大小上限。
bool openCurrentLogFileLocked(LogState &state, const QDir &logDir, const QDate &today) bool openCurrentLogFileLocked(LogState &state, const QDir &logDir, const QDate &today)
{ {
const QString prefix = QStringLiteral("%1_%2_") if (state.currentFilePrefix.isEmpty() || state.activeDate != today)
.arg(today.toString(QStringLiteral("yyyy-MM-dd"))) {
.arg(QCoreApplication::applicationPid()); state.currentFilePrefix = createLogFilePrefix(QDateTime::currentDateTime());
}
const QString prefix = state.currentFilePrefix + QLatin1Char('_');
int part = 1; int part = 1;
while (true) while (true)
{ {
@@ -232,14 +243,12 @@ bool openCurrentLogFileLocked(LogState &state, const QDir &logDir, const QDate &
} }
} }
// 在当前文件即将超过上限时切到下一个空闲分卷;连续序号便于现场按时间顺序定位日志。 // 在当前文件即将超过上限时切到同一详细时间会话的下一个空闲分卷;连续序号便于现场按顺序定位日志。
bool rotateLogFileLocked(LogState &state) bool rotateLogFileLocked(LogState &state)
{ {
const QFileInfo currentInfo(state.file); const QFileInfo currentInfo(state.file);
const QDir logDir = currentInfo.dir(); const QDir logDir = currentInfo.dir();
const QString prefix = QStringLiteral("%1_%2_") const QString prefix = state.currentFilePrefix + QLatin1Char('_');
.arg(QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd")))
.arg(QCoreApplication::applicationPid());
int part = state.currentPart + 1; int part = state.currentPart + 1;
QString filePath; QString filePath;
do do
@@ -396,6 +405,7 @@ void LogHandler::uninstallMessageHandler()
g_logState.currentFileBytes = 0; g_logState.currentFileBytes = 0;
g_logState.currentPart = 0; g_logState.currentPart = 0;
g_logState.activeDate = QDate(); g_logState.activeDate = QDate();
g_logState.currentFilePrefix.clear();
g_logState.installed = false; g_logState.installed = false;
} }
+4 -4
View File
@@ -3,7 +3,6 @@
#include <vector> #include <vector>
#include <QDate> #include <QDate>
#include <QCoreApplication>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@@ -215,9 +214,10 @@ void LogHandlerTest::rotatesAtConfiguredSize()
// 验证清理策略只处理超过保留期且符合 MyLogger 命名约定的文件,宿主业务日志必须保留。 // 验证清理策略只处理超过保留期且符合 MyLogger 命名约定的文件,宿主业务日志必须保留。
void LogHandlerTest::removesOnlyExpiredOwnedLogs() void LogHandlerTest::removesOnlyExpiredOwnedLogs()
{ {
const QString oldOwnedName = QStringLiteral("%1_%2_001.log") // 过期文件必须严格符合新命名规则:日期、启动时分秒和分卷号,
.arg(QDate::currentDate().addDays(-31).toString(QStringLiteral("yyyy-MM-dd"))) // 才能验证清理逻辑不会把同目录中其他业务日志误认为 MyLogger 文件。
.arg(QCoreApplication::applicationPid()); const QString oldOwnedName = QStringLiteral("%1_000000_001.log")
.arg(QDate::currentDate().addDays(-31).toString(QStringLiteral("yyyy-MM-dd")));
const QString oldOwnedPath = m_logDirectory.filePath(oldOwnedName); const QString oldOwnedPath = m_logDirectory.filePath(oldOwnedName);
const QString hostLogPath = m_logDirectory.filePath(QStringLiteral("host-business.log")); const QString hostLogPath = m_logDirectory.filePath(QStringLiteral("host-business.log"));
QFile oldOwnedFile(oldOwnedPath); QFile oldOwnedFile(oldOwnedPath);