原文链接:https://blog.csdn.net/gongjianbo1992/article/details/108030391
相对于第三方的日志库,在 Qt 中使用 QDebug 打印更便捷,有时候也需要对 QDebug 输出进行重定向,如写入文件等。
在 Qt4 中使用 qInstallMsgHandler 函数设置重定向的函数指针:
typedef void (*QtMsgHandler)(QtMsgType, const char *);
Q_CORE_EXPORT QT_DEPRECATED QtMsgHandler qInstallMsgHandler(QtMsgHandler);
在 Qt5 中应该使用 qInstallMessageHandler 来注册函数指针:
typedef void (*QtMessageHandler)(QtMsgType, const QMessageLogContext &, const QString &);
Q_CORE_EXPORT QtMessageHandler qInstallMessageHandler(QtMessageHandler);
返回的函数指针我们可以保存起来,需要输出到控制台时进行调用。
默认 Release 模式 QMessageLogContext 不含上下文信息,可以用宏定义 QT_MESSAGELOGCONTEXT 开启,pro 文件加上:
DEFINES += QT_MESSAGELOGCONTEXT
一个最简单的示例如下,重定向到文件:
#include
#include
#include
#include
#include
#include
//重定向qdebug输出到文件
void myMessageHandle(QtMsgType , const QMessageLogContext& , const QString& msg)
{
static QMutex mut; //多线程打印时需要加锁
QMutexLocker locker(&mut);
QFile file("log.txt");
if(file.open(QIODevice::WriteOnly|QIODevice::Append))
{
QTextStream stream(&file);
stream<<msg<<endl;
file.close();
}
}
int main()
{
//设置重定向操作的函数
qInstallMessageHandler(myMessageHandle);
qDebug()<<"Test debug111";
qDebug()<<"Test debug222";
return 0;
}
需求很简单,同时输出到界面中的编辑框、文件、控制台。
代码链接:https://github.com/gongjianbo/SimpleQtLogger
运行效果(图片为旧版截图):
输出到控制台,我是保存了调用 qInstallMessageHandler 时返回的函数指针,然后调用进行默认的输出。
输出到文件,因为函数调用发生在 qDebug() 调用的线程,所以需要加锁。
输出到界面,我使用了信号槽的方式,将文本发送给 connect 的槽。
此外,增加了按日期和文件大小来新建文件的逻辑。
当然,还可以添加一些细节,如日志等级的控制等。
下面是部分源码:
#include
#include "LogManager.h"
#include "mainwindow.h"
int main(int argc, char *argv[])
{
LogManager::getInstance()->initManager();//初始化
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
#pragma once
#include
#include
#include
#include
/**
* @brief 简易的日志管理类,作为单例
* @author 龚建波 - https://github.com/gongjianbo
* @date 2020-08-13
* @details
* 1.初始化时调用 initManager 重定向 QDebug 输出
* 析构时自动调用 freeManager,也可以手动调用 freeManager
* 2.根据时间戳每天重新生成一个文件,超过文件大小也会重新生成
*/
class LogManager : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(LogManager)
LogManager();
public:
~LogManager();
// 获取单例实例
static LogManager *getInstance();
// 获取带 html 样式标签的富文本
Q_INVOKABLE static QString richText(int msgType, const QString &log);
// 初始化,如重定向等
void initManager(const QString &dir = QString());
// 释放
void freeManager();
// 文件最大大小,超过则新建文件,单位字节
qint64 getFileSizeLimit() const;
void setFileSizeLimit(qint64 limit);
private:
// 重定向到此接口
static void outputHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg);
// 获取重定向的打印信息,在静态函数种回调该接口
void outputLog(QtMsgType type, const QMessageLogContext &context, const QString &msg);
// 计算下一次生成文件的时间
qint64 calcNextTime() const;
// 每次写入时判断是否打开,是否需要新建文件
void prepareFile();
signals:
// 可以关联信号接收日志信息,如显示到 ui 中
// 注意,如果槽函数为 lambda 或者其他没有接收者的情况,需要保证槽函数中的变量有效性
// 因为 static 变量的生命周期更长,可能槽函数所在模块已经释放资源,最好 connect 加上接收者
void newLog(int msgType, const QString &log);
private:
// 保留默认 handle,用于输出到控制台
QtMessageHandler defaultOutput = nullptr;
// 输出到文件
QFile logFile;
// 输出路径
QString logDir;
// 多线程操作时需要加锁
mutable QMutex logMutex;
// 下一次生成文件的时间戳,单位毫秒
qint64 fileNextTime{ 0 };
// 文件最大大小,超过则新建文件,单位字节
qint64 fileSizeLimit{ 1024 * 1024 * 32 };
};
#include "LogManager.h"
#include
#include
#include
#include
#include
LogManager::LogManager()
{
}
LogManager::~LogManager()
{
freeManager();
}
LogManager *LogManager::getInstance()
{
// 单例,初次调用时实例化
static LogManager instance;
return &instance;
}
QString LogManager::richText(int msgType, const QString &log)
{
QString log_text;
QTextStream stream(&log_text);
switch (msgType) {
case QtDebugMsg: stream << ""; break;
case QtInfoMsg: stream << ""; break;
case QtWarningMsg: stream << ""; break;
case QtCriticalMsg: stream << ""; break;
case QtFatalMsg: stream << ""; break;
default: stream << ""; break;
}
stream << log << "";
return log_text;
}
void LogManager::initManager(const QString &dir)
{
QMutexLocker locker(&logMutex);
// 保存路径
logDir = dir;
if (logDir.isEmpty())
{
// 用到了 QCoreApplication::applicationDirPath(),需要先实例化一个app
if (qApp) {
logDir = qApp->applicationDirPath() + "/log";
} else {
int argc = 0;
QCoreApplication app(argc,nullptr);
logDir = app.applicationDirPath() + "/log";
}
}
// 计算下次创建文件的时间点
fileNextTime = calcNextTime();
// 重定向qdebug到自定义函数
defaultOutput = qInstallMessageHandler(LogManager::outputHandler);
}
void LogManager::freeManager()
{
QMutexLocker locker(&logMutex);
logFile.close();
if (defaultOutput) {
qInstallMessageHandler(defaultOutput);
defaultOutput = nullptr;
}
}
qint64 LogManager::getFileSizeLimit() const
{
return fileSizeLimit;
}
void LogManager::setFileSizeLimit(qint64 limit)
{
fileSizeLimit = limit;
}
void LogManager::outputHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
// 转发给单例的成员函数
LogManager::getInstance()->outputLog(type, context, msg);
}
void LogManager::outputLog(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
// widget 中的 log,context.category = default
// qml 中的 log,context.category = qml,此时默认的 output 会增加一个 "qml:" 前缀输出
// fprintf(stderr, "print: type = %d, category = %s \n", type, context.category);
// 如果要写文件需要加锁,因为函数调用在 debug 调用线程
QMutexLocker locker(&logMutex);
QString out_text;
QTextStream stream(&out_text);
// 时间
stream << QDateTime::currentDateTime().toString("[yyyy-MM-dd hh:mm:ss]");
// 日志类型
switch (type) {
case QtDebugMsg: stream << "[Debug]"; break;
case QtInfoMsg: stream << "[Info]"; break;
case QtWarningMsg: stream << "[Warning]"; break;
case QtCriticalMsg: stream << "[Critical]"; break;
case QtFatalMsg: stream << "[Fatal]"; break;
default: stream << "[Unknown]"; break;
}
// 线程 id
stream << "[" << QThread::currentThreadId() << "]";
// 输出位置
stream << "[" << context.file << ":" << context.line << "]";
// 日志信息
stream << msg;
// 判断是否需要打开或者新建文件
prepareFile();
if (logFile.isOpen()) {
// 写入文件
stream.setDevice(&logFile);
stream << out_text << Qt::endl;
}
// 发送信号给需要的对象,如 ui 上显示日志
emit newLog(type, out_text);
// 默认的输出,控制台
// 区分日志类型给文本加颜色
// 常见格式为:\e[显示方式;背景颜色;前景文字颜色m << 输出字符串 << \e[0m
// 其中 \e=\033
// -----------------
// 背景色 字体色
// 40: 30: 黑
// 41: 31: 红
// 42: 32: 绿
// 43: 33: 黄
// 44: 34: 蓝
// 45: 35: 紫
// 46: 36: 深绿
// 47: 37: 白
// -----------------
QString cmd_text;
stream.setString(&cmd_text);
switch (type) {
case QtDebugMsg: // debug 绿色
stream << "\033[32m"; break;
case QtInfoMsg: // info 蓝色
stream << "\033[34m"; break;
case QtWarningMsg: // warning 黄色
stream << "\033[33m"; break;
case QtCriticalMsg: // critical 红字
stream << "\033[31m"; break;
case QtFatalMsg: // fatal 黑底红字
// qFatal 表示致命错误,默认处理会报异常的
stream << "\033[0;31;40m"; break;
default: // defualt 默认颜色
stream << "\033[0m"; break;
}
stream << out_text << "\033[0m";
defaultOutput(type, context, cmd_text);
}
qint64 LogManager::calcNextTime() const
{
// 可以参考 spdlog 的 daily_file_sink 优化,这里先用 Qt 接口进行实现
return QDate::currentDate().addDays(1).startOfDay().toMSecsSinceEpoch();
}
void LogManager::prepareFile()
{
// 写入文件
// 先计算好下一次生成文件的时间点,然后和当前进行比较,这里没有考虑调节系统日期的情况
if (fileNextTime <= QDateTime::currentDateTime().toMSecsSinceEpoch()){
logFile.close();
// 计算下次创建文件的时间点
fileNextTime = calcNextTime();
}
// 文件超过了大小
if (logFile.isOpen() && logFile.size() >= fileSizeLimit) {
logFile.close();
}
// 生成文件名,打开文件
if (!logFile.isOpen()) {
// 创建文件前创建目录,QFile 不会自动创建不存在的目录
QDir dir(logDir);
if (!dir.exists()) {
dir.mkpath(logDir);
}
// 文件日期
QString file_day = QDate::currentDate().toString("yyyyMMdd");
QString file_path = QString("%1/log_%2.txt").arg(logDir).arg(file_day);
logFile.setFileName(file_path);
if (logFile.exists() && logFile.size() >= fileSizeLimit) {
QString file_time = QTime::currentTime().toString("hhmmss");
file_path = QString("%1/log_%2_%3.txt").arg(logDir).arg(file_day).arg(file_time);
logFile.setFileName(file_path);
}
// 打开新的文件
// Append 追加模式,避免同一文件被清除
if (!logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
emit newLog(QtWarningMsg, "Open log file error:" + logFile.errorString() + logFile.fileName());
}
}
}
原文链接:https://blog.csdn.net/u011218356/article/details/103344231
简介
这几天在交接工作,把之前手头的一个项目交接一下,想着增加一个日志模块,去查了一下,Qt自带的日志模块 qInstallMessageHandler 。
qInstallMessageHandler–说明
qInstallMessageHandler 位于 - Global Qt Declarations下边,属于全局函数。
QtMessageHandler qInstallMessageHandler(QtMessageHandler handler)
Installs a Qt message handler which has been defined previously. Returns a pointer to the previous message handler.
The message handler is a function that prints out debug messages, warnings, critical and fatal error messages. The Qt library (debug mode) contains hundreds of warning messages that are printed when internal errors (usually invalid function arguments) occur. Qt built in release mode also contains such warnings unless QT_NO_WARNING_OUTPUT and/or QT_NO_DEBUG_OUTPUT have been set during compilation. If you implement your own message handler, you get total control of these messages.
The default message handler prints the message to the standard output under X11 or to the debugger under Windows. If it is a fatal message, the application aborts immediately.
Only one message handler can be defined, since this is usually done on an application-wide basis to control debug output.
To restore the message handler, call qInstallMessageHandler(0).
官方解释主要说了 qInstallMesageHandler 的作用,其实主要是是对 打印消息的控制,能控制打印的输出,调试,信息,警告,严重,致命错误等五个等级。
#include
#include
#include
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
QByteArray localMsg = msg.toLocal8Bit();
switch (type) {
case QtDebugMsg:
fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
break;
case QtInfoMsg:
fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
break;
case QtWarningMsg:
fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
break;
case QtCriticalMsg:
fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
break;
case QtFatalMsg:
fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
abort();
}
}
int main(int argc, char **argv)
{
qInstallMessageHandler(myMessageOutput);
QApplication app(argc, argv);
...
return app.exec();
}
源码
日志模块思路如下:
1.读取日志配置文件,设置文件输出等级。可以用做,在正式项目中调试与日常关键信息打印。
2.Log消息输出方法–输出文本。
3.消息模块注册(个人理解)
源码
枚举变量–设置日志等级 0~4 5个等级
enum{
Fatal = 0,
Critical,
Warning,
Info,
Debug
}LogLeaver;
int LogType = 4; //日志等级初始化设置
//初始化读取配置文件
void ReadLogInit()
{
QFile file ("./log_conf.ini");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)){//判断文件是否可执行
return;
}
while (!file.atEnd()) {
QByteArray strBuf = file.readLine();
if(strBuf == "[LOG_CONFIG]\n")
{
strBuf = file.readLine();
LogType = strBuf.mid(strBuf.size()-1,1).toInt();
}
}
}
//配置文件格式
[LOG_CONFIG]
LogLeaver=4
void message_output(QtMsgType type,const QMessageLogContext &context,const QString &msg)
{
//加锁:避免对文件的同时读写
static QMutex mutex;
mutex.lock();
//读写消息
QByteArray localMsg = msg.toLocal8Bit();
//输出的字符串
QString strOutStream = "";
//case 生成要求格式日志文件,加日志等级过滤
switch (type) {
case QtDebugMsg:
if(LogType == Debug)
{
strOutStream = QString("%1 %2 %3 %4 [Debug] %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg));
}
break;
case QtInfoMsg:
if(LogType >= Info)
{
strOutStream = QString("%1 %2 %3 %4 [Info]: %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg));
}
break;
case QtWarningMsg:
if(LogType >= Warning)
{
strOutStream = QString("%1 %2 %3 %4 [Warning]: %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg));
}
break;
case QtCriticalMsg:
if(LogType >= Critical)
{
strOutStream = QString("%1 %2 %3 %4 [Critical]: %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg));
}
break;
case QtFatalMsg:
if(LogType >= Fatal)
{
strOutStream = QString("%1 %2 %3 %4 [Fatal]: %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg));
}
abort();
}
//每天生成一个新的log日志文件,文件名 yyyyMMdd.txt
QString strFileName = QString("%1.txt").arg(QDateTime::currentDateTime().date().toString("yyyyMMdd"));
QFile logfile(strFileName);
logfile.open(QIODevice::WriteOnly | QIODevice::Append);
if(strOutStream != "")
{
QTextStream logStream(&logfile);
logStream<<strOutStream<<"\r\n";
}
//清楚缓存文件,解锁
logfile.flush();
logfile.close();
mutex.unlock();
}
int main(int argc, char *argv[])
{
ReadLogInit();//读取日志等级
qInstallMessageHandler(message_output);//安装消息处理函数,依靠回调函数,重定向,全局处理
QApplication a(argc, argv);
qInfo()<<"\r\n\r\n\r\n**PCCamera start**";
//to doing......
}
原文链接:https://blog.csdn.net/yang1fei2/article/details/125210633
在开发QT程序的时候,很多开发者也就仅仅用QT的日志模块qDebug一下调试信息,在真正的日志记录上还是采用一些别的日志库。其实QT的日志模块还是很强大的,可以满足日常的基本需求。这里就详细介绍一下QT日志模块的个性化使用方法。
默认情况下,日志格式是只输出对应的日志内容没有额外信息的。我们可以通过修改环境变量QT_MESSAGE_PATTERN或者调用方法 qSetMessagePattern来修改日志的输出格式。日志格式中常用的占位符号如下所示:
%{appname} 应用程序的名称(QCoreApplication::applicationName())
%{category} 日志所处的领域
%{file} 打印该日志的文件路径
%{function} 打印日志的函数
%{line} 打印日志在文件中的行数
%{message} 日志的内容
%{pid} 打印日志的程序的PID(QCoreApplication::applicationPid())
%{threadid} 打印日志的线程ID
%{qthreadptr} 打印日志的线程指针
%{type} 日志级别("debug", "warning", "critical" or "fatal")
%{time process}日志发生时程序启动了多久
%{time boot} 日志发生时系统启动了多久
%{time [format]}以固定时间格式输出日志打印的时间,默认为QISODate格式
格式化日志的调用方法如下:
int main(int argc,char*argv[])
{
QCoreApplication app(argc, argv);
qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss}--[%{type}]--%{function}:%{message}");
qDebug() << "exception occured";
qInfo() << "call other function";
return app.exec();
}
输出的日志内容格式如下:
2022-06-09 10:09:54--[debug]--main:exception occured
2022-06-09 10:09:55--[info]--main:call other function
我们还可以使用条件变量
%{if-debug}, %{if-info} %{if-warning}, %{if-critical} or %{if-fatal}
给不同级别的日志指定不同的格式,使用方法如下:
int main(int argc,char*argv[])
{
QCoreApplication app(argc, argv);
//针对Warning信息和Fatal信息输出了额外的内容
qputenv("QT_MESSAGE_PATTERN", QByteArray("%{time yyyy-MM-dd hh:mm:ss} [%{type}]%{if-warning}[%{function}]%{endif}%{if-fatal}[%{function}--%{line}]%{endif}:%{message}"));
qDebug() << "debuginfo occured";
qInfo() << "call other function";
qWarning() << "doesn't work";
qFatal("fatal error");
return app.exec();
}
输出的日内容如下:
2022-06-09 10:48:32 [debug]:debuginfo occured
2022-06-09 10:48:32 [info]:call other function
2022-06-09 10:48:32 [warning][main]:doesn't work
2022-06-09 10:48:32 [fatal][main--116]:fatal error
我们可以通过修改QT_MESSAGE_PATTERN环境变量动态的修改日志的输出格式。如果同时调用qSetMessagePattern和QT_MESSAGE_PATTERN环境变量来修改日志的输出格式,那么QT_MESSAGE_PATTERN的优先级要高于qSetMessagePattern。
QT默认的日志内容是输出到终端的,不会输出到文件里面,如果需要将日志内容输出到文件中我们需要通过qInstallMessageHandler设置日志信息处理函数。使用方法如下:
//日志消息的处理函数
void logmessageHander(QtMsgType type, const QMessageLogContext& context, const QString& message)
{
//获取格式化的日志信息
QString typeStr = qFormatLogMessage(type,context,message);
//可以根据日志的级别进行过滤
QString levelText;
switch (type) {
case QtDebugMsg:
levelText = "Debug";
break;
case QtInfoMsg:
levelText = "Info";
break;
case QtWarningMsg:
levelText = "Warning";
break;
case QtCriticalMsg:
levelText = "Critical";
break;
case QtFatalMsg:
levelText = "Fatal";
break;
}
QFile file("myapp.log");
file.open(QIODevice::WriteOnly | QIODevice::Append);
QTextStream textStream(&file);
textStream << typeStr << endl;
}
int main(int argc,char*argv[])
{
QCoreApplication app(argc, argv);
qSetMessagePattern("%{time yyyy-MM-dd hh:mm:ss} [%{type}]%{if-warning}[%{function}]%{endif}%{if-fatal}[%{function}--%{line}]%{endif}:%{message}");
qInstallMessageHandler(logmessageHander);
qDebug() << "debuginfo occured";
qInfo() << "call other function";
qWarning() << "doesn't work";
qFatal("fatal error");
return app.exec();
}
如果需要关闭日志输出,取消之前注册的日志处理函数,我们可以调用
//取消注册的日志处理函数
qInstallMessageHandler(0);
在调试一些复杂对象的时候,我们需要输出对象的成员信息到日志当中。但是默认情况下qt的日志库是不支持输出自定义对象的。这时候我们可以通过重写操作符实现对自定义象的日志输出。使用方法如下:
//消费者信息类
struct Customer {
QString name;
int age;
QString clientid;
QString addr;
};
QDebug operator<<(QDebug debug, const Customer& customer)
{
//保存QDebug的状态
QDebugStateSaver saver(debug);
debug.nospace() << "("
<< "name: " << customer.name << ","
<< "age: " << customer.age << ","
<< "clientid: " << customer.clientid << ","
<< "addr: " << customer.addr << ","
<< ")";
return debug;
}
int main(int argc,char*argv[])
{
QCoreApplication app(argc, argv);
Customer customer = { "Liming", 22, "677888", "北京市海淀区"};
qDebug() << "Customer info" << customer;
return app.exec();
}
QDebugStateSaver类可以保存QDebug的配置,并在销毁的时候自动恢复。使用它我们可以避免在操作符重载的时候破坏QDebug中的配置。
https://blog.csdn.net/qq_45662588/article/details/116259372
原文链接:https://blog.csdn.net/lm409/article/details/74908484
项目中需求一日志模块,主要实现两大功能:1.自动打印信息至日志文件;2.软件意外退出时保留信息以便跟踪问题。
本文结合了 Qt 自定义日志工具 和 让程序在崩溃时体面的退出之CallStack 提供的方法,补充实现了文章中未具体给出的管理日志文件大小和数量的功能。
环境:vs2012+Qt5.2(注:Qt5.5之后引入qInfo(),影响不大)
基本原理是使用 qInstallMessageHandler()接管qDebug(), qWarning()等调试信息,然后将信息流存储至本地日志文件,管理日志文件。
代码在原作者基础上做了部分调整:
1.更改日志存储名称格式,用QDateTime取代QDate,以避免当日记录多条日志时的覆盖问题;
2.增加日志文件个数的判断;
3.增加日志文件大小的检测;
4.屏蔽根据修改日期保存日志机制,以免在不同日期开启软件后冲掉以前的有用log,仅凭文件大小另存log文件,然后控制文件数量。
LogHandler.cpp
#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信息即可。
LogHandler.cpp
//程式异常捕获
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<CallStackInfo> arrCallStackInfo = GetCallStack(pException->ContextRecord);
// 输出CallStack
qCritical() << "CallStack: " << endl;
for (vector<CallStackInfo>::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
让程序在崩溃时体面的退出之CallStack
原文链接:https://blog.csdn.net/feiyangqingyun/article/details/121314920
之前已经开源过基础版本,近期根据客户需求和自己的项目需求,提炼出通用需求部分,对整个日志重定向输出类重新规划和重写代码。
用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()停止服务。
以上作品完整源码下载都在开源主页,会持续不断更新作品数量和质量,欢迎各位关注。
本开源项目已经成功升级到V2.0版本,分门别类,图文并茂,保你爽到爆。
Qt开源武林秘籍开发经验,看完学完,20K起薪,没有找我!
国内站点:https://gitee.com/feiyangqingyun/QWidgetDemo
国际站点:https://github.com/feiyangqingyun/QWidgetDemo
开源秘籍:https://gitee.com/feiyangqingyun/qtkaifajingyan
个人主页:https://qtchina.blog.csdn.net/
视频主页:https://space.bilibili.com/687803542
#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();
}
}