QTCN开发网

> Qt应用版

> Qt自制简易好看的日志系统 [打印本页]

登录 - 注册 - 回复主题 - 发表主题

dd759378563

2019-05-01 02:18

Qt自制简易好看的日志系统 github仓库链接

简介

一个完善的软件工程,自然是少不了log系统的。这次涛哥教大家,用最少的代码做一个轻量又好看的log系统。涛哥知道有现成的log4cpp、log4cplus之类的,也有使用过。这次是抱着学习的心态来造这个轮子的,造轮子的过程才能学到更多知识,才能有进步、有提升,难道不是么?

预览

先看一下成果

原理

html格式的log

为了实现“代码最少”和“好看”的需求,涛哥把log写进了一个html文件。这样的log相当于一个静态的网页,只要装有浏览器的操作系统,都可以打开并看到上面图示那样的log。涛哥给这个html文件设计了一个固定的模板的:

```xml

TaoLogger

...

```

以下是重构后的代码:

```html

TaoLogger 日志文件

TaoLogger日志文件

```

筛选器功能的实现

为了实现筛选器功能,我请教了前端同事,他给了我一个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";

}

```

TaoLogger

...

这里省略一大堆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("

%2
");

//预定的消息类型映射表

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(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) {

// 如果文件不存在,写入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