簡介
一個完善的軟件工程,自然是少不了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倉庫鏈接
轉載聲明
文章出自濤哥的博客 – 點擊這裏查看濤哥的博客
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可, 轉載請註明出處, 謝謝合作 © 濤哥
聯繫方式
作者 | 濤哥 |
---|---|
開發理念 | 弘揚魯班文化,傳承工匠精神 |
博客 | https://wentaojia2014.github.io |
知乎 | https://www.zhihu.com/people/wentao-jia |
郵箱 | [email protected] |
微信 | xsd2410421 |
759378563 |
請放心聯繫我,樂於提供諮詢服務,也可洽談有償技術支持相關事宜。
打賞
覺得分享的內容還不錯, 就請作者喝杯奶茶吧~~