一、前言
之前已经开源过基础版本,近期根据客户需求和自己的项目需求,提炼出通用需求部分,对整个日志重定向输出类重新规划和重写代码。
用Qt这个一站式超大型GUI超市做开发已经十二年了,陆陆续续开发过至少几十个程序,除了一些算不算项目的小工具外,大部分的程序都需要有个日志的输出功能,希望可以将程序的运行状态存储到文本文件或者数据库或者做其他处理等,Qt对这个日志输出也做了很好的封装,在Qt4是qInstallMsgHandler,Qt5及Qt6里边是qInstallMessageHandler,有了这个神器,只要在你的项目中所有qDebug qInfo等输出的日志信息,都会重定向接收到。
网上大部分人写的demo都是接收到输出打印日志存储到文本文件,其实这就带给很多人误解,容易产生以为日志只能输出到文本文件,其实安装了日志钩子以后,拿到了所有调试打印信息,你完全可以用来存储到数据库及输出html有颜色区分格式的文件,或者网络转发输出(尤其适用于嵌入式linux*面程序,现场不方便外接调试打印的设备)。
做过的这么多项目中,Qt4、Qt5、Qt6的都有,我一般保留四个版本,4.8.7,为了兼容Qt4, 5.7.0,最后的支持XP的版本, 最新的长期支持版本5.15.2 最高的新版本6.2.1。毫无疑问,我要封装的这个日志类,也要同时支持Qt4、Qt5、Qt6的,而且提供友好的接口。
二、主要功能
- 支持动态启动和停止。
- 支持日志存储的目录。
- 支持网络发出打印日志。
- 支持输出日志上下文信息比如所在代码文件、行号、函数名等。
- 支持设置日志文件大小限制,超过则自动分文件,默认128kb。
- 支持按照日志行数自动分文件,和日志大小条件互斥。
- 可选按照日期时间区分文件名存储日志。
- 日志文件命名规则优先级:行数》大小》日期。
- 自动加锁支持多线程。
- 可以分别控制哪些类型的日志需要重定向输出。
- 支持Qt4+Qt5+Qt6,开箱即用。
- 使用方式最简单,调用函数start()启动服务,stop()停止服务。
三、效果图
四、开源主页
以上作品完整源码下载都在开源主页,会持续不断更新作品数量和质量,欢迎各位关注。
- 国内站点:https://gitee.com/feiyangqingyun/QWidgetDemo
- 国际站点:https://github.com/feiyangqingyun/QWidgetDemo
- 个人主页:https://blog.csdn.net/feiyangqingyun
- 知乎主页:https://www.zhihu.com/people/feiyangqingyun/
五、核心代码
#pragma execution_character_set("utf-8")
#include "savelog.h"
#include "qmutex.h"
#include "qdir.h"
#include "qfile.h"
#include "qtcpsocket.h"
#include "qtcpserver.h"
#include "qdatetime.h"
#include "qapplication.h"
#include "qtimer.h"
#include "qstringlist.h"
#define QDATE qPrintable(QDate::currentDate().toString("yyyy-MM-dd"))
#define QDATETIMS qPrintable(QDateTime::currentDateTime().toString("yyyy-MM-dd-HH-mm-ss"))
//日志重定向
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
void Log(QtMsgType type, const QMessageLogContext &context, const QString &msg)
#else
void Log(QtMsgType type, const char *msg)
#endif
{
//加锁,防止多线程中qdebug太频繁导致崩溃
static QMutex mutex;
QMutexLocker locker(&mutex);
QString content;
//这里可以根据不同的类型加上不同的头部用于区分
int msgType = SaveLog::Instance()->getMsgType();
switch (type) {
case QtDebugMsg:
if ((msgType & MsgType_Debug) == MsgType_Debug) {
content = QString("Debug %1").arg(msg);
}
break;
#if (QT_VERSION >= QT_VERSION_CHECK(5,5,0))
case QtInfoMsg:
if ((msgType & MsgType_Info) == MsgType_Info) {
content = QString("Infox %1").arg(msg);
}
break;
#endif
case QtWarningMsg:
if ((msgType & MsgType_Warning) == MsgType_Warning) {
content = QString("Warnx %1").arg(msg);
}
break;
case QtCriticalMsg:
if ((msgType & MsgType_Critical) == MsgType_Critical) {
content = QString("Error %1").arg(msg);
}
break;
case QtFatalMsg:
if ((msgType & MsgType_Fatal) == MsgType_Fatal) {
content = QString("Fatal %1").arg(msg);
}
break;
}
//没有内容则返回
if (content.isEmpty()) {
return;
}
//加上打印代码所在代码文件、行号、函数名
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
if (SaveLog::Instance()->getUseContext()) {
int line = context.line;
QString file = context.file;
QString function = context.function;
if (line > 0) {
content = QString("行号: %1 文件: %2 函数: %3\n%4").arg(line).arg(file).arg(function).arg(content);
}
}
#endif
//还可以将数据转成html内容分颜色区分
//将内容传给函数进行处理
SaveLog::Instance()->save(content);
}
QScopedPointer<SaveLog> SaveLog::self;
SaveLog *SaveLog::Instance()
{
if (self.isNull()) {
static QMutex mutex;
QMutexLocker locker(&mutex);
if (self.isNull()) {
self.reset(new SaveLog);
}
}
return self.data();
}
SaveLog::SaveLog(QObject *parent) : QObject(parent)
{
//必须用信号槽形式,不然提示 QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread
//估计日志钩子可能单独开了线程
connect(this, SIGNAL(send(QString)), SendLog::Instance(), SLOT(send(QString)));
isRun = false;
maxRow = currentRow = 0;
maxSize = 128;
toNet = false;
useContext = true;
//全局的文件对象,在需要的时候打开而不是每次添加日志都打开
file = new QFile(this);
//默认取应用程序根目录
path = qApp->applicationDirPath();
//默认取应用程序可执行文件名称
QString str = qApp->applicationFilePath();
QStringList list = str.split("/");
name = list.at(list.count() - 1).split(".").at(0);
fileName = "";
//默认所有类型都输出
msgType = MsgType(MsgType_Debug | MsgType_Info | MsgType_Warning | MsgType_Critical | MsgType_Fatal);
}
SaveLog::~SaveLog()
{
file->close();
}
void SaveLog::openFile(const QString &fileName)
{
//当文件名改变时才新建和打开文件而不是每次都打开文件(效率极低)或者一开始打开文件
if (this->fileName != fileName) {
this->fileName = fileName;
//先关闭之前的
if (file->isOpen()) {
file->close();
}
//重新设置新的日志文件
file->setFileName(fileName);
//以 Append 追加的形式打开
file->open(QIODevice::WriteOnly | QIODevice::Append | QFile::Text);
}
}
bool SaveLog::getUseContext()
{
return this->useContext;
}
MsgType SaveLog::getMsgType()
{
return this->msgType;
}
//安装日志钩子,输出调试信息到文件,便于调试
void SaveLog::start()
{
if (isRun) {
return;
}
isRun = true;
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
qInstallMessageHandler(Log);
#else
qInstallMsgHandler(Log);
#endif
}
//卸载日志钩子
void SaveLog::stop()
{
if (!isRun) {
return;
}
isRun = false;
this->clear();
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
qInstallMessageHandler(0);
#else
qInstallMsgHandler(0);
#endif
}
void SaveLog::clear()
{
currentRow = 0;
fileName.clear();
if (file->isOpen()) {
file->close();
}
}
void SaveLog::save(const QString &content)
{
//如果重定向输出到网络则通过网络发出去,否则输出到日志文件
if (toNet) {
emit send(content);
} else {
//目录不存在则先新建目录
QDir dir(path);
if (!dir.exists()) {
dir.mkdir(path);
}
//日志存储规则有多种策略 优先级 行数>大小>日期
//1: 设置了最大行数限制则按照行数限制来
//2: 设置了大小则按照大小来控制日志文件
//3: 都没有设置都存储到日期命名的文件,只有当日期变化了才会切换到新的日志文件
bool needOpen = false;
if (maxRow > 0) {
currentRow++;
if (fileName.isEmpty()) {
needOpen = true;
} else if (currentRow >= maxRow) {
needOpen = true;
}
} else if (maxSize > 0) {
//1MB=1024*1024 经过大量测试 QFile().size() 方法速度非常快
//首次需要重新打开文件以及超过大小需要重新打开文件
if (fileName.isEmpty()) {
needOpen = true;
} else if (file->size() > (maxSize * 1024)) {
needOpen = true;
}
} else {
//日期改变了才会触发
QString fileName = QString("%1/%2_log_%3.txt").arg(path).arg(name).arg(QDATE);
openFile(fileName);
}
if ((maxRow > 0 || maxSize > 0) && needOpen) {
currentRow = 0;
QString fileName = QString("%1/%2_log_%3.txt").arg(path).arg(name).arg(QDATETIMS);
openFile(fileName);
}
//用文本流的输出速度更快
QTextStream stream(file);
stream << content << "\n";
}
}
void SaveLog::setMaxRow(int maxRow)
{
//这里可以限定最大最小值
if (maxRow >= 0) {
this->maxRow = maxRow;
this->clear();
}
}
void SaveLog::setMaxSize(int maxSize)
{
//这里可以限定最大最小值
if (maxSize >= 0) {
this->maxSize = maxSize;
this->clear();
}
}
void SaveLog::setListenPort(int listenPort)
{
SendLog::Instance()->setListenPort(listenPort);
}
void SaveLog::setToNet(bool toNet)
{
this->toNet = toNet;
if (toNet) {
SendLog::Instance()->start();
} else {
SendLog::Instance()->stop();
}
}
void SaveLog::setUseContext(bool useContext)
{
this->useContext = useContext;
}
void SaveLog::setPath(const QString &path)
{
this->path = path;
}
void SaveLog::setName(const QString &name)
{
this->name = name;
}
void SaveLog::setMsgType(const MsgType &msgType)
{
this->msgType = msgType;
}
//网络发送日志数据类
QScopedPointer<SendLog> SendLog::self;
SendLog *SendLog::Instance()
{
if (self.isNull()) {
static QMutex mutex;
QMutexLocker locker(&mutex);
if (self.isNull()) {
self.reset(new SendLog);
}
}
return self.data();
}
SendLog::SendLog(QObject *parent) : QObject(parent)
{
listenPort = 6000;
socket = NULL;
//实例化网络通信服务器对象
server = new QTcpServer(this);
connect(server, SIGNAL(newConnection()), this, SLOT(newConnection()));
}
SendLog::~SendLog()
{
if (socket != NULL) {
socket->disconnectFromHost();
}
server->close();
}
void SendLog::newConnection()
{
//限定就一个连接
while (server->hasPendingConnections()) {
socket = server->nextPendingConnection();
}
}
void SendLog::setListenPort(int listenPort)
{
this->listenPort = listenPort;
}
void SendLog::start()
{
//启动端口监听
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
server->listen(QHostAddress::AnyIPv4, listenPort);
#else
server->listen(QHostAddress::Any, listenPort);
#endif
}
void SendLog::stop()
{
if (socket != NULL) {
socket->disconnectFromHost();
socket = NULL;
}
server->close();
}
void SendLog::send(const QString &content)
{
if (socket != NULL && socket->isOpen()) {
socket->write(content.toUtf8());
//socket->flush();
}
}