QTCN开发网
> Qt应用版
> Qt自制简易好看的日志系统 [打印本页]
登录 - 注册 - 回复主题 - 发表主题
dd759378563
2019-05-01 02:18
Qt自制简易好看的日志系统 github仓库链接
简介
一个完善的软件工程,自然是少不了log系统的。这次涛哥教大家,用最少的代码做一个轻量又好看的log系统。涛哥知道有现成的log4cpp、log4cplus之类的,也有使用过。这次是抱着学习的心态来造这个轮子的,造轮子的过程才能学到更多知识,才能有进步、有提升,难道不是么?
预览
先看一下成果
原理
html格式的log
为了实现“代码最少”和“好看”的需求,涛哥把log写进了一个html文件。这样的log相当于一个静态的网页,只要装有浏览器的操作系统,都可以打开并看到上面图示那样的log。涛哥给这个html文件设计了一个固定的模板的:
```xml
body {
background: #18242b;
color: #afc6d1;
margin-right: 20px;
margin-left: 20px;
font-size: 14px;
font-family: Arial, sans-serif, sans;
}
a {...}
...```
以下是重构后的代码:
```html
a {
text-decoration: none;
}
a:link {
color: #a0b2bb;
}
a:active {
color: #f59504;
}
a:visited {
color: #adc7d4;
}
a:hover {
color: #e49115;
}
h1, h2 {
text-align: center;
}
.d, .w, .c, .f, .i {
padding: 3px;
overflow: auto;
}
.d, .i, .w, .c, .f {
background-color: #0f1011, #294453, #7993a0, #ff952b, #fc0808;
color: #a8c1ce, #a8c1ce, #1b2329, #1d2930, #19242b;
}
TaoLogger日志文件
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"]);
}
```
function selectType() {
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);
}
}
筛选器功能的实现
为了实现筛选器功能,我请教了前端同事,他给了我一个JQuery版本的解决方案,只需要几行代码,但需要引入一个大大的JQuery.js文件。我自己也尝试用QML和JavaScript编写了一个筛选器,不到20行代码,真是自己动手丰衣足食啊。
Log模板的用法
使用Log模板非常简单,只需将模板作为HTML文件的前部分,然后以追加的方式在模板后面添加每一条日志即可。需要注意的是,HTML的body结束标记没有写,但浏览器仍然可以正常打开,这表明了很强的容错性。
当然,每条日志都有一定的格式要求:
```html
```
这里增加了一对div标签,其class属性设置为d、i、w、c、f这几个字符中的一个,分别对应debug、info、warning、critical、fatal这五个日志级别。设置div的class属性就是给筛选器用来做筛选的关键。
Log模板的存取
文件读取?太慢了。这段固定的字符串可以直接编译进代码中,程序启动时直接装载到内存即可。那么在C++中,如何存储这段带有转义字符的字符串呢?涛哥给出的答案是:C++11的“原始字符串字面量”或者叫“R字符串”。可以参考这里 cppreference。
简单来说,是这样写的:
```cpp
string logTemplate = R"(xxxxxx)";
```
只要有了 R"( )" 这个写法,括号中间随便写转义字符、换行符都可以。当然,为了方便让编译器识别哪个才是真正的“结束括号”,C++11标准提出了括号前后增加分隔符的写法,即:
```cpp
string logTemplate = R"prefix(xxxxxx)prefix";
```
左括号的前面和右括号的后面,是同样的一段字符串作为分隔符就可以了。
涛哥的代码里是这样使用的:
```cpp
namespace Logger
{
const static QString logTemplate = u8R"logTemplate(
)logTemplate";
}
```
...
这里省略一大堆html代码
...
Qt的log系统
Qt的log分类包括:调试信息(qDebug)、常规信息(qInfo)、警告(qWarning)和严重问题(qCritical)。涛哥翻了Qt5.12的源码,发现这几个打印最终都是通过fprintf(stderr)或者fprintf(stdout)来实现输出的,不同的地方就在于Log类型。如果要用好这个分类,那我们平时使用打印的时候,就要注意做区分。
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等函数。
重构后的代码如下:
```cpp
#include
#include
namespace Logger
{
//默认存储路径为当前路径的Log文件夹下,默认文件数量为1024
void initLog(const QString& logPath = QStringLiteral("Log"), int logMaxCount = 1024);
//静态变量,记录存储路径
static QString gLogDir;
//静态变量,记录最大存储数量
static int gLogMaxCount;
} // namespace Logger
void Logger::initLog(const QString &logPath, int logMaxCount)
{
gLogDir = logPath;
gLogMaxCount = logMaxCount;
}
```
使用方法:
```cpp
#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__
// 初始化日志存储路径和容量
Logger::initLog("Log", 1024);
// 使用日志函数输出信息
LOG_DEBUG << u8"山有木兮木有枝,心悦君兮君不知。";
```
以下是重构后的代码:
```cpp
void setupLog()
{
//记录输出消息处理函数
qInstallMessageHandler(outputMessage);
//记录日志路径
QString logPath = QCoreApplication::applicationDirPath() + "/" + gLogDir;
//记录最大存储数
gLogMaxCount = logMaxCount;
//检查存储文件夹,不存在则创建
QDir dir(logPath);
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)
{
//生成一个QString副本,达到最大程度的复用。
static const QString messageTemp = QString("
//预定的消息类型映射表
QString formattedMsg;
switch (type)
{
case QtDebugMsg:
formattedMsg = QString("Debug: %1").arg(msg);
break;
case QtWarningMsg:
formattedMsg = QString("Warning: %1").arg(msg);
break;
case QtCriticalMsg:
formattedMsg = QString("Critical: %1").arg(msg);
break;
case QtFatalMsg:
formattedMsg = QString("Fatal: %1").arg(msg);
break;
default:
formattedMsg = QString("%1").arg(msg);
}
QFile file(logPath + "/log.html");
if (file.open(QIODevice::Append | QIODevice::Text))
{
file.write(messageTemp.arg(formattedMsg).toUtf8());
file.close();
}
}
```
以下是重构后的代码:
```cpp
static const char typeList[] = {'d', 'w', 'c', 'f', 'i'};
// 锁
QMutex mutex;
// 获取当前时间
QDateTime dt = QDateTime::currentDateTime();
// 将时间作为文件名
// 每分钟一个文件
QString fileNameDt = dt.toString("yyyy-MM-dd_hh");
// 每小时一个文件
// QString fileNameDt = dt.toString("yyyy-MM-dd_");
// 每天一个文件
// 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
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) {
// 如果文件不存在,写入htmlMessage到文件中
text_stream << htmlMessage;
} else {
// 如果文件已存在,追加htmlMessage到文件中
text_stream << "\n" << htmlMessage;
}
// 关闭文件流和文件
text_stream.close();
file.close();
// 解锁
mutex.unlock();
```
重构后的代码如下:
```cpp
// 文件不存在的情况下,先把我们的html模板写进去。
text_stream << logTemplate << "r
";
// 往文件流里面追加数据
text_stream << htmlMessage;
file.close();
mutex.unlock();
// 解锁
// 把log都写到文件了,QtCreator 或者VS 不就看不到输出了?
// 这里用Win32的方式多加了一次输出,当然也可以使用std::cout fprintf。不能再使用qDebug了,因为这是在qDebug的回调里,会无限递归调用的。
::OutputDebugString(message.toStdWString().data());
::OutputDebugString(L"\r
");
}
// 文件句柄复用
if (!file.isOpen()) {
file.open(QFile::Append | QFile::Text);
}
mutex.lock();
text_stream << file.readAll();
mutex.unlock();
```
多线程测试:涛哥同时起了8个线程,每个线程输出1000条log信息,并统计最终结果。具体代码可以去github查看。
感谢反馈,已修改。主要是在 std::thread join 之后,当前线程会等待。
big_mouse
2020-04-14 21:38
查看完整版本: [-- Qt自制简易好看的日志系统 --] [-- top --]
Powered by
phpwind
v8.7
Code ©2003-2011
phpwind
Gzip disabled