Qt自制簡易好看的日誌系統

簡介

一個完善的軟件工程,自然是少不了log系統的。

這次濤哥教大家,用最少的代碼做一個輕量又好看的log系統。

濤哥知道有現成的log4cpp、log4cplus之類的,也有使用過。

這次是抱着學習的心態來造這個輪子的,造輪子的過程才能學到

更多知識,纔能有進步、有提升,難道不是麼?

預覽

先看一下成果

預覽

原理

html格式的log

爲了實現 “代碼最少” 和 “好看” 的需求,濤哥把log寫進了一個html文件。

這樣的log相當於一個靜態的網頁,只要裝有瀏覽器的操作系統,都可以打開並看到上面圖示那樣的log。

濤哥給這個html文件設計了一個固定的模板的:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>

<head>
    <title>TaoLogger</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style type="text/css" id="logCss">
        body {
            background: #18242b;
            color: #afc6d1;
            margin-right: 20px;
            margin-left: 20px;
            font-size: 14px;
            font-family: Arial, sans-serif, sans;
        }

        a {
            text-decoration: none;
        }

        a:link {
            color: #a0b2bb;
        }

        a:active {
            color: #f59504;
        }

        a:visited {
            color: #adc7d4;
        }

        a:hover {
            color: #e49115;
        }

        h1 {
            text-align: center;
        }

        h2 {
            color: #ebe5e5;
        }

        .d,
        .w,
        .c,
        .f,
        .i {
            padding: 3px;
            overflow: auto;
        }

        .d {
            background-color: #0f1011;
            color: #a8c1ce;
        }

        .i {
            background-color: #294453;
            color: #a8c1ce;
        }

        .w {
            background-color: #7993a0;
            color: #1b2329;
        }

        .c {
            background-color: #ff952b;
            color: #1d2930;
        }

        .f {
            background-color: #fc0808;
            color: #19242b;
        }
    </style>
</head>

<body>
    <h1><a href="https://wentaojia2014.github.io">TaoLogger</a> 日誌文件</h1>
    <script type="text/JavaScript">
        function objHide(obj) {
            obj.style.display="none"
        }
        function objShow(obj) {
            obj.style.display="block"
        }
        function selectType() {
            var sel = document.getElementById("typeSelect");
            const hideList = new Set(['d', 'i', 'w', 'c', 'f']);
            if (sel.value === 'a') {
                hideList.forEach(element => {
                    var list = document.querySelectorAll('.' + element);
                    list.forEach(objShow);
                });
            } else {
                var ss = hideList;
                ss.delete(sel.value);
                ss.forEach(element => {
                    var list = document.querySelectorAll('.' + element);
                    list.forEach(objHide);
                });
                var showList = document.querySelectorAll('.' + sel.value);
                showList.forEach(objShow);
            }
        }
    </script>
    <select id="typeSelect" onchange="selectType()">
        <option value='a' selected="selected">All</option>
        <option value='d'>Debug</option>
        <option value='i'>Info</option>
        <option value='w'>Warning</option>
        <option value='c'>Critical</option>
        <option value='f'>Fatal</option>
    </select>

(如果你不懂html,也沒關係,直接拿過去用就好了)

這個模板只使用了一些很基本的html元素和css樣式表,篩選器那裏用了一點JavaScript。

(篩選器功能,我去請教了一下前端的同事,給了我一個JQuery版本,只要很少幾行代碼,但是要帶上一個大大的JQuery.js。。。)

(濤哥我也寫了不少qml,多多少少還是懂點js的,於是就自己寫了這麼一個篩選器。不到20行代碼,真是自己動手豐衣足食啊。)

  • Log模板的用法

很簡單的,模板作爲html文件的前面部分,接下來每一行log,以追加的方式跟在模板後面就行了。

(html的body結束標記並沒有寫,瀏覽器都能正常打開。容錯性真的強!)

當然, 每一條log有個格式要求:

    <div class="d"> 山有木兮木有枝,心悅君兮君不知。</div>

就是增加了一對div標記, div的class屬性要設置爲d、i、w、c、f這幾個字符中的一個,分別是

debug、info、warning、critical、fatal的首字母, 這正是Qt所提供的log分類。

設置div的class屬性,就是給篩選器用來做篩選。

  • Log模板的存取

