簡介
一個完善的軟體工程,自然是少不了log系統的。
這次濤哥教大家,用最少的代碼做一個輕量又好看的log系統。
濤哥知道有現成的log4cpp、log4cplus之類的,也有使用過。
這次是抱著學習的心態來造這個輪子的,造輪子的過程才能學到
更多知識,纔能有進步、有提升,難道不是麼?
先看一下成果
為了實現 「代碼最少」 和 「好看」 的需求,濤哥把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://jaredtao.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行代碼,真是自己動手豐衣足食啊。)
很簡單的,模板作為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屬性,就是給篩選器用來做篩選。
文件讀取? 不,太慢了。
這就是一段固定的字元串,直接編譯進代碼裏,程序啟動的時候直接裝載到內存就好了。
那麼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的列印信息,大家普遍使用的是qDebug,不過Qt除了qDebug,還有qInfo, qWarning, qCritical等等。
濤哥翻了Qt5.12的源碼,發現這幾個列印最終都是通過fprintf(stderr)或者fprintf(stdout)來實現輸出的,
不同的地方就在於Log類型。如果要用好這個分類,那我們平時使用列印的時候,就要注意做區分:
- 調試信息用qDebug
- 常規信息用qInfo
- 警告用qWarning
- 比較嚴重的問題用qCritical
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類型、文件名、行號 等信息。也可以不改任何代碼、改環境變數來做到
這裡有個問題,就是文件名和行號在debug模式正常,Release模式會變成空的。
要解決這個問題,那麼就需要編譯器提供的內置宏__FILE__ 和 __LINE__了
__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還提供了一個函數 qInstallMessageHandler,可以插入一個回調函數,讓每一行qDebug/qInfo等
函數的列印信息,都經過這個回調來處理。看一下幫助文檔:
其實幫助文檔已經提供了一個簡易的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) { // } }
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> "); //預定的消息類型映射表 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 << " "; } //往文件流裡面追加數據 text_stream << htmlMessage;
file.close(); mutex.unlock(); //解鎖
//把log都寫到文件了,QtCreator 或者VS 不就看不到輸出了? //這裡用Win32的方式多加了一次輸出,當然也可以使用std::cout fprintf。不能再使用qDebug了,因為這是在qDebug的回調裏,會無限遞歸調用的。 ::OutputDebugString(message.toStdWString().data()); ::OutputDebugString(L" "); }
感謝 Qt俠@劉典武指出了優化的地方,應該復用文件句柄,不要每次都打開關閉文件,所以濤哥改了一下。
這裡貼個小烏龜的變更圖吧,當然github上也有變更記錄的。
濤哥同時起了8個線程,每個線程輸出1000條log信息,並統計最終結果。
代碼去github吧。
jaredtao/TaoLogger?github.com
文章出自濤哥的博客 – 點擊這裡查看濤哥的博客
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可, 轉載請註明出處, 謝謝合作 ? 濤哥
作者 濤哥
開發理念 弘揚魯班文化,傳承工匠精神
博客
郵箱[email protected]
微信 xsd2410421
QQ 759378563
請放心聯繫我,樂於提供諮詢服務,也可洽談技術支持相關事宜。
推薦閱讀: