Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b41d744965 | |||
| 62ed176bdd |
+32
-5
@@ -1,7 +1,34 @@
|
||||
project(MyLogger)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
add_library(${PROJECT_NAME} ${PROJECT_SOURCE_DIR}/loghandler.cpp)
|
||||
project(MyLogger LANGUAGES CXX)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
Qt5::Core
|
||||
)
|
||||
# 同时识别 Qt5 与 Qt6,避免公共模块把消费者固定在单一 Qt 主版本。
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core)
|
||||
|
||||
add_library(MyLogger SHARED
|
||||
loghandler.cpp
|
||||
loghandler.h
|
||||
)
|
||||
|
||||
# 目标自身导出符号,使用方只获得公共头文件、C++17 要求和 Qt Core 传递依赖。
|
||||
target_compile_features(MyLogger PUBLIC cxx_std_17)
|
||||
target_compile_definitions(MyLogger PRIVATE MYLOGGER_LIBRARY QT_MESSAGELOGCONTEXT)
|
||||
target_include_directories(MyLogger PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>)
|
||||
target_link_libraries(MyLogger PUBLIC Qt${QT_VERSION_MAJOR}::Core)
|
||||
|
||||
# 测试目标直接编译日志源码并开启测试访问宏,避免依赖已安装 DLL,也不改变正式库的公开行为。
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)
|
||||
add_executable(MyLoggerTests
|
||||
tests/tst_loghandler.cpp
|
||||
loghandler.cpp
|
||||
loghandler.h
|
||||
)
|
||||
target_compile_features(MyLoggerTests PRIVATE cxx_std_17)
|
||||
target_compile_definitions(MyLoggerTests PRIVATE MYLOGGER_STATIC MYLOGGER_TESTING QT_MESSAGELOGCONTEXT)
|
||||
target_include_directories(MyLoggerTests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(MyLoggerTests PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Test)
|
||||
add_test(NAME MyLoggerTests COMMAND MyLoggerTests)
|
||||
endif()
|
||||
|
||||
+8
-6
@@ -1,7 +1,9 @@
|
||||
# 本 pri 用于把 MyLogger 源码直接嵌入 qmake 宿主;不得同时链接 MyLogger 动态库,
|
||||
# 否则同一进程会拥有两份全局日志状态并争夺 Qt 的唯一消息处理器。
|
||||
QT += core
|
||||
CONFIG += c++17
|
||||
DEFINES += MYLOGGER_STATIC QT_MESSAGELOGCONTEXT
|
||||
|
||||
INCLUDEPATH += $$PWD/
|
||||
|
||||
#在release下输出debug信息
|
||||
DEFINES += QT_MESSAGELOGCONTEXT
|
||||
|
||||
SOURCES += \
|
||||
$$PWD/loghandler.cpp
|
||||
HEADERS += $$PWD/loghandler.h
|
||||
SOURCES += $$PWD/loghandler.cpp
|
||||
|
||||
+5
-10
@@ -1,25 +1,20 @@
|
||||
QT += core
|
||||
TEMPLATE = lib
|
||||
DEFINES += MYLOGGER_LIBRARY
|
||||
TARGET = MyLogger
|
||||
|
||||
# 动态库自身导出 API,并强制保留 Qt 消息上下文;消费者只通过头文件中的导入宏引用符号。
|
||||
DEFINES += MYLOGGER_LIBRARY QT_MESSAGELOGCONTEXT
|
||||
CONFIG += c++17
|
||||
|
||||
INCLUDEPATH += $$PWD/
|
||||
|
||||
DESTDIR = $$PWD/../../Build/Libs
|
||||
MOC_DIR = $$PWD/Build/moc
|
||||
OBJECTS_DIR = $$PWD/Build/objs
|
||||
RCC_DIR = $$PWD/Build/resources
|
||||
UI_DIR = $$PWD/Build/ui
|
||||
|
||||
#在release下输出debug信息
|
||||
DEFINES += QT_MESSAGELOGCONTEXT
|
||||
|
||||
HEADERS += \
|
||||
$$PWD/loghandler.h
|
||||
|
||||
SOURCES += \
|
||||
$$PWD/loghandler.cpp
|
||||
HEADERS += $$PWD/loghandler.h
|
||||
SOURCES += $$PWD/loghandler.cpp
|
||||
|
||||
unix {
|
||||
target.path = /usr/lib
|
||||
|
||||
@@ -1,68 +1,59 @@
|
||||
## 概述
|
||||
# MyLogger
|
||||
|
||||
- Singleton.h
|
||||
MyLogger 是用于 Qt 程序的进程级日志模块,接管 `qDebug()`、`qInfo()`、`qWarning()`、`qCritical()` 和 `qFatal()`。
|
||||
|
||||
懒汉模式的单例模板类
|
||||
## 行为
|
||||
|
||||
- loghandler.h/cpp
|
||||
- 默认写入 `{applicationDirPath}/logs`,日志文件名为 `yyyy-MM-dd_hhmmss_分卷号.log`,例如 `2026-07-14_153045_001.log`。
|
||||
- 单个文件最大 10 MiB,达到上限自动创建下一个分卷;仅清理本模块生成且超过 30 天的日志。
|
||||
- 普通日志最多每秒 flush 一次;`qCritical()` 和 `qFatal()` 会立即 flush。
|
||||
- 安装时保存此前的 Qt 消息处理器,卸载时恢复它。Qt 每个进程只能有一个全局消息处理器,安装期间不要由其他模块再次调用 `qInstallMessageHandler()`。
|
||||
|
||||
日志类
|
||||
## qmake:链接动态库
|
||||
|
||||
- MyLogger.pri
|
||||
先构建 `MyLogger.pro`,然后在宿主工程中链接生成的库并包含头文件。不要再包含 `MyLogger.pri`。
|
||||
|
||||
子模块qt pri文件
|
||||
```pro
|
||||
LIBS += -L$$PWD/Libs/MyLogger/build -lMyLogger
|
||||
INCLUDEPATH += $$PWD/Libs/MyLogger
|
||||
```
|
||||
|
||||
## 使用
|
||||
## qmake:源码嵌入
|
||||
|
||||
### 克隆代码
|
||||
|
||||
克隆代码并复制到工程Libs目录下
|
||||
|
||||
### 添加子工程
|
||||
|
||||
在主工程pro文件中,添加:
|
||||
不单独构建动态库时,可在宿主 `.pro` 中使用:
|
||||
|
||||
```pro
|
||||
include($$PWD/Libs/MyLogger/MyLogger.pri)
|
||||
```
|
||||
|
||||
### 使用
|
||||
源码嵌入与链接动态库只能二选一。
|
||||
|
||||
#### 日志使用
|
||||
## 使用
|
||||
|
||||
```c++
|
||||
#include <QApplication>
|
||||
//头文件
|
||||
在 `Q(Core)Application` 创建后、需要记录第一条 Qt 日志前安装;应用退出前卸载。
|
||||
|
||||
```cpp
|
||||
#include "loghandler.h"
|
||||
int main(int argc, char *argv[])
|
||||
|
||||
if (!LogHandler::Get().installMessageHandler())
|
||||
{
|
||||
QApplication a(argc, argv);
|
||||
//安装
|
||||
LogHandler::Get().installMessageHandler();
|
||||
Debug() << "Hello";
|
||||
qDebug() << "当前时间是: " << QTime::currentTime().toString("hh:mm:ss");
|
||||
qInfo() << QString("God bless you!");
|
||||
//卸载
|
||||
LogHandler::Get().uninstallMessageHandler();
|
||||
return a.exec();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 单例模板类使用
|
||||
|
||||
```c++
|
||||
#include "Singleton.h"
|
||||
class AppConfig : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
//通过宏
|
||||
SINGLETON(AppConfig);
|
||||
.....
|
||||
// 日志目录不可用;应用可继续运行,也可在权限恢复后再次尝试安装。
|
||||
}
|
||||
|
||||
qInfo() << "application started";
|
||||
|
||||
LogHandler::Get().uninstallMessageHandler();
|
||||
```
|
||||
|
||||
## 单元测试
|
||||
|
||||
`tests/MyLoggerTests.pro` 使用 QtTest 直接编译日志源码,并通过仅测试可见的 `MYLOGGER_TESTING` 访问类把日志写入临时目录。
|
||||
|
||||
```powershell
|
||||
cd Libs/MyLogger/tests
|
||||
qmake MyLoggerTests.pro
|
||||
mingw32-make -j12
|
||||
.\release\MyLoggerTests.exe
|
||||
```
|
||||
|
||||
测试覆盖安装失败后的重试、旧 handler 恢复、UTF-8 内容、并发写入、分卷与过期日志清理。CMake 使用 `-DBUILD_TESTING=ON` 后可通过 `ctest` 执行同一套用例。
|
||||
|
||||
+405
-118
@@ -1,157 +1,444 @@
|
||||
#include "loghandler.h"
|
||||
#include <iostream>
|
||||
#include <QDebug>
|
||||
#include "loghandler.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDate>
|
||||
#include <QDateTime>
|
||||
#include <QMutexLocker>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QRegularExpression>
|
||||
#include <QScopedValueRollback>
|
||||
#include <QTextStream>
|
||||
#include <QCoreApplication>
|
||||
|
||||
QMutex g_mutex;
|
||||
QFile g_file;
|
||||
QTextStream g_stream;
|
||||
qint64 g_lastFlushMs = 0;
|
||||
// Qt6 将 QTextStream 编码接口改为 QStringConverter 枚举;Qt5 分支继续使用 setCodec,
|
||||
// 条件包含避免 Qt5 公用库因为不存在该头文件而无法构建。
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
#include <QStringConverter>
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr qint64 kLogFlushIntervalMs = 1000;
|
||||
constexpr qint64 kMaxLogFileBytes = 10LL * 1024 * 1024;
|
||||
constexpr int kLogRetentionDays = 30;
|
||||
|
||||
// 消息处理函数
|
||||
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
// 日志运行状态集中在一个私有对象中,所有文件访问和安装状态均受同一把锁保护,
|
||||
// 使重复安装、退出期卸载和后台线程同时输出日志时不会访问到半关闭的 QTextStream。
|
||||
struct LogState
|
||||
{
|
||||
QMutexLocker locker(&g_mutex);
|
||||
QString level, colorLevel;
|
||||
QMutex mutex;
|
||||
QFile file;
|
||||
QTextStream stream;
|
||||
QtMessageHandler previousHandler = nullptr;
|
||||
qint64 lastFlushMs = 0;
|
||||
qint64 currentFileBytes = 0;
|
||||
int currentPart = 0;
|
||||
QDate activeDate;
|
||||
QString currentFilePrefix;
|
||||
bool installed = false;
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
// 测试编译时将文件系统副作用收敛到临时目录,并用小阈值快速覆盖分卷逻辑。
|
||||
QString testLogDirectory;
|
||||
qint64 testMaxLogFileBytes = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case QtDebugMsg:
|
||||
level = "DEBUG";
|
||||
colorLevel = "\033[36m" + level + "\033[0m"; // 青色
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
level = "INFO ";
|
||||
colorLevel = "\033[32m" + level + "\033[0m"; // 绿色
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
level = "WARN ";
|
||||
colorLevel = "\033[33m" + level + "\033[0m"; // 黄色
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
level = "ERROR";
|
||||
colorLevel = "\033[31m" + level + "\033[0m"; // 红色
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
level = "FATAL";
|
||||
colorLevel = "\033[31m" + level + "\033[0m"; // 红色
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
LogState g_logState;
|
||||
|
||||
// 输出到日志文件, 格式: 时间 - [Level] (文件名:行数, 函数): 消息
|
||||
QString fileName = context.file ? QString::fromUtf8(context.file) : QStringLiteral("?");
|
||||
int index = fileName.lastIndexOf(QDir::separator());
|
||||
if (index >= 0)
|
||||
{
|
||||
fileName = fileName.mid(index + 1);
|
||||
}
|
||||
|
||||
const QString funcName = context.function ? QString::fromUtf8(context.function) : QStringLiteral("?");
|
||||
const QString lineText = QString::number(context.line);
|
||||
const QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
|
||||
const QString consoleMsg = QString("%1-[%2](%3:%4,%5): %6")
|
||||
.arg(timestamp, colorLevel, fileName, lineText, funcName, msg);
|
||||
const QString fileMsg = QString("%1-[%2](%3:%4,%5): %6")
|
||||
.arg(timestamp, level, fileName, lineText, funcName, msg);
|
||||
|
||||
std::cout << consoleMsg.toLocal8Bit().constData() << std::endl;
|
||||
|
||||
if (g_file.isOpen())
|
||||
{
|
||||
g_stream << fileMsg << "\n";
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
if (type >= QtCriticalMsg || now - g_lastFlushMs >= kLogFlushIntervalMs)
|
||||
{
|
||||
g_stream.flush();
|
||||
g_file.flush();
|
||||
g_lastFlushMs = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static QString LinuxRuntimeRoot()
|
||||
// Linux 部署包可能由启动脚本指定可写运行根目录,或由 AppImage 挂载在只读位置;
|
||||
// 这里优先复用 LC 已验证的环境变量策略,未提供时保持各平台原有的应用目录回退行为。
|
||||
QString runtimeRootPath()
|
||||
{
|
||||
#ifdef Q_OS_LINUX
|
||||
QByteArray root = qgetenv("LC_APP_ROOT");
|
||||
if(!root.isEmpty())
|
||||
#if defined(Q_OS_LINUX)
|
||||
const QByteArray configuredRoot = qgetenv("LC_APP_ROOT");
|
||||
if (!configuredRoot.isEmpty())
|
||||
{
|
||||
return QDir::cleanPath(QString::fromLocal8Bit(root));
|
||||
return QDir::cleanPath(QString::fromLocal8Bit(configuredRoot));
|
||||
}
|
||||
|
||||
QByteArray appImage = qgetenv("APPIMAGE");
|
||||
if(!appImage.isEmpty())
|
||||
const QByteArray appImagePath = qgetenv("APPIMAGE");
|
||||
if (!appImagePath.isEmpty())
|
||||
{
|
||||
return QFileInfo(QString::fromLocal8Bit(appImage)).absolutePath();
|
||||
return QFileInfo(QString::fromLocal8Bit(appImagePath)).absolutePath();
|
||||
}
|
||||
#endif
|
||||
|
||||
return QCoreApplication::applicationDirPath();
|
||||
}
|
||||
|
||||
// 给Qt安装消息处理函数
|
||||
void LogHandler::installMessageHandler()
|
||||
// 测试覆盖目录优先于部署策略,正式环境则在统一运行根目录下创建 logs,
|
||||
// 从而同时兼容 Windows exe 同级日志、Linux 启动脚本和 AppImage 部署。
|
||||
QString logDirectoryPath()
|
||||
{
|
||||
static bool installFlag = false;
|
||||
if(installFlag)
|
||||
return;
|
||||
installFlag = true;
|
||||
//获取日期
|
||||
auto _currentDate = QDateTime::currentDateTime().toString("yyyy-MM-dd");
|
||||
|
||||
//打开/创建文件
|
||||
#ifdef Q_OS_LINUX
|
||||
auto _appDirPath = LinuxRuntimeRoot();
|
||||
#else
|
||||
auto _appDirPath = QCoreApplication::applicationDirPath();
|
||||
#endif
|
||||
|
||||
auto _logDirPath = _appDirPath + "/logs/";
|
||||
QDir _logDir(_logDirPath);
|
||||
if(!_logDir.exists())
|
||||
_logDir.mkpath(_logDirPath);
|
||||
|
||||
QString _logPath;
|
||||
for(int i = 1; i < 1000; ++i)
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
if (!g_logState.testLogDirectory.isEmpty())
|
||||
{
|
||||
_logPath = _logDirPath + _currentDate + QString("_%1.log").arg(i);
|
||||
QFileInfo _fileInfo(_logPath);
|
||||
if(!_fileInfo.exists())
|
||||
return g_logState.testLogDirectory;
|
||||
}
|
||||
#endif
|
||||
return runtimeRootPath() + QStringLiteral("/logs");
|
||||
}
|
||||
|
||||
// 正式分卷阈值保持 10 MiB,测试仅通过编译期访问类降低阈值以缩短执行时间。
|
||||
qint64 maxLogFileBytes()
|
||||
{
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
if (g_logState.testMaxLogFileBytes > 0)
|
||||
{
|
||||
return g_logState.testMaxLogFileBytes;
|
||||
}
|
||||
#endif
|
||||
return kMaxLogFileBytes;
|
||||
}
|
||||
|
||||
// Qt 内部或文件错误路径可能再次产生日志;同线程递归时不再进入互斥写盘路径,
|
||||
// 防止非递归互斥锁死锁,同时保留最小 stderr 输出用于诊断原始故障。
|
||||
thread_local bool g_isHandlingMessage = false;
|
||||
|
||||
// 仅在标准输出确实连接终端时插入 ANSI 颜色,避免控制字符污染重定向文件和 Windows GUI 进程输出。
|
||||
bool supportsAnsiColor()
|
||||
{
|
||||
#if defined(Q_OS_WIN)
|
||||
return _isatty(_fileno(stdout)) != 0;
|
||||
#else
|
||||
return isatty(fileno(stdout)) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// 将 Qt 日志等级转换为固定宽度文本,未知枚举值也保留可读标识以兼容后续 Qt 扩展。
|
||||
QString levelName(QtMsgType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case QtDebugMsg:
|
||||
return QStringLiteral("DEBUG");
|
||||
case QtInfoMsg:
|
||||
return QStringLiteral("INFO ");
|
||||
case QtWarningMsg:
|
||||
return QStringLiteral("WARN ");
|
||||
case QtCriticalMsg:
|
||||
return QStringLiteral("ERROR");
|
||||
case QtFatalMsg:
|
||||
return QStringLiteral("FATAL");
|
||||
default:
|
||||
return QStringLiteral("UNKWN");
|
||||
}
|
||||
}
|
||||
|
||||
// 控制台颜色仅是交互显示效果,文件日志始终使用无控制字符的等级文本。
|
||||
QString consoleLevel(QtMsgType type, const QString &level)
|
||||
{
|
||||
if (!supportsAnsiColor())
|
||||
{
|
||||
return level;
|
||||
}
|
||||
|
||||
const char *color = "\033[0m";
|
||||
switch (type)
|
||||
{
|
||||
case QtDebugMsg:
|
||||
color = "\033[36m";
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
color = "\033[32m";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
color = "\033[33m";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
case QtFatalMsg:
|
||||
color = "\033[31m";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return QString::fromLatin1(color) + level + QStringLiteral("\033[0m");
|
||||
}
|
||||
|
||||
// 兼容 Windows 反斜杠与编译器可能输出的正斜杠路径,只在日志中保留文件名以控制行宽。
|
||||
QString shortFileName(const QMessageLogContext &context)
|
||||
{
|
||||
const QString fileName = context.file ? QString::fromUtf8(context.file) : QStringLiteral("?");
|
||||
const int separatorIndex = qMax(fileName.lastIndexOf(QLatin1Char('/')),
|
||||
fileName.lastIndexOf(QLatin1Char('\\')));
|
||||
return separatorIndex >= 0 ? fileName.mid(separatorIndex + 1) : fileName;
|
||||
}
|
||||
|
||||
// 日志文件以一次打开日志时的本地秒级时间作为会话标识;分卷只追加序号,
|
||||
// 既能让现场直接看出开始时间,也避免把进程 ID 暴露到文件名中。
|
||||
QString createLogFilePrefix(const QDateTime &dateTime)
|
||||
{
|
||||
return dateTime.toString(QStringLiteral("yyyy-MM-dd_hhmmss"));
|
||||
}
|
||||
|
||||
// 仅清理本模块按“日期_时分秒_分卷”命名的日志,避免公共 logs 目录中的宿主业务文件被误删。
|
||||
void removeExpiredLogs(const QDir &logDir, const QDate &today)
|
||||
{
|
||||
const QRegularExpression logNamePattern(
|
||||
QStringLiteral("^\\d{4}-\\d{2}-\\d{2}_\\d{6}_\\d{3}\\.log$"));
|
||||
const QDate oldestDate = today.addDays(-kLogRetentionDays);
|
||||
const QFileInfoList files = logDir.entryInfoList(QStringList() << QStringLiteral("*.log"),
|
||||
QDir::Files | QDir::Readable);
|
||||
for (const QFileInfo &fileInfo : files)
|
||||
{
|
||||
if (!logNamePattern.match(fileInfo.fileName()).hasMatch())
|
||||
{
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
const QDate fileDate = QDate::fromString(fileInfo.fileName().left(10), Qt::ISODate);
|
||||
if (fileDate.isValid() && fileDate < oldestDate)
|
||||
{
|
||||
QFile::remove(fileInfo.absoluteFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在锁保护下为当前日期选择可继续写入的分卷;同一会话始终复用首次生成的详细时间前缀,
|
||||
// 已满分卷永不复用,防止重启后突破大小上限。
|
||||
bool openCurrentLogFileLocked(LogState &state, const QDir &logDir, const QDate &today)
|
||||
{
|
||||
if (state.currentFilePrefix.isEmpty() || state.activeDate != today)
|
||||
{
|
||||
state.currentFilePrefix = createLogFilePrefix(QDateTime::currentDateTime());
|
||||
}
|
||||
const QString prefix = state.currentFilePrefix + QLatin1Char('_');
|
||||
int part = 1;
|
||||
while (true)
|
||||
{
|
||||
const QString filePath = logDir.filePath(prefix + QStringLiteral("%1.log").arg(part, 3, 10, QLatin1Char('0')));
|
||||
const QFileInfo fileInfo(filePath);
|
||||
if (!fileInfo.exists() || fileInfo.size() < maxLogFileBytes())
|
||||
{
|
||||
state.file.setFileName(filePath);
|
||||
if (!state.file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.stream.setDevice(&state.file);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
state.stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
state.stream.setCodec("UTF-8");
|
||||
#endif
|
||||
state.currentPart = part;
|
||||
// QTextStream 可能缓存尚未落盘的内容,分卷判断必须从已打开文件大小开始累计,
|
||||
// 不能在每次消息时直接依赖 QFile::size(),否则高频日志会在 flush 前突破上限。
|
||||
state.currentFileBytes = state.file.size();
|
||||
state.activeDate = today;
|
||||
return true;
|
||||
}
|
||||
++part;
|
||||
}
|
||||
}
|
||||
|
||||
// 在当前文件即将超过上限时切到同一详细时间会话的下一个空闲分卷;连续序号便于现场按顺序定位日志。
|
||||
bool rotateLogFileLocked(LogState &state)
|
||||
{
|
||||
const QFileInfo currentInfo(state.file);
|
||||
const QDir logDir = currentInfo.dir();
|
||||
const QString prefix = state.currentFilePrefix + QLatin1Char('_');
|
||||
int part = state.currentPart + 1;
|
||||
QString filePath;
|
||||
do
|
||||
{
|
||||
filePath = logDir.filePath(prefix + QStringLiteral("%1.log").arg(part, 3, 10, QLatin1Char('0')));
|
||||
++part;
|
||||
} while (QFileInfo::exists(filePath));
|
||||
|
||||
state.stream.flush();
|
||||
state.file.flush();
|
||||
state.file.close();
|
||||
state.stream.setDevice(nullptr);
|
||||
state.file.setFileName(filePath);
|
||||
if (!state.file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.stream.setDevice(&state.file);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
state.stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
state.stream.setCodec("UTF-8");
|
||||
#endif
|
||||
state.currentPart = part - 1;
|
||||
state.currentFileBytes = 0;
|
||||
state.activeDate = QDate::currentDate();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 全局 Qt 回调负责格式化并串行写盘;致命等级只保证最后一次 flush,终止动作仍由 Qt 在回调返回后执行。
|
||||
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
if (g_isHandlingMessage)
|
||||
{
|
||||
const QByteArray recursiveMessage = msg.toLocal8Bit();
|
||||
std::fputs(recursiveMessage.constData(), stderr);
|
||||
std::fputc('\n', stderr);
|
||||
return;
|
||||
}
|
||||
QScopedValueRollback<bool> recursionGuard(g_isHandlingMessage, true);
|
||||
|
||||
const QString level = levelName(type);
|
||||
const QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz"));
|
||||
const QString fileName = shortFileName(context);
|
||||
const QString functionName = context.function ? QString::fromUtf8(context.function) : QStringLiteral("?");
|
||||
const QString fileMessage = QStringLiteral("%1-[%2](%3:%4,%5): %6")
|
||||
.arg(timestamp, level, fileName)
|
||||
.arg(context.line)
|
||||
.arg(functionName, msg);
|
||||
const QString consoleMessage = QStringLiteral("%1-[%2](%3:%4,%5): %6")
|
||||
.arg(timestamp, consoleLevel(type, level), fileName)
|
||||
.arg(context.line)
|
||||
.arg(functionName, msg);
|
||||
|
||||
const QByteArray consoleBytes = consoleMessage.toLocal8Bit();
|
||||
std::fwrite(consoleBytes.constData(), 1, static_cast<size_t>(consoleBytes.size()), stdout);
|
||||
std::fputc('\n', stdout);
|
||||
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
if (!g_logState.installed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 跨午夜时先关闭旧日期文件再按新日期选择分卷;轮转失败后的后续消息也会重试打开,
|
||||
// 这样临时磁盘或权限故障恢复后无需重启应用即可恢复文件日志。
|
||||
const QDate today = QDate::currentDate();
|
||||
if (g_logState.activeDate != today && g_logState.file.isOpen())
|
||||
{
|
||||
g_logState.stream.flush();
|
||||
g_logState.file.flush();
|
||||
g_logState.file.close();
|
||||
g_logState.stream.setDevice(nullptr);
|
||||
g_logState.currentFileBytes = 0;
|
||||
g_logState.currentPart = 0;
|
||||
}
|
||||
if (!g_logState.file.isOpen())
|
||||
{
|
||||
const QDir logDir(logDirectoryPath());
|
||||
if (!openCurrentLogFileLocked(g_logState, logDir, today))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
g_file.setFileName(_logPath);
|
||||
if(!g_file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append))
|
||||
const qint64 messageBytes = fileMessage.toUtf8().size() + 1;
|
||||
if (g_logState.currentFileBytes + messageBytes > maxLogFileBytes() && !rotateLogFileLocked(g_logState))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
g_stream.setDevice(&g_file);
|
||||
g_stream.setCodec("UTF-8");
|
||||
g_lastFlushMs = QDateTime::currentMSecsSinceEpoch();
|
||||
qInstallMessageHandler(messageHandler); // 给 Qt 安装自定义消息处理函数
|
||||
g_logState.stream << fileMessage << '\n';
|
||||
g_logState.currentFileBytes += messageBytes;
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
if (type == QtCriticalMsg || type == QtFatalMsg || now - g_logState.lastFlushMs >= kLogFlushIntervalMs)
|
||||
{
|
||||
g_logState.stream.flush();
|
||||
g_logState.file.flush();
|
||||
g_logState.lastFlushMs = now;
|
||||
if (type == QtCriticalMsg || type == QtFatalMsg)
|
||||
{
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消安装消息处理函数并释放资源
|
||||
// 安装函数只在文件已成功打开后才接管 Qt 全局 handler,失败时保留宿主原有 handler 并允许后续重试。
|
||||
bool LogHandler::installMessageHandler()
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
if (g_logState.installed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const QDir logDir(logDirectoryPath());
|
||||
if (!logDir.exists() && !QDir().mkpath(logDir.absolutePath()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const QDate today = QDate::currentDate();
|
||||
removeExpiredLogs(logDir, today);
|
||||
if (!openCurrentLogFileLocked(g_logState, logDir, today))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
g_logState.lastFlushMs = QDateTime::currentMSecsSinceEpoch();
|
||||
g_logState.previousHandler = qInstallMessageHandler(messageHandler);
|
||||
g_logState.installed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 卸载函数在锁内先同步关闭流再恢复旧 handler,确保回调并发进入时不会写到已释放的 QTextStream。
|
||||
void LogHandler::uninstallMessageHandler()
|
||||
{
|
||||
QMutexLocker locker(&g_mutex);
|
||||
if(g_file.isOpen())
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
if (!g_logState.installed)
|
||||
{
|
||||
g_stream.flush();
|
||||
g_file.flush();
|
||||
g_file.close();
|
||||
return;
|
||||
}
|
||||
g_stream.setDevice(nullptr);
|
||||
qInstallMessageHandler(nullptr);
|
||||
|
||||
qInstallMessageHandler(g_logState.previousHandler);
|
||||
g_logState.previousHandler = nullptr;
|
||||
if (g_logState.file.isOpen())
|
||||
{
|
||||
g_logState.stream.flush();
|
||||
g_logState.file.flush();
|
||||
g_logState.file.close();
|
||||
}
|
||||
g_logState.stream.setDevice(nullptr);
|
||||
g_logState.currentFileBytes = 0;
|
||||
g_logState.currentPart = 0;
|
||||
g_logState.activeDate = QDate();
|
||||
g_logState.currentFilePrefix.clear();
|
||||
g_logState.installed = false;
|
||||
}
|
||||
|
||||
// 查询函数与安装/卸载共用互斥锁,避免其他线程在关闭过程看到过期安装状态。
|
||||
bool LogHandler::isInstalled() const
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
return g_logState.installed;
|
||||
}
|
||||
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
// 测试目录设置在同一把锁下更新,并要求 handler 已卸载,保证没有线程持有旧目录的文件句柄。
|
||||
void LogHandlerTestAccess::setLogDirectory(const QString &directory)
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
Q_ASSERT(!g_logState.installed);
|
||||
g_logState.testLogDirectory = directory;
|
||||
}
|
||||
|
||||
// 分卷阈值只影响随后打开的测试文件;非正值表示删除覆盖并回退到正式默认值。
|
||||
void LogHandlerTestAccess::setMaxLogFileBytes(qint64 bytes)
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
Q_ASSERT(!g_logState.installed);
|
||||
g_logState.testMaxLogFileBytes = bytes > 0 ? bytes : 0;
|
||||
}
|
||||
|
||||
// 测试结束后统一清除覆盖状态,避免同一 QtTest 进程中的下一条用例意外复用临时环境。
|
||||
void LogHandlerTestAccess::reset()
|
||||
{
|
||||
QMutexLocker locker(&g_logState.mutex);
|
||||
Q_ASSERT(!g_logState.installed);
|
||||
g_logState.testLogDirectory.clear();
|
||||
g_logState.testMaxLogFileBytes = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
+48
-6
@@ -1,13 +1,37 @@
|
||||
#ifndef LOGHANDLER_H
|
||||
#ifndef LOGHANDLER_H
|
||||
#define LOGHANDLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QtCore/qglobal.h>
|
||||
|
||||
class Q_DECL_EXPORT LogHandler
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
#include <QString>
|
||||
#endif
|
||||
|
||||
// 公用库在动态库、源码嵌入和静态链接三种场景下使用不同导出声明,
|
||||
// 避免消费者把库符号错误标记为导出符号;源码嵌入模式由 MyLogger.pri 定义 MYLOGGER_STATIC。
|
||||
#if defined(MYLOGGER_STATIC)
|
||||
# define MYLOGGER_EXPORT
|
||||
#elif defined(MYLOGGER_LIBRARY)
|
||||
# define MYLOGGER_EXPORT Q_DECL_EXPORT
|
||||
#else
|
||||
# define MYLOGGER_EXPORT Q_DECL_IMPORT
|
||||
#endif
|
||||
|
||||
// 进程级 Qt 消息处理器封装类,负责把 qDebug 等全局日志安全地写入本地文件。
|
||||
// 同一进程只能安装一个 Qt 消息处理器,因此宿主应把其安装和卸载权交给同一处生命周期管理代码。
|
||||
class MYLOGGER_EXPORT LogHandler
|
||||
{
|
||||
public:
|
||||
void installMessageHandler(); // 给Qt安装消息处理函数
|
||||
void uninstallMessageHandler(); // 取消安装消息处理函数并释放资源
|
||||
// 安装全局消息处理器并打开当天日志;目录或文件不可用时返回 false,调用者可在恢复权限后重试。
|
||||
bool installMessageHandler();
|
||||
|
||||
// 卸载当前处理器、恢复安装前的处理器并同步落盘;重复调用不会影响已经恢复后的状态。
|
||||
void uninstallMessageHandler();
|
||||
|
||||
// 返回本库是否仍持有并安装了 Qt 全局消息处理器,供宿主避免重复管理生命周期。
|
||||
bool isInstalled() const;
|
||||
|
||||
// 返回进程内唯一实例;C++11 局部静态对象保证首次构造线程安全。
|
||||
static LogHandler& Get()
|
||||
{
|
||||
static LogHandler m_logHandler;
|
||||
@@ -15,7 +39,25 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
LogHandler() {};
|
||||
// 禁止外部构造,确保文件句柄和 Qt 全局 handler 只由唯一实例管理。
|
||||
LogHandler() = default;
|
||||
};
|
||||
|
||||
#if defined(MYLOGGER_TESTING)
|
||||
// 仅测试构建可见的访问类,用于把文件系统和分卷阈值隔离到临时目录;
|
||||
// 正式库不定义该宏,因此不会向宿主程序暴露运行时配置接口。
|
||||
class LogHandlerTestAccess
|
||||
{
|
||||
public:
|
||||
// 设置测试专用日志目录;调用前必须卸载 handler,避免正在写入的文件被切换。
|
||||
static void setLogDirectory(const QString &directory);
|
||||
|
||||
// 覆盖单文件分卷阈值;传入非正数时恢复正式库的 10 MiB 默认值。
|
||||
static void setMaxLogFileBytes(qint64 bytes);
|
||||
|
||||
// 清除测试覆盖项,恢复正式库默认路径和分卷阈值。
|
||||
static void reset();
|
||||
};
|
||||
#endif
|
||||
|
||||
#endif // LOGHANDLER_H
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
QT += core testlib
|
||||
TEMPLATE = app
|
||||
TARGET = MyLoggerTests
|
||||
|
||||
# 测试直接编译被测源码,并显式开启测试访问宏;不会链接或改写正式 MyLogger 动态库。
|
||||
CONFIG += testcase console c++17
|
||||
DEFINES += MYLOGGER_STATIC MYLOGGER_TESTING QT_MESSAGELOGCONTEXT
|
||||
INCLUDEPATH += $$PWD/..
|
||||
|
||||
HEADERS += $$PWD/../loghandler.h
|
||||
SOURCES += \
|
||||
$$PWD/tst_loghandler.cpp \
|
||||
$$PWD/../loghandler.cpp
|
||||
@@ -0,0 +1,262 @@
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <QDate>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
|
||||
#include "loghandler.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::atomic_int g_previousHandlerCalls{0};
|
||||
|
||||
// 测试替代 handler 只计数、不调用 Qt 日志 API,避免断言旧 handler 恢复时自身触发递归输出。
|
||||
void previousHandler(QtMsgType, const QMessageLogContext &, const QString &)
|
||||
{
|
||||
++g_previousHandlerCalls;
|
||||
}
|
||||
}
|
||||
|
||||
// MyLogger 的 QtTest 用例类:每个用例使用独立临时目录和独立全局 handler,
|
||||
// 使进程级 qInstallMessageHandler 状态不会跨用例泄漏或污染开发机实际 logs 目录。
|
||||
class LogHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
// 套件开始前保存测试进程原有 handler,结束后恢复,避免 QtTest 宿主的日志策略被本模块测试改变。
|
||||
void initTestCase();
|
||||
void cleanupTestCase();
|
||||
|
||||
// 每条用例前后重置全局 logger、测试覆盖和临时目录,确保用例可独立重复运行。
|
||||
void init();
|
||||
void cleanup();
|
||||
|
||||
// 覆盖安装成功、重复安装、重复卸载以及安装状态查询。
|
||||
void installAndUninstallAreIdempotent();
|
||||
|
||||
// 覆盖日志目录不可用后的失败返回及恢复目录后可重新安装。
|
||||
void installFailureCanBeRetried();
|
||||
|
||||
// 覆盖安装期间接管输出、卸载后恢复调用前 Qt handler 的公共库契约。
|
||||
void previousHandlerIsRestored();
|
||||
|
||||
// 覆盖 UTF-8 内容、等级和源文件信息已实际写入文件。
|
||||
void writesUtf8MessageToFile();
|
||||
|
||||
// 覆盖多线程写入不会死锁,且每个线程的固定标识都能完整落盘。
|
||||
void writesFromMultipleThreads();
|
||||
|
||||
// 覆盖达到测试阈值后产生连续分卷文件。
|
||||
void rotatesAtConfiguredSize();
|
||||
|
||||
// 覆盖只清理由模块命名规则匹配且超过保留期的日志,不触碰宿主文件。
|
||||
void removesOnlyExpiredOwnedLogs();
|
||||
|
||||
private:
|
||||
// 返回临时目录内模块生成的日志内容,用于在 qCritical 强制 flush 后做精确断言。
|
||||
QString readAllLogs() const;
|
||||
|
||||
// 清理前一条用例留下的文件,使当前用例的文件数量和分卷序号只反映自身行为。
|
||||
void clearTestLogDirectory();
|
||||
|
||||
// 保存测试进程原始 handler;临时目录由每条用例创建,生命周期覆盖断言和 cleanup。
|
||||
QtMessageHandler m_originalHandler = nullptr;
|
||||
QTemporaryDir m_logDirectory;
|
||||
};
|
||||
|
||||
// 套件开始时保存 QtTest 进程原有 handler,后续用例可自由替换而不影响测试进程退出后的默认行为。
|
||||
void LogHandlerTest::initTestCase()
|
||||
{
|
||||
m_originalHandler = qInstallMessageHandler(nullptr);
|
||||
qInstallMessageHandler(m_originalHandler);
|
||||
}
|
||||
|
||||
// 套件结束时先确保 logger 已释放文件句柄,再清除测试覆盖并恢复原始 handler。
|
||||
void LogHandlerTest::cleanupTestCase()
|
||||
{
|
||||
LogHandler::Get().uninstallMessageHandler();
|
||||
LogHandlerTestAccess::reset();
|
||||
qInstallMessageHandler(m_originalHandler);
|
||||
}
|
||||
|
||||
// 每条测试开始前创建干净的目录和替代 handler,防止进程级状态从前序用例泄漏。
|
||||
void LogHandlerTest::init()
|
||||
{
|
||||
QVERIFY(m_logDirectory.isValid());
|
||||
LogHandler::Get().uninstallMessageHandler();
|
||||
LogHandlerTestAccess::reset();
|
||||
clearTestLogDirectory();
|
||||
LogHandlerTestAccess::setLogDirectory(m_logDirectory.path());
|
||||
qInstallMessageHandler(previousHandler);
|
||||
g_previousHandlerCalls = 0;
|
||||
}
|
||||
|
||||
// 每条测试结束后恢复运行时默认状态,保证 QtTest 自身和下一条用例不再受 MyLogger 接管。
|
||||
void LogHandlerTest::cleanup()
|
||||
{
|
||||
LogHandler::Get().uninstallMessageHandler();
|
||||
LogHandlerTestAccess::reset();
|
||||
qInstallMessageHandler(m_originalHandler);
|
||||
}
|
||||
|
||||
// 验证安装与卸载均幂等,重复调用不会留下错误状态或引发全局 handler 竞争。
|
||||
void LogHandlerTest::installAndUninstallAreIdempotent()
|
||||
{
|
||||
QVERIFY(LogHandler::Get().installMessageHandler());
|
||||
QVERIFY(LogHandler::Get().isInstalled());
|
||||
QVERIFY(LogHandler::Get().installMessageHandler());
|
||||
|
||||
LogHandler::Get().uninstallMessageHandler();
|
||||
QVERIFY(!LogHandler::Get().isInstalled());
|
||||
LogHandler::Get().uninstallMessageHandler();
|
||||
}
|
||||
|
||||
// 用普通文件冒充目录模拟路径不可用,随后切回临时目录验证失败不会永久锁死安装状态。
|
||||
void LogHandlerTest::installFailureCanBeRetried()
|
||||
{
|
||||
const QString blockedPath = m_logDirectory.filePath(QStringLiteral("not-a-directory"));
|
||||
QFile blockedFile(blockedPath);
|
||||
QVERIFY(blockedFile.open(QIODevice::WriteOnly));
|
||||
blockedFile.close();
|
||||
|
||||
LogHandlerTestAccess::setLogDirectory(blockedPath);
|
||||
QVERIFY(!LogHandler::Get().installMessageHandler());
|
||||
QVERIFY(!LogHandler::Get().isInstalled());
|
||||
|
||||
LogHandlerTestAccess::setLogDirectory(m_logDirectory.path());
|
||||
QVERIFY(LogHandler::Get().installMessageHandler());
|
||||
}
|
||||
|
||||
// 验证安装期间旧 handler 不接收消息,而卸载后立即恢复为当前 Qt 全局处理器。
|
||||
void LogHandlerTest::previousHandlerIsRestored()
|
||||
{
|
||||
QVERIFY(LogHandler::Get().installMessageHandler());
|
||||
qInfo() << "while-mylogger-is-installed";
|
||||
QCOMPARE(g_previousHandlerCalls.load(), 0);
|
||||
|
||||
LogHandler::Get().uninstallMessageHandler();
|
||||
qInfo() << "after-mylogger-is-uninstalled";
|
||||
QCOMPARE(g_previousHandlerCalls.load(), 1);
|
||||
}
|
||||
|
||||
// 验证 qCritical 立即 flush 后,日志文件保留 UTF-8 中文、等级和源文件上下文。
|
||||
void LogHandlerTest::writesUtf8MessageToFile()
|
||||
{
|
||||
QVERIFY(LogHandler::Get().installMessageHandler());
|
||||
qCritical() << QStringLiteral("中文 UTF-8 日志标记");
|
||||
|
||||
const QString content = readAllLogs();
|
||||
QVERIFY(content.contains(QStringLiteral("ERROR")));
|
||||
QVERIFY(content.contains(QStringLiteral("中文 UTF-8 日志标记")));
|
||||
QVERIFY(content.contains(QStringLiteral("tst_loghandler.cpp")));
|
||||
}
|
||||
|
||||
// 用多个标准线程并发输出固定标识,验证互斥写盘不会死锁或混淆消息内容。
|
||||
void LogHandlerTest::writesFromMultipleThreads()
|
||||
{
|
||||
QVERIFY(LogHandler::Get().installMessageHandler());
|
||||
constexpr int threadCount = 4;
|
||||
constexpr int messagesPerThread = 40;
|
||||
std::vector<std::thread> workers;
|
||||
workers.reserve(threadCount);
|
||||
|
||||
for (int threadIndex = 0; threadIndex < threadCount; ++threadIndex)
|
||||
{
|
||||
workers.emplace_back([threadIndex]()
|
||||
{
|
||||
for (int messageIndex = 0; messageIndex < messagesPerThread; ++messageIndex)
|
||||
{
|
||||
qInfo().noquote() << QStringLiteral("worker-%1-message-%2").arg(threadIndex).arg(messageIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (std::thread &worker : workers)
|
||||
{
|
||||
worker.join();
|
||||
}
|
||||
|
||||
qCritical() << "flush-concurrent-log";
|
||||
const QString content = readAllLogs();
|
||||
for (int threadIndex = 0; threadIndex < threadCount; ++threadIndex)
|
||||
{
|
||||
for (int messageIndex = 0; messageIndex < messagesPerThread; ++messageIndex)
|
||||
{
|
||||
QVERIFY(content.contains(QStringLiteral("worker-%1-message-%2").arg(threadIndex).arg(messageIndex)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通过测试专用的小阈值触发多次轮转,并确认连续的第一、第二分卷均被创建。
|
||||
void LogHandlerTest::rotatesAtConfiguredSize()
|
||||
{
|
||||
LogHandlerTestAccess::setMaxLogFileBytes(1024);
|
||||
QVERIFY(LogHandler::Get().installMessageHandler());
|
||||
const QString payload(300, QLatin1Char('x'));
|
||||
for (int index = 0; index < 10; ++index)
|
||||
{
|
||||
qInfo().noquote() << QStringLiteral("rotate-%1-%2").arg(index).arg(payload);
|
||||
}
|
||||
qCritical() << "flush-rotated-log";
|
||||
|
||||
const QStringList files = QDir(m_logDirectory.path()).entryList(QStringList() << QStringLiteral("*.log"), QDir::Files);
|
||||
QVERIFY(files.size() >= 2);
|
||||
QVERIFY(!files.filter(QRegularExpression(QStringLiteral("_001\\.log$"))).isEmpty());
|
||||
QVERIFY(!files.filter(QRegularExpression(QStringLiteral("_002\\.log$"))).isEmpty());
|
||||
}
|
||||
|
||||
// 验证清理策略只处理超过保留期且符合 MyLogger 命名约定的文件,宿主业务日志必须保留。
|
||||
void LogHandlerTest::removesOnlyExpiredOwnedLogs()
|
||||
{
|
||||
// 过期文件必须严格符合新命名规则:日期、启动时分秒和分卷号,
|
||||
// 才能验证清理逻辑不会把同目录中其他业务日志误认为 MyLogger 文件。
|
||||
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 hostLogPath = m_logDirectory.filePath(QStringLiteral("host-business.log"));
|
||||
QFile oldOwnedFile(oldOwnedPath);
|
||||
QVERIFY(oldOwnedFile.open(QIODevice::WriteOnly));
|
||||
oldOwnedFile.close();
|
||||
QFile hostLogFile(hostLogPath);
|
||||
QVERIFY(hostLogFile.open(QIODevice::WriteOnly));
|
||||
hostLogFile.close();
|
||||
|
||||
QVERIFY(LogHandler::Get().installMessageHandler());
|
||||
QVERIFY(!QFileInfo::exists(oldOwnedPath));
|
||||
QVERIFY(QFileInfo::exists(hostLogPath));
|
||||
}
|
||||
|
||||
// 汇总当前用例生成的 UTF-8 日志,避免依赖单一分卷名称或文件系统遍历顺序。
|
||||
QString LogHandlerTest::readAllLogs() const
|
||||
{
|
||||
QString content;
|
||||
const QFileInfoList files = QDir(m_logDirectory.path()).entryInfoList(QStringList() << QStringLiteral("*.log"), QDir::Files);
|
||||
for (const QFileInfo &fileInfo : files)
|
||||
{
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
content += QString::fromUtf8(file.readAll());
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// 临时目录仅允许本测试创建普通文件;每条用例前删除它们可消除前序分卷对后续断言的影响。
|
||||
void LogHandlerTest::clearTestLogDirectory()
|
||||
{
|
||||
const QFileInfoList entries = QDir(m_logDirectory.path()).entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||
for (const QFileInfo &entry : entries)
|
||||
{
|
||||
QVERIFY2(QFile::remove(entry.absoluteFilePath()), qPrintable(entry.absoluteFilePath()));
|
||||
}
|
||||
}
|
||||
|
||||
QTEST_MAIN(LogHandlerTest)
|
||||
#include "tst_loghandler.moc"
|
||||
Reference in New Issue
Block a user