文件讀取? 不,太慢了。

這就是一段固定的字符串,直接編譯進代碼裏,程序啓動的時候直接裝載到內存就好了。

那麼C++裏面,怎麼才能裝下這段帶有轉義字符的字符串呢?濤哥的答案是:C++11的 “原始字符串字面量”或者叫 “R字符串”

可以參考這裏 cppreference

簡單來說,是這樣寫的:

string logTemplate = R"(xxxxxx)";

只要有了 R"( )" 這個寫法,括號中間隨便寫轉義字符、換行符都行。當然爲了方便讓編譯器識別哪個

纔是真正的’結束括號’,C++11標準提出了括號前後增加分隔符的寫法,即:

string logTemplate = R"prefix(xxxxxx)prefix";

左括號的前面和右括號的後面, 是同樣的一段字符串作爲分隔符就行了。

濤哥的代碼裏是這麼用的

namespace Logger
{
    const static QString logTemplate = u8R"logTemplate(
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>

<head>
    <title>TaoLogger</title>
    ...
    這裏省略一大堆html代碼
    ...

    )logTemplate";

}

Qt的log系統

  • Qt的log分類

Qt的打印信息,大家普遍使用的是qDebug,不過Qt除了qDebug,還有qInfo, qWarning, qCritical等等。

濤哥翻了Qt5.12的源碼,發現這幾個打印最終都是通過fprintf(stderr)或者fprintf(stdout)來實現輸出的,

不同的地方就在於Log類型。如果要用好這個分類,那我們平時使用打印的時候,就要注意做區分:

- 調試信息用qDebug

- 常規信息用qInfo

- 警告用qWarning

- 比較嚴重的問題用qCritical
  • Qt的log格式化

Qt提供了一個函數qSetMessagePattern,用來定製輸出信息。

例如:

    qSetMessagePattern("[%{time yyyyMMdd h:mm:ss.zzz t} %{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}] %{file}:%{line} - %{message}");

一般只要在main.cpp中添加這一行代碼,之後的qDebug、qInfo等函數都會按照這個格式來輸出,包含了

時間戳、log類型、文件名、行號 等信息。也可以不改任何代碼、改環境變量來做到

預覽
預覽

  • Release模式信息缺失

這裏有個問題,就是文件名和行號在debug模式正常,Release模式會變成空的。

要解決這個問題,那麼就需要編譯器提供的內置宏__FILE____LINE__

濤哥寫了這樣幾個宏,代替qDebug和qInfo等函數。

#define LOG_DEBUG qDebug() << __FILE__ << __FUNCTION__ << __LINE__
#define LOG_INFO qInfo() << __FILE__ << __FUNCTION__ << __LINE__
#define LOG_WARN qWarning() << __FILE__ << __FUNCTION__ << __LINE__
#define LOG_CRIT qCritical() << __FILE__ << __FUNCTION__ << __LINE__

用法類似這樣:

    LOG_DEBUG << u8"山有木兮木有枝,心悅君兮君不知。";
  • Qt的寫log文件

Qt還提供了一個函數 qInstallMessageHandler,可以插入一個回調函數,讓每一行qDebug/qInfo等

函數的打印信息,都經過這個回調來處理。看一下幫助文檔:

預覽

其實幫助文檔已經提供了一個簡易的log功能,濤哥就是在這個功能的基礎上,做了一些定製化的修改。

融合

預覽

  • log存儲路徑和容量

濤哥寫了一個函數和一組靜態變量,用來設置和記錄log存儲的路徑和容量

頭文件中的聲明

#pragma once
#include <QDebug>

namespace Logger
{
//默認存儲路徑爲當前路徑的Log文件夾下,默認文件數量爲1024
void initLog(const QString& logPath = QStringLiteral("Log"), int logMaxCount = 1024);

} // namespace Logger

CPP中的實現

