簡介

一個完善的軟體工程,自然是少不了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://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行代碼,真是自己動手豐衣足食啊。)

  • 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>
");
//預定的消息類型映射表
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吧。

github倉庫鏈接

jaredtao/TaoLogger?

github.com圖標

轉載聲明

文章出自濤哥的博客 – 點擊這裡查看濤哥的博客

本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可, 轉載請註明出處, 謝謝合作 ? 濤哥

聯繫方式

作者 濤哥

開發理念 弘揚魯班文化,傳承工匠精神

博客

濤哥的博客?

jaredtao.github.io

郵箱[email protected]

微信 xsd2410421

QQ 759378563

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

推薦閱讀:

相關文章