1. 程式人生 > >Qt編寫除錯日誌輸出類帶網路轉發(開源)

Qt編寫除錯日誌輸出類帶網路轉發(開源)

開發十年,就只剩下這套架構體系了! >>>   

用qt開發商業程式已經九年了,陸陸續續開發過至少幾十個程式,除了一些算不算專案的小工具外,大部分的程式都需要有個日誌的輸出功能,希望可以將程式的執行狀態儲存到文字檔案或者資料庫或者做其他處理等,qt對這個日誌輸出也做了很好的封裝,在Qt4是qInstallMsgHandler,Qt5裡邊是qInstallMessageHandler,有了這個神器,只要在你的專案中所有qdebug qinfo等輸出的日誌資訊,都會重定向接收到,網上大部分人寫的demo都是接收到輸出列印日誌儲存到文字檔案,其實這就帶給很多人誤解,容易產生以為日誌只能輸出到文字檔案,其實安裝了日誌鉤子以後,拿到了所有除錯列印資訊,你完全可以用來儲存到資料庫+html有顏色區分格式的檔案+網路轉發輸出(尤其適用於嵌入式linux**面程式,現場不方便外接除錯列印的裝置)。
做過的這麼多專案中,Qt4和Qt5的都有,我一般保留四個版本,4.8.7,為了相容qt4, 5.7.0,最後的支援XP的版本, 最新的長期支援版本5.9.7 最高的新版本5.12。毫無疑問,我要封裝的這個日誌類,也要支援4+5的,而且提供友好的介面。
1:支援動態啟動和停止。
2:支援日誌儲存的目錄。
3:支援網路發出列印日誌。
4:支援Qt4+Qt5。開箱即用。
5:支援多執行緒。
6:使用做到最簡單,start即可。

完整程式碼下載:

網路接收日誌工具截圖:

完整程式碼:

#ifndef SAVELOG_H
#define SAVELOG_H

#include <QObject>

class QFile;
class QTcpSocket;
class QTcpServer;

#ifdef quc
#if (QT_VERSION < QT_VERSION_CHECK(5,7,0))
#include <QtDesigner/QDesignerExportWidget>
#else
#include <QtUiPlugin/QDesignerExportWidget>
#endif

class QDESIGNER_WIDGET_EXPORT SaveLog : public QObject
#else
class SaveLog : public QObject
#endif

{
    Q_OBJECT
public:
    static SaveLog *Instance();
    explicit SaveLog(QObject *parent = 0);
    ~SaveLog();

private:
    static QScopedPointer<SaveLog> self;

    //檔案物件
    QFile *file;
    //是否重定向到網路
    bool toNet;
    //日誌檔案路徑
    QString path;
    //日誌檔名稱
    QString name;
    //日誌檔案完整名稱
    QString fileName;

signals:
    void send(const QString &content);

public slots:
    //啟動日誌服務
    void start();
    //暫停日誌服務
    void stop();
    //儲存日誌
    void save(const QString &content);

    //設定是否重定向到網路
    void setToNet(bool toNet);
    //設定日誌檔案存放路徑
    void setPath(const QString &path);
    //設定日誌檔名稱
    void setName(const QString &name);

};

class SendLog : public QObject
{
    Q_OBJECT
public:
    static SendLog *Instance();
    explicit SendLog(QObject *parent = 0);
    ~SendLog();

private:
    static QScopedPointer<SendLog> self;
    QTcpSocket *socket;
    QTcpServer *server;

private slots:
    void newConnection();

public slots:
    //傳送日誌
    void send(const QString &content);
};

#endif // SAVELOG_H
#include "savelog.h"
#include "qmutex.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"))