namespace Logger
{
//靜態變量,記錄存儲路徑
static QString gLogDir;
//靜態變量,記錄最大存儲數量
static int gLogMaxCount;

void initLog(const QString &logPath, int logMaxCount)
{
    //安裝回調
    qInstallMessageHandler(outputMessage);
    //記錄路徑
    gLogDir = QCoreApplication::applicationDirPath() + "/" + logPath;
    //記錄最大存儲數
    gLogMaxCount = logMaxCount;
    //檢查存儲文件夾,不存在則創建
    QDir dir(gLogDir);
    if (!dir.exists())
    {
        dir.mkpath(dir.absolutePath());
    }
    //獲取文件列表
    QStringList infoList = dir.entryList(QDir::Files, QDir::Name);
    //硬盤空間有限,超過最大存儲數的都刪掉。
    while (infoList.size() > gLogMaxCount)
    {
        //每次刪第一個。文件名其實是默認按時間排序的,第一個就是時間最早的。
        dir.remove(infoList.first());
        infoList.removeFirst();
    }
}
static void outputMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
    //
}
}
  • log存儲
static void outputMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
    //每一條消息的約定格式。%1即log類型,%2即log內容。這裏用靜態變量,每次用的時候填充
    //生成一個QString副本,達到最大程度的複用。
    static const QString messageTemp= QString("<div class=\"%1\">%2</div>\r\n");
    //預定的消息類型映射表
    static const char typeList[] = {'d', 'w', 'c', 'f', 'i'};
    //鎖
    static QMutex mutex;
    //取時間
    QDateTime dt = QDateTime::currentDateTime();
    
    //時間作爲文件名

    //每分鐘一個文件
    //QString fileNameDt = dt.toString("yyyy-MM-dd_hh_mm");

    //每小時一個文件
    QString fileNameDt = dt.toString("yyyy-MM-dd_hh");

    //每天一個文件
    //QString fileNameDt = dt.toString("yyyy-MM-dd_");
    //時間戳
    QString contentDt = dt.toString("yyyy-MM-dd hh:mm:ss");
    //消息的前面寫上時間戳,後面寫內容。 msg如果是用LOG_WARN那幾個宏打印的,本身已經帶了文件名和行號了。
    QString message = QString("%1 %2").arg(contentDt).arg(msg);
    
    //組裝一條html格式的log
    QString htmlMessage = messageTemp.arg(typeList[static_cast<int>(type)]).arg(message);

    QFile file(QString("%1/%2_log.html").arg(gLogDir).arg(fileNameDt));
    //這裏開始鎖起來,多線程安全
    mutex.lock();
    bool exist = file.exists();
    //寫 | 追加的方式
    file.open(QIODevice::WriteOnly | QIODevice::Append);
    //文件流
    QTextStream text_stream(&file);
    //注意字符編碼
    text_stream.setCodec("UTF-8");
    if (!exist)
    {
        //文件不存在的情況下,先把我們的html模板寫進去。
        text_stream << logTemplate << "\r\n";
    }
    //往文件流裏面追加數據
    text_stream << htmlMessage;

    file.close();
    mutex.unlock();
    //解鎖
    
    //把log都寫到文件了,QtCreator 或者VS 不就看不到輸出了?
    //這裏用Win32的方式多加了一次輸出,當然也可以使用std::cout fprintf。不能再使用qDebug了,因爲這是在qDebug的回調裏,會無限遞歸調用的。
    ::OutputDebugString(message.toStdWString().data());
    ::OutputDebugString(L"\r\n");
}

文件句柄複用

有朋友(Qt俠- 劉典武)指出了優化的地方,應該複用文件句柄,不要每次都打開關閉文件,所以濤哥改了一下。

這裏貼個小烏龜的變更圖吧,當然github上也有變更記錄的。

預覽

多線程測試

濤哥同時起了8個線程,每個線程輸出1000條log信息,並統計最終結果。

預覽

預覽

代碼去github吧。

github倉庫鏈接

TaoLogger

轉載聲明

文章出自濤哥的博客 – 點擊這裏查看濤哥的博客
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可, 轉載請註明出處, 謝謝合作 © 濤哥

聯繫方式


作者 濤哥
開發理念 弘揚魯班文化,傳承工匠精神
博客 https://wentaojia2014.github.io
知乎 https://www.zhihu.com/people/wentao-jia
郵箱 [email protected]
微信 xsd2410421
QQ 759378563

請放心聯繫我,樂於提供諮詢服務,也可洽談有償技術支持相關事宜。

打賞


覺得分享的內容還不錯, 就請作者喝杯奶茶吧~~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章