Qt 自定义日志工具
Qt
C++ 中比较不错的日志工具有 log4cxx,log4qt 等,但是它们都不能和 qDebug(), qInfo() 等有机的结合在一起,所以在 Qt 中使用总觉得不够舒服,感谢 Qt 提供了 qInstallMessageHandler() 这个函数,使用这个函数可以安装自定义的日志输出处理函数,把日志输出到文件,控制台等,具体的使用可以查看 Qt 的帮助文档。
本文主要是介绍使用 qInstallMessageHandler() 实现一个简单的日志工具,例如调用 qDebug() << “Hi”,输出的内容会同时输出到日志文件和控制台,并且日志文件如果不是当天创建的,会使用它的创建日期备份起来,涉及到的文件有:
kcLog.pro:工程
main.cpp: 使用示例
Singleton.h: 单例模版
kcLog.h: 自定义日志相关类的头文件
kcLog.cpp: 自定义日志相关类的实现文件
工程下载地址:
https://download.csdn.net/download/sirkang/12418621
定义 QT_MESSAGELOGCONTEXT
qDebug 其实是一个宏: #define qDebug QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).debug,在 Debug 版本的时候会输出行号,文件名,函数名等,但是在 Release 版本的时候不会输出,为了输出它们,需要在 .pro 文件里加入下面的定义:
DEFINES += QT_MESSAGELOGCONTEXT
kcLog.pro
QT -= core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
#打印日志方式
CONFIG += console
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
DEFINES += QT_MESSAGELOGCONTEXT
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
kcLog.cpp \
main.cpp
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
HEADERS += \
Singleton.h \
kcLog.h
main.cpp
#include
#include
#include "kcLog.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
//安装消息处理函数
Singleton::getInstance().installMessageHandler();
//输出测试,查看是否写入到文件(写入)
qDebug("qDebug:安装消息处理函数,写入到文件!");
qInfo("qInfo:安装消息处理函数,写入到文件!");
qWarning("qWarning:安装消息处理函数,写入到文件!");
qCritical("qCritical:安装消息处理函数,写入到文件!");
// qFatal("qFatal:安装消息处理函数,写入到文件!"); // 写入该行直接致命错误!,以下代码不执行!
//卸载消息处理函数
Singleton::getInstance().uninstallMessageHandler();
//输出测试,查看是否写入到文件(不写入)
qDebug() << "qDebug:卸载消息处理函数,不写入到文件!";
qInfo() << "qInfo:卸载消息处理函数,不写入到文件!";
//再次安装消息处理函数
Singleton::getInstance().installMessageHandler();
//输出测试,查看是否写入到文件(写入)
qDebug() << "qDebug:再次安装消息处理函数,写入到文件!";
qInfo() << "qInfo:再次安装消息处理函数,写入到文件!";
return app.exec();
}
kcLog.cpp
/******************************************************************************************
* loghanfler.cpp
* by kangchuang
* time:20200514
*
* 使用方法如下:
#include "kcLog.h"
#include
#include
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
//安装消息处理函数
Singleton::getInstance().installMessageHandler();
//输出测试,查看是否写入到文件(写入)
qDebug("qDebug:安装消息处理函数,写入到文件!");
qInfo("qInfo:安装消息处理函数,写入到文件!");
qWarning("qWarning:安装消息处理函数,写入到文件!");
qCritical("qCritical:安装消息处理函数,写入到文件!");
// qFatal("qFatal:安装消息处理函数,写入到文件!"); // 写入该行直接致命错误!,以下代码不执行!
//卸载消息处理函数
Singleton::getInstance().uninstallMessageHandler();
//输出测试,查看是否写入到文件(不写入)
qDebug() << "qDebug:卸载消息处理函数,不写入到文件!";
qInfo() << "qInfo:卸载消息处理函数,不写入到文件!";
//再次安装消息处理函数
Singleton::getInstance().installMessageHandler();
//输出测试,查看是否写入到文件(写入)
qDebug() << "qDebug:再次安装消息处理函数,写入到文件!";
qInfo() << "qInfo:再次安装消息处理函数,写入到文件!";
return app.exec();
}
******************************************************************************************/
#include "kcLog.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/************************************************************************************************************
* *
* kcLogPrivate *
* *
***********************************************************************************************************/
struct kcLogPrivate {
kcLogPrivate();
~kcLogPrivate();
// 打开日志文件 log.txt,如果日志文件不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 log.txt
void openAndBackupLogFile();
// 消息处理函数
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg);
// 如果日志所在目录不存在,则创建
void makeSureLogDirectory() const;
QDir logDir; // 日志文件夹
QTimer renameLogFileTimer; // 重命名日志文件使用的定时器
QTimer flushLogFileTimer; // 刷新输出到日志文件的定时器
QDate logFileCreatedDate; // 日志文件创建的时间
static QFile *logFile; // 日志文件
static QTextStream *logOut; // 输出日志的 QTextStream,使用静态对象就是为了减少函数调用的开销
static QMutex logMutex; // 同步使用的 mutex
};
// 初始化 static 变量
QMutex kcLogPrivate::logMutex;
QFile *kcLogPrivate::logFile = nullptr;
QTextStream *kcLogPrivate::logOut = nullptr;
kcLogPrivate::kcLogPrivate() {
logDir.setPath("log"); // TODO: 日志文件夹的路径,为 exe 所在目录下的 log 文件夹,可从配置文件读取
QString logPath = logDir.absoluteFilePath("log.txt"); // 日志的路径
// 日志文件创建的时间
// QFileInfo::created(): On most Unix systems, this function returns the time of the last status change.
// 所以不能运行时使用这个函数检查创建时间,因为会在运行时变化,于是在程序启动时保存下日志文件的最后修改时间,
// 在后面判断如果不是今天则用于重命名 log.txt
// 如果是 Qt 5.10 后,lastModified() 可以使用 birthTime() 代替
logFileCreatedDate = QFileInfo(logPath).lastModified().date();
// 打开日志文件,如果不是当天创建的,备份已有日志文件
openAndBackupLogFile();
// 十分钟检查一次日志文件创建时间
renameLogFileTimer.setInterval(1000 * 60 * 10); // TODO: 可从配置文件读取
// renameLogFileTimer.setInterval(1000); // 为了快速测试看到日期变化后是否新创建了对应的日志文件,所以 1 秒检查一次
renameLogFileTimer.start();
QObject::connect(&renameLogFileTimer, &QTimer::timeout, [this] {
QMutexLocker locker(&kcLogPrivate::logMutex);
openAndBackupLogFile();
});
// 定时刷新日志输出到文件,尽快的能在日志文件里看到最新的日志
flushLogFileTimer.setInterval(1000); // TODO: 可从配置文件读取
flushLogFileTimer.start();
QObject::connect(&flushLogFileTimer, &QTimer::timeout, [] {
// qDebug() << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); // 测试不停的写入内容到日志文件
QMutexLocker locker(&kcLogPrivate::logMutex);
if (nullptr != logOut) {
logOut->flush();
}
});
}
kcLogPrivate::~kcLogPrivate() {
if (nullptr != logFile) {
logFile->flush();
logFile->close();
delete logOut;
delete logFile;
// 因为他们是 static 变量
logOut = nullptr;
logFile = nullptr;
}
}
// 打开日志文件 log.txt,如果不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 log.txt
void kcLogPrivate::openAndBackupLogFile() {
// 总体逻辑:
// 1. 程序启动时 logFile 为 nullptr,初始化 logFile,有可能是同一天打开已经存在的 logFile,所以使用 Append 模式
// 2. logFileCreatedDate is nullptr, 说明日志文件在程序开始时不存在,所以记录下创建时间
// 3. 程序运行时检查如果 logFile 的创建日期和当前日期不相等,则使用它的创建日期重命名,然后再生成一个新的 log.txt 文件
makeSureLogDirectory(); // 如果日志所在目录不存在,则创建
QString logPath = logDir.absoluteFilePath("log.txt"); // 日志的路径
// [[1]] 程序启动时 logFile 为 nullptr
if (nullptr == logFile) {
logFile = new QFile(logPath);
logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) ? new QTextStream(logFile) : nullptr;
if (nullptr != logOut) {
logOut->setCodec("UTF-8");
}
// [[2]] 如果文件是第一次创建,则创建日期是无效的,把其设置为当前日期
if (logFileCreatedDate.isNull()) {
logFileCreatedDate = QDate::currentDate();
}
// TODO: 可以检查日志文件超过 30 个,删除 30 天前的日志文件
}
// [[3]] 程序运行时如果创建日期不是当前日期,则使用创建日期重命名,并生成一个新的 log.txt
if (logFileCreatedDate != QDate::currentDate()) {
logFile->flush();
logFile->close();
delete logOut;
delete logFile;
QString newLogPath = logDir.absoluteFilePath(logFileCreatedDate.toString("yyyy-MM-dd.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) : nullptr;
logFileCreatedDate = QDate::currentDate();
if (nullptr != logOut) {
logOut->setCodec("UTF-8");
}
}
}
// 如果日志所在目录不存在,则创建
void kcLogPrivate::makeSureLogDirectory() const {
if (!logDir.exists()) {
logDir.mkpath("."); // 可以递归的创建文件夹
}
}
// 消息处理函数
void kcLogPrivate::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
QMutexLocker locker(&kcLogPrivate::logMutex);
QString level;
switch (type) {
case QtDebugMsg:
level = "DEBUG";
break;
case QtInfoMsg:
level = "INFO ";
break;
case QtWarningMsg:
level = "WARN ";
break;
case QtCriticalMsg:
level = "ERROR";
break;
case QtFatalMsg:
level = "FATAL";
break;
default:
break;
}
// 输出到标准输出: Windows 下 std::cout 使用 GB2312,而 msg 使用 UTF-8,但是程序的 Local 也还是使用 UTF-8
#if defined(Q_OS_WIN)
QByteArray localMsg = QTextCodec::codecForName("GB2312")->fromUnicode(msg); //msg.toLocal8Bit();
#else
QByteArray localMsg = msg.toLocal8Bit();
#endif
std::cout << std::string(localMsg) << std::endl;
if (nullptr == kcLogPrivate::logOut) {
return;
}
// 输出到日志文件, 格式: 时间 - [Level] (文件名:行数, 函数): 消息
QString fileName = context.file;
int index = fileName.lastIndexOf(QDir::separator());
fileName = fileName.mid(index + 1);
(*kcLogPrivate::logOut) << QString("%1 - [%2] (%3:%4, %5): %6\n")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(level)
.arg(fileName)
.arg(context.line)
.arg(context.function)
.arg(msg);
}
/************************************************************************************************************
* *
* kcLog *
* *
***********************************************************************************************************/
kcLog::kcLog() : d(nullptr) {
}
kcLog::~kcLog() {
}
void kcLog::installMessageHandler() {
QMutexLocker locker(&kcLogPrivate::logMutex);
if (nullptr == d) {
d = new kcLogPrivate();
qInstallMessageHandler(kcLogPrivate::messageHandler); // 给 Qt 安装自定义消息处理函数
}
}
void kcLog::uninstallMessageHandler() {
QMutexLocker locker(&kcLogPrivate::logMutex);
qInstallMessageHandler(nullptr);
delete d;
d = nullptr;
}
kcLog.h
#ifndef kcLog_H
#define kcLog_H
#include "Singleton.h"
#define kcLogInstance Singleton::getInstance()
struct kcLogPrivate;
class kcLog {
SINGLETON(kcLog) // 使用单例模式
public:
void uninstallMessageHandler(); // 释放资源
void installMessageHandler(); // 给 Qt 安装消息处理函数
private:
kcLogPrivate *d;
};
#endif // kcLog_H
Singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H
//Singleton.h下载地址:
//https://download.csdn.net/download/sirkang/12418621
#endif // SINGLETON_H
main() 函数里的 qDebug() 输出都是在 UI 线程,kcLog是否多线程安全?怎么测试?
日志的相关配置数据例如输出目录等都是写死在程序里的,如果写到配置文件里是不是更灵活?
日志的格式也是写死在程序里的,如果能做到通过配置修改日志格式那就更强大了,就像 log4cxx 一样
测试如何快速的看到不同日期生成的日志文件不同?
删除超过 30 天的日志
单个日志文件例如大于 100M 后重新创建一个新的日志文件
工程下载地址:
https://download.csdn.net/download/sirkang/12418621