TikTok 美國員工抱怨工作壓力大:需週末加班,跨時區工作
尊重原創版權: https://www.gewuweb.com/hot/8479.html
Qt開源作品39-日誌輸出增強版V2022
## 一、前言
之前已經開源過基礎版本,近期根據客戶需求和自己的專案需求,提煉出通用需求部分,對整個日誌重定向輸出類重新規劃和重寫程式碼。
用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的,而且提供友好的介面。
## 二、主要功能
1. 支援動態啟動和停止。
2. 支援日誌儲存的目錄。
3. 支援網路發出列印日誌。
4. 支援輸出日誌上下文資訊比如所在程式碼檔案、行號、函式名等。
5. 支援設定日誌檔案大小限制,超過則自動分檔案,預設128kb。
6. 支援按照日誌行數自動分檔案,和日誌大小條件互斥。
7. 可選按照日期時間區分檔名儲存日誌。
8. 日誌檔案命名規則優先順序:行數》大小》日期。
9. 自動加鎖支援多執行緒。
10. 可以分別控制哪些型別的日誌需要重定向輸出。
11. 支援Qt4+Qt5+Qt6,開箱即用。
12. 使用方式最簡單,呼叫函式start()啟動服務,stop()停止服務。
## 三、效果圖
## 四、開源主頁
以上作品完整原始碼下載都在開源主頁,會持續不斷更新作品數量和質量,歡迎各位關注。
1.
國內站點:https://gitee.com/feiyangqingyun/QWidgetDemo
2.
國際站點:https://github.com/feiyangqingyun/QWidgetDemo
3.
4.
知乎主頁: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();
}
}