//日誌重定向
#if (QT_VERSION <= QT_VERSION_CHECK(5,0,0))
void Log(QtMsgType type, const char *msg)
#else
void Log(QtMsgType type, const QMessageLogContext &, const QString &msg)
#endif
{
    //加鎖,防止多執行緒中qdebug太頻繁導致崩潰
    QMutex mutex;
    QMutexLocker locker(&mutex);
    QString content;

    //這裡可以根據不同的型別加上不同的頭部用於區分
    switch (type) {
    case QtDebugMsg:
        content = QString("%1").arg(msg);
        break;

    case QtWarningMsg:
        content = QString("%1").arg(msg);
        break;

    case QtCriticalMsg:
        content = QString("%1").arg(msg);
        break;

    case QtFatalMsg:
        content = QString("%1").arg(msg);
        break;
    }

    SaveLog::Instance()->save(content);
}

QScopedPointer<SaveLog> SaveLog::self;
SaveLog *SaveLog::Instance()
{
    if (self.isNull()) {
        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)));

    file = new QFile(this);
    toNet = false;
    //預設取應用程式根目錄
    path = qApp->applicationDirPath();
    //預設取應用程式可執行檔名稱
    QString str = qApp->applicationFilePath();
    QStringList list = str.split("/");
    name = list.at(list.count() - 1).split(".").at(0);
    fileName = "";
}

SaveLog::~SaveLog()
{
    file->close();    
}

//安裝日誌鉤子,輸出除錯資訊到檔案,便於除錯
void SaveLog::start()
{
#if (QT_VERSION <= QT_VERSION_CHECK(5,0,0))
    qInstallMsgHandler(Log);
#else
    qInstallMessageHandler(Log);
#endif
}

//解除安裝日誌鉤子
void SaveLog::stop()
{
#if (QT_VERSION <= QT_VERSION_CHECK(5,0,0))
    qInstallMsgHandler(0);
#else
    qInstallMessageHandler(0);
#endif
}

void SaveLog::save(const QString &content)
{
    //如果重定向輸出到網路則通過網路發出去,否則輸出到日誌檔案
    if (toNet) {
        emit send(content);
    } else {
        //方法改進:之前每次輸出日誌都開啟檔案,改成只有當日期改變時才新建和開啟檔案
        QString fileName = QString("%1/%2_log_%3.txt").arg(path).arg(name).arg(QDATE);
        if (this->fileName != fileName) {
            this->fileName = fileName;
            if (file->isOpen()) {
                file->close();
            }

            file->setFileName(fileName);
            file->open(QIODevice::WriteOnly | QIODevice::Append | QFile::Text);
        }

        QTextStream logStream(file);
        logStream << content << "\n";
    }
}

void SaveLog::setToNet(bool toNet)
{
    this->toNet = toNet;
}

void SaveLog::setPath(const QString &path)
{
    this->path = path;
}

void SaveLog::setName(const QString &name)
{
    this->name = name;
}


//網路傳送日誌資料類
QScopedPointer<SendLog> SendLog::self;
SendLog *SendLog::Instance()
{
    if (self.isNull()) {
        QMutex mutex;
        QMutexLocker locker(&mutex);
        if (self.isNull()) {
            self.reset(new SendLog);
        }
    }

    return self.data();
}

SendLog::SendLog(QObject *parent)
{
    socket = NULL;
    server = new QTcpServer(this);
    connect(server, SIGNAL(newConnection()), this, SLOT(newConnection()));

    int listenPort = 6000;
#if (QT_VERSION > QT_VERSION_CHECK(5,0,0))
    server->listen(QHostAddress::AnyIPv4, listenPort);
#else
    server->listen(QHostAddress::Any, listenPort);
#endif
}

SendLog::~SendLog()
{
    if (socket != NULL) {
        socket->disconnectFromHost();
    }

    server->close();
}

void SendLog::newConnection()
{
    while (server->hasPendingConnections()) {
        socket = server->nextPendingConnection();
    }
}

void SendLog::send(const QString &content)
{
    if (socket != NULL && socket->isOpen()) {
        socket->write(content.toUtf8());
        socket->flush();
    }
}