项目中需求一日志模块,主要实现两大功能:1.自动打印信息至日志文件;2.软件意外退出时保留信息以便跟踪问题。
本文结合了 Qt 自定义日志工具 和 让程序在崩溃时体面的退出之CallStack 提供的方法,补充实现了文章中未具体给出的管理日志文件大小和数量的功能。
环境:vs2012+Qt5.2(注:Qt5.5之后引入qInfo(),影响不大)
基本原理是使用 qInstallMessageHandler()接管qDebug(), qWarning()等调试信息,然后将信息流存储至本地日志文件,管理日志文件。
代码在原作者基础上做了部分调整:
1.更改日志存储名称格式,用QDateTime取代QDate,以避免当日记录多条日志时的覆盖问题;
2.增加日志文件个数的判断;
3.增加日志文件大小的检测;
4.屏蔽根据修改日期保存日志机制,以免在不同日期开启软件后冲掉以前的有用log,仅凭文件大小另存log文件,然后控制文件数量。
#include "LogHandler.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define LOGLIMIT_NUM 5 //日志文件存档个数
#define LOGLIMIT_SIZE 500 //单个日志文件存档大小限制,单位KB
/************************************************************************************************************
* *
* LogHandlerPrivate *
* *
***********************************************************************************************************/
struct LogHandlerPrivate {
LogHandlerPrivate();
~LogHandlerPrivate();
// 打开日志文件 protocal.log,如果日志文件不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 protocal.log
void openAndBackupLogFile();
// 消息处理函数
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg);
// 如果日志所在目录不存在,则创建
void makeSureLogDirectory() const;
// 检测当前日志文件大小
void checkLogFiles();
QDir logDir; // 日志文件夹
QTimer renameLogFileTimer; // 重命名日志文件使用的定时器
QTimer flushLogFileTimer; // 刷新输出到日志文件的定时器
QDateTime logFileCreatedDate; // 日志文件创建的时间
static QFile *logFile; // 日志文件
static QTextStream *logOut; // 输出日志的 QTextStream,使用静态对象就是为了减少函数调用的开销
static QMutex logMutex; // 同步使用的 mutex
};
// 初始化 static 变量
QMutex LogHandlerPrivate::logMutex;
QFile* LogHandlerPrivate::logFile = NULL;
QTextStream* LogHandlerPrivate::logOut = NULL;
LogHandlerPrivate::LogHandlerPrivate() {
logDir.setPath("Log"); // TODO: 日志文件夹的路径,为 exe 所在目录下的 log 文件夹,可从配置文件读取
QString logPath = logDir.absoluteFilePath("protocal.log"); // 日志的路径
// 日志文件创建的时间
// QFileInfo::created(): On most Unix systems, this function returns the time of the last status change.
// 所以不能运行时使用这个函数检查创建时间,因为会在运行时变化,所以在程序启动时保存下日志文件创建的时间
logFileCreatedDate = QFileInfo(logPath).lastModified();
//QString temp= logFileCreatedDate.toString("yyyy-MM-dd hh:mm:ss");
// 打开日志文件,如果不是当天创建的,备份已有日志文件
openAndBackupLogFile();
// 五分钟检查一次日志文件创建时间
renameLogFileTimer.setInterval(1000 * 60 * 5); // TODO: 可从配置文件读取
//renameLogFileTimer.setInterval(1000*60); // 为了快速测试看到日期变化后是否新创建了对应的日志文件,所以 1 分钟检查一次
renameLogFileTimer.start();
QObject::connect(&renameLogFileTimer, &QTimer::timeout, [this] {
QMutexLocker locker(&LogHandlerPrivate::logMutex);
openAndBackupLogFile();
});
// 定时刷新日志输出到文件,尽快的能在日志文件里看到最新的日志
flushLogFileTimer.setInterval(1000); // TODO: 可从配置文件读取
flushLogFileTimer.start();
QObject::connect(&flushLogFileTimer, &QTimer::timeout, [this] {
// qDebug() << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); // 测试不停的写入内容到日志文件
QMutexLocker locker(&LogHandlerPrivate::logMutex);
// if (NULL != logOut) {
// logOut->flush();
// }
checkLogFiles();//每秒检查一次文件是否超过限制大小
});
}
LogHandlerPrivate::~LogHandlerPrivate() {
if (NULL != logFile) {
logFile->flush();
logFile->close();
delete logOut;
delete logFile;
// 因为他们是 static 变量
logOut = NULL;
logFile = NULL;
}
}
// 打开日志文件 protocal.log,如果不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd_hhmmss.log,并重新创建一个 protocal.log
void LogHandlerPrivate::openAndBackupLogFile() {
// 总体逻辑:
// 1. 程序启动时 logFile 为 NULL,初始化 logFile,有可能是同一天打开已经存在的 logFile,所以使用 Append 模式
// 2. logFileCreatedDate is null, 说明日志文件在程序开始时不存在,所以记录下创建时间
// 3. 程序运行时检查如果 logFile 的创建日期和当前日期不相等,则使用它的创建日期重命名,然后再生成一个新的 protocal.log 文件
// 4. 检查日志文件超过 LOGLIMIT_NUM 个,删除最早的
makeSureLogDirectory(); // 如果日志所在目录不存在,则创建
QString logPath = logDir.absoluteFilePath("protocal.log"); // 日志的路径
// [[1]] 程序启动时 logFile 为 NULL
if (NULL == logFile) {
logFile = new QFile(logPath);
logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) ? new QTextStream(logFile) : NULL;
if (NULL != logOut) {
logOut->setCodec("UTF-8");
}
// [[2]] 如果文件是第一次创建,则创建日期是无效的,把其设置为当前日期
if (logFileCreatedDate.isNull()) {
logFileCreatedDate = QDateTime::currentDateTime();
}
}
// [[3]] 程序运行时如果创建日期不是当前日期,则使用创建日期重命名,并生成一个新的 protocal.log
//不使用该特性,以免在不同日期开启软件后冲掉以前的有用log,仅凭文件大小另存log文件,见checkLogFiles
// if (logFileCreatedDate.date() != QDate::currentDate()) {
// logFile->flush();
// logFile->close();
// delete logOut;
// delete logFile;
//
// QString newLogPath = logDir.absoluteFilePath(logFileCreatedDate.toString("yyyy-MM-dd_hhmmss.log"));;
// QFile::copy(logPath, newLogPath); // Bug: 按理说 rename 会更合适,但是 rename 时最后一个文件总是显示不出来,需要 killall Finder 后才出现
// QFile::remove(logPath); // 删除重新创建,改变创建时间
//
// logFile = new QFile(logPath);
// logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) ? new QTextStream(logFile) : NULL;
// logFileCreatedDate = QDateTime::currentDateTime();
//
// if (NULL != logOut) {
// logOut->setCodec("UTF-8");
// }
// }
// [[4]] 检查日志文件超过 LOGLIMIT_NUM 个,删除最早的
logDir.setFilter(QDir::Files);
logDir.setNameFilters(QStringList() << "*.log");//根据文件后缀过滤日志文件
QFileInfoList logFiles = logDir.entryInfoList();
for (int i = 0; i < logFiles.length() - LOGLIMIT_NUM; ++i)
QFile::remove(logFiles[i].absoluteFilePath());
//根据文件名称进一步过滤
// QMap fileDates;
// for (int i = 0; i < logFiles.length(); ++i)
// {
// QString name = logFiles[i].baseName();
// QDateTime fileDateTime = QDateTime::fromString(name, "yyyy-MM-dd");
//
// if (fileDateTime.isValid())
// fileDates.insert(fileDateTime, logFiles[i].absoluteFilePath());
// }
// QList fileDateNames = fileDates.values();
// for (int i = 0; i < fileDateNames.length() - LOGFILESLIMIT; ++i)
// QFile::remove(fileDateNames[i]);
}
// 如果日志所在目录不存在,则创建
void LogHandlerPrivate::makeSureLogDirectory() const {
if (!logDir.exists()) {
logDir.mkpath("."); // 可以递归的创建文件夹
}
}
// 检测当前日志文件大小
void LogHandlerPrivate::checkLogFiles() {
// 如果 protocal.log 文件大小超过5M,重新创建一个日志文件,原文件存档为yyyy-MM-dd_hhmmss.log
if (logFile->size() > 1024*LOGLIMIT_SIZE) {
logFile->flush();
logFile->close();
delete logOut;
delete logFile;
QString logPath = logDir.absoluteFilePath("protocal.log"); // 日志的路径
QString newLogPath = logDir.absoluteFilePath(logFileCreatedDate.toString("yyyy-MM-dd_hhmmss.log"));;
QFile::copy(logPath, newLogPath); // Bug: 按理说 rename 会更合适,但是 rename 时最后一个文件总是显示不出来,需要 killall Finder 后才出现
QFile::remove(logPath); // 删除重新创建,改变创建时间
logFile = new QFile(logPath);
logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) ? new QTextStream(logFile) : NULL;
logFileCreatedDate = QDateTime::currentDateTime();
if (NULL != logOut) {
logOut->setCodec("UTF-8");
}
}
}
// 消息处理函数
void LogHandlerPrivate::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
QMutexLocker locker(&LogHandlerPrivate::logMutex);
QString level;
switch (type) {
case QtDebugMsg:
level = "Debug";
break;
// case QtInfoMsg://This function was introduced in Qt 5.5.
// level = "Info ";
// break;
case QtWarningMsg:
level = "Warning";
break;
case QtCriticalMsg:
level = "Error";
break;
case QtFatalMsg:
level = "Fatal";
break;
default:;
}
// 输出到标准输出
QByteArray localMsg = msg.toLocal8Bit();
//std::cout << std::string(localMsg) << std::endl;
if (NULL == LogHandlerPrivate::logOut) {
return;
}
// 输出到日志文件, 格式: 时间 - [Level] (文件名:行数, 函数): 消息
QString fileName = context.file;
int index = fileName.lastIndexOf(QDir::separator());
fileName = fileName.mid(index + 1);
(*LogHandlerPrivate::logOut) << QString("%1 - [%2] (%3:%4): %5\n")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(level)
.arg(fileName).arg(context.line)/*.arg(context.function)*/.arg(msg);
logOut->flush();//直接刷新到文件
}
/************************************************************************************************************
* *
* LogHandler *
* *
***********************************************************************************************************/
LogHandler::LogHandler() : d(NULL) {
}
LogHandler::~LogHandler() {
}
void LogHandler::installMessageHandler() {
QMutexLocker locker(&LogHandlerPrivate::logMutex);
if (NULL == d) {
d = new LogHandlerPrivate();
qInstallMessageHandler(LogHandlerPrivate::messageHandler); // 给 Qt 安装自定义消息处理函数
}
}
void LogHandler::release() {
QMutexLocker locker(&LogHandlerPrivate::logMutex);
qInstallMessageHandler(0);
delete d;
d = NULL;
}
让程序在崩溃时体面的退出之总结博主在系列文章中做了详尽的说明。
我的应用目的是在程序崩溃时能体面退出,然后记录基本的CallStack信息到日志文件,所以只用到了前面两部分内容。在上文的基础上,用qCritical()或其他方法输出Crash信息和CallStack信息即可。
//程式异常捕获
LONG ApplicationCrashHandler(EXCEPTION_POINTERS *pException){
/*
***保存数据代码***
*/
// 创建Dump文件目录
QDir DumpDir;
DumpDir.setPath("Log");
LPCWSTR DumpPath = (const wchar_t*) DumpDir.absoluteFilePath("ProtocolTester.dmp").utf16();// Dump文件的路径
CreateDumpFile(DumpPath, pException);
// 确保有足够的栈空间
#ifdef _M_IX86
if (pException->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW)
{
static char TempStack[1024 * 128];
__asm mov eax,offset TempStack[1024 * 128];
__asm mov esp,eax;
}
#endif
CrashInfo crashinfo = GetCrashInfo(pException->ExceptionRecord);
// 输出Crash信息
qCritical() << "ErrorCode: " << crashinfo.ErrorCode << endl;
qCritical() << "Address: " << crashinfo.Address << endl;
qCritical() << "Flags: " << crashinfo.Flags << endl;
vector arrCallStackInfo = GetCallStack(pException->ContextRecord);
// 输出CallStack
qCritical() << "CallStack: " << endl;
for (vector ::iterator i = arrCallStackInfo.begin(); i != arrCallStackInfo.end(); ++i)
{
CallStackInfo callstackinfo = (*i);
qCritical() << callstackinfo.MethodName << "() : [" << callstackinfo.ModuleName << "] (File: " << callstackinfo.FileName << " @Line " << callstackinfo.LineNumber << ")" << endl;
}
//这里弹出一个错误对话框并退出程序
EXCEPTION_RECORD* record = pException->ExceptionRecord;
QString errCode(QString::number(record->ExceptionCode,16)),errAdr(QString::number((uint)record->ExceptionAddress,16)),errMod;
QMessageBox::critical(NULL,QStringLiteral("Error"),QStringLiteral("很抱歉,程序出错了。
")+
QStringLiteral("错误代码:%1错误地址:%2").arg(errCode).arg(errAdr),
QMessageBox::Ok);
return EXCEPTION_EXECUTE_HANDLER;
}
本文实现了一个轻量的Qt日志模块,功能肯定是没有log4qt或log4cxx等强大,但也基本满足了项目应用需求,想了解log4qt也可以查看DevBean豆子大神的github
最后附上相关源码:一个轻量的Qt日志模块
Qt 自定义日志工具
让程序在崩溃时体面的退出之CallStack
Qt 日志模块的使用
Qt5 中使用 log4qt 输出日志