Content Table

Qt 自定义日志工具

C++ 中比较不错的日志工具有 log4cxxlog4qt 等,但是它们都不能和 qDebug(), qInfo() 等有机的结合在一起,所以在 Qt 中使用总觉得不够舒服,感谢 Qt 提供了 qInstallMessageHandler() 这个函数,使用这个函数可以安装自定义的日志输出处理函数,把日志输出到文件,控制台等,具体的使用可以查看 Qt 的帮助文档。

本文主要是介绍使用 qInstallMessageHandler() 实现一个简单的日志工具,例如调用 qDebug() << "Hi",输出的内容会同时输出到日志文件和控制台,并且日志文件如果不是当天创建的,会使用它的创建日期备份起来,涉及到的文件有:

  • main.cpp: 使用示例
  • Singleton.h: 单例模版
  • LogHandler.h: 自定义日志相关类的头文件
  • LogHandler.cpp: 自定义日志相关类的实现文件

定义 QT_MESSAGELOGCONTEXT

qDebug 其实是一个宏: #define qDebug QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).debug,在 Debug 版本的时候会输出行号,文件名,函数名等,但是在 Release 版本的时候不会输出,为了输出它们,需要在 .pro 文件里加入下面的定义:

1
DEFINES += QT_MESSAGELOGCONTEXT

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "LogHandler.h"

#include <QApplication>
#include <QDebug>
#include <QTime>
#include <QPushButton>

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

// [[1]] 安装消息处理函数
Singleton<LogHandler>::getInstance().installMessageHandler();

// [[2]] 输出测试,查看是否写入到文件
qDebug() << "Hello";
qDebug() << "当前时间是: " << QTime::currentTime().toString("hh:mm:ss");
qInfo() << QString("God bless you!");

QPushButton *button = new QPushButton("退出");
button->show();
QObject::connect(button, &QPushButton::clicked, [&app] {
qDebug() << "退出";
app.quit();
});

// [[3]] 删除自定义消息处理,然后启用
Singleton<LogHandler>::getInstance().uninstallMessageHandler();
qDebug() << "........"; // 不写入日志
Singleton<LogHandler>::getInstance().installMessageHandler();

int ret = app.exec(); // 事件循环结束

// [[4]] 程序结束时释放 LogHandler 的资源,例如刷新并关闭日志文件
Singleton<LogHandler>::getInstance().uninstallMessageHandler();

return ret;
}

控制台输出:

1
2
3
4
5
Hello
当前时间是: "16:29:42"
"God bless you!"
........
退出

日志文件:
位置: exe 所在目录的 log 目录下的 log.txt
格式: 时间 - [Level] (文件名:行数, 函数): 消息

1
2
3
4
16:29:42 - [Debug] (main.cpp:15, int main(int, char **)): Hello
16:29:42 - [Debug] (main.cpp:16, int main(int, char **)): 当前时间是: "16:29:42"
16:29:42 - [Info ] (main.cpp:17, int main(int, char **)): "God bless you!"
16:29:46 - [Debug] (main.cpp:22, auto main(int, char **)::(anonymous class)::operator()() const): 退出

LogHandler.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef LOGHANDLER_H
#define LOGHANDLER_H

#include "Singleton.h"

#define LogHandlerInstance Singleton<LogHandler>::getInstance()

struct LogHandlerPrivate;

class LogHandler {
SINGLETON(LogHandler) // 使用单例模式
public:
void uninstallMessageHandler(); // 释放资源
void installMessageHandler(); // 给 Qt 安装消息处理函数

private:
LogHandlerPrivate *d;
};

#endif // LOGHANDLER_H

LogHandler.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#include "LogHandler.h"

#include <stdio.h>
#include <stdlib.h>
#include <QDebug>
#include <QDateTime>
#include <QMutexLocker>
#include <QtGlobal>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QTimer>
#include <QTextStream>
#include <iostream>
#include <QTextCodec>

/************************************************************************************************************
* *
* LogHandlerPrivate *
* *
***********************************************************************************************************/
struct LogHandlerPrivate {
LogHandlerPrivate();
~LogHandlerPrivate();

// 打开日志文件 log.txt,如果日志文件不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 log.txt
void openAndBackupLogFile();

// 消息处理函数
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg);

// 如果日志所在目录不存在,则创建
void makeSureLogDirectory() const;

QDir logDir; // 日志文件夹
QTimer renameLogFileTimer; // 重命名日志文件使用的定时器
QTimer flushLogFileTimer; // 刷新输出到日志文件的定时器
QDate logFileCreatedDate; // 日志文件创建的时间

static QFile *logFile; // 日志文件
static QTextStream *logOut; // 输出日志的 QTextStream,使用静态对象就是为了减少函数调用的开销
static QMutex logMutex; // 同步使用的 mutex
};

// 初始化 static 变量
QMutex LogHandlerPrivate::logMutex;
QFile* LogHandlerPrivate::logFile = nullptr;
QTextStream* LogHandlerPrivate::logOut = nullptr;

LogHandlerPrivate::LogHandlerPrivate() {
logDir.setPath("log"); // TODO: 日志文件夹的路径,为 exe 所在目录下的 log 文件夹,可从配置文件读取
QString logPath = logDir.absoluteFilePath("log.txt"); // 日志的路径
// 日志文件创建的时间
// QFileInfo::created(): On most Unix systems, this function returns the time of the last status change.
// 所以不能运行时使用这个函数检查创建时间,因为会在运行时变化,于是在程序启动时保存下日志文件的最后修改时间,
// 在后面判断如果不是今天则用于重命名 log.txt
// 如果是 Qt 5.10 后,lastModified() 可以使用 birthTime() 代替
logFileCreatedDate = QFileInfo(logPath).lastModified().date();

// 打开日志文件,如果不是当天创建的,备份已有日志文件
openAndBackupLogFile();

// 十分钟检查一次日志文件创建时间
renameLogFileTimer.setInterval(1000 * 60 * 10); // TODO: 可从配置文件读取
// renameLogFileTimer.setInterval(1000); // 为了快速测试看到日期变化后是否新创建了对应的日志文件,所以 1 秒检查一次
renameLogFileTimer.start();
QObject::connect(&renameLogFileTimer, &QTimer::timeout, [this] {
QMutexLocker locker(&LogHandlerPrivate::logMutex);
openAndBackupLogFile();
});

// 定时刷新日志输出到文件,尽快的能在日志文件里看到最新的日志
flushLogFileTimer.setInterval(1000); // TODO: 可从配置文件读取
flushLogFileTimer.start();
QObject::connect(&flushLogFileTimer, &QTimer::timeout, [] {
// qDebug() << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); // 测试不停的写入内容到日志文件
QMutexLocker locker(&LogHandlerPrivate::logMutex);
if (nullptr != logOut) {
logOut->flush();
}
});
}

LogHandlerPrivate::~LogHandlerPrivate() {
if (nullptr != logFile) {
logFile->flush();
logFile->close();
delete logOut;
delete logFile;

// 因为他们是 static 变量
logOut = nullptr;
logFile = nullptr;
}
}

// 打开日志文件 log.txt,如果不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 log.txt
void LogHandlerPrivate::openAndBackupLogFile() {
// 总体逻辑:
// 1. 程序启动时 logFile 为 nullptr,初始化 logFile,有可能是同一天打开已经存在的 logFile,所以使用 Append 模式
// 2. logFileCreatedDate is nullptr, 说明日志文件在程序开始时不存在,所以记录下创建时间
// 3. 程序运行时检查如果 logFile 的创建日期和当前日期不相等,则使用它的创建日期重命名,然后再生成一个新的 log.txt 文件

makeSureLogDirectory(); // 如果日志所在目录不存在,则创建
QString logPath = logDir.absoluteFilePath("log.txt"); // 日志的路径

// [[1]] 程序启动时 logFile 为 nullptr
if (nullptr == logFile) {
logFile = new QFile(logPath);
logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) ? new QTextStream(logFile) : nullptr;

if (nullptr != logOut) {
logOut->setCodec("UTF-8");
}

// [[2]] 如果文件是第一次创建,则创建日期是无效的,把其设置为当前日期
if (logFileCreatedDate.isNull()) {
logFileCreatedDate = QDate::currentDate();
}

// TODO: 可以检查日志文件超过 30 个,删除 30 天前的日志文件
}

// [[3]] 程序运行时如果创建日期不是当前日期,则使用创建日期重命名,并生成一个新的 log.txt
if (logFileCreatedDate != QDate::currentDate()) {
logFile->flush();
logFile->close();
delete logOut;
delete logFile;

QString newLogPath = logDir.absoluteFilePath(logFileCreatedDate.toString("yyyy-MM-dd.log"));;
QFile::copy(logPath, newLogPath); // Bug: 按理说 rename 会更合适,但是 rename 时最后一个文件总是显示不出来,需要 killall Finder 后才出现
QFile::remove(logPath); // 删除重新创建,改变创建时间

logFile = new QFile(logPath);
logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) ? new QTextStream(logFile) : nullptr;
logFileCreatedDate = QDate::currentDate();

if (nullptr != logOut) {
logOut->setCodec("UTF-8");
}
}
}

// 如果日志所在目录不存在,则创建
void LogHandlerPrivate::makeSureLogDirectory() const {
if (!logDir.exists()) {
logDir.mkpath("."); // 可以递归的创建文件夹
}
}

// 消息处理函数
void LogHandlerPrivate::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
QMutexLocker locker(&LogHandlerPrivate::logMutex);
QString level;

switch (type) {
case QtDebugMsg:
level = "DEBUG";
break;
case QtInfoMsg:
level = "INFO ";
break;
case QtWarningMsg:
level = "WARN ";
break;
case QtCriticalMsg:
level = "ERROR";
break;
case QtFatalMsg:
level = "FATAL";
break;
default:
break;
}

// 输出到标准输出: Windows 下 std::cout 使用 GB2312,而 msg 使用 UTF-8,但是程序的 Local 也还是使用 UTF-8
#if defined(Q_OS_WIN)
QByteArray localMsg = QTextCodec::codecForName("GB2312")->fromUnicode(msg); //msg.toLocal8Bit();
#else
QByteArray localMsg = msg.toLocal8Bit();
#endif

std::cout << std::string(localMsg) << std::endl;

if (nullptr == LogHandlerPrivate::logOut) {
return;
}

// 输出到日志文件, 格式: 时间 - [Level] (文件名:行数, 函数): 消息
QString fileName = context.file;
int index = fileName.lastIndexOf(QDir::separator());
fileName = fileName.mid(index + 1);

(*LogHandlerPrivate::logOut) << QString("%1 - [%2] (%3:%4, %5): %6\n")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(level)
.arg(fileName).arg(context.line).arg(context.function).arg(msg);
}

/************************************************************************************************************
* *
* LogHandler *
* *
***********************************************************************************************************/
LogHandler::LogHandler() : d(nullptr) {
}

LogHandler::~LogHandler() {
}

void LogHandler::installMessageHandler() {
QMutexLocker locker(&LogHandlerPrivate::logMutex);

if (nullptr == d) {
d = new LogHandlerPrivate();
qInstallMessageHandler(LogHandlerPrivate::messageHandler); // 给 Qt 安装自定义消息处理函数
}
}

void LogHandler::uninstallMessageHandler() {
QMutexLocker locker(&LogHandlerPrivate::logMutex);
qInstallMessageHandler(nullptr);
delete d;
d = nullptr;
}

Singleton.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#ifndef SINGLETON_H
#define SINGLETON_H

#include <QMutex>
#include <QScopedPointer>

////////////////////////////////////////////////////////////////////////////////
/// ///
/// Singleton signature ///
/// ///
////////////////////////////////////////////////////////////////////////////////
/**
* 使用方法:
* 1. 定义类为单例:
* class ConnectionPool {
* SINGLETON(ConnectionPool) // Here
* public:
*
* 2. 实现无参构造函数,析构函数
* 3. 获取单例类的对象:
* Singleton<ConnectionPool>::getInstance();
* ConnectionPool &pool = Singleton<ConnectionPool>::getInstance();
* 注意: 如果单例的类需要释放的资源和 Qt 底层的信号系统有关系,例如 QSettings,QSqlDatabase 等,
* 需要在程序结束前手动释放(也就是在 main() 函数返回前调用释放资源的函数),
* 否则有可能在程序退出时报系统底层的信号错误,导致如 QSettings 的数据没有保存。
*/
template <typename T>
class Singleton {
public:
static T& getInstance();

Singleton(const Singleton &other);
Singleton<T>& operator=(const Singleton &other);

private:
static QMutex mutex;
static QScopedPointer<T> instance;
};

////////////////////////////////////////////////////////////////////////////////
/// ///
/// Singleton definition ///
/// ///
////////////////////////////////////////////////////////////////////////////////
template <typename T> QMutex Singleton<T>::mutex;
template <typename T> QScopedPointer<T> Singleton<T>::instance;

template <typename T>
T& Singleton<T>::getInstance() {
if (instance.isNull()) {
mutex.lock();
if (instance.isNull()) {
instance.reset(new T());
}
mutex.unlock();
}

return *instance.data();
}

////////////////////////////////////////////////////////////////////////////////
/// ///
/// Singleton Macro ///
/// ///
////////////////////////////////////////////////////////////////////////////////
#define SINGLETON(Class) \
private: \
Class(); \
~Class(); \
Class(const Class &other); \
Class& operator=(const Class &other); \
friend class Singleton<Class>; \
friend struct QScopedPointerDeleter<Class>;

#endif // SINGLETON_H

思考

  1. main() 函数里的 qDebug() 输出都是在 UI 线程,LogHandler 是否多线程安全?怎么测试?
  2. 日志的相关配置数据例如输出目录等都是写死在程序里的,如果写到配置文件里是不是更灵活?
  3. 日志的格式也是写死在程序里的,如果能做到通过配置修改日志格式那就更强大了,就像 log4cxx 一样
  4. 测试如何快速的看到不同日期生成的日志文件不同?
  5. 删除超过 30 天的日志
  6. 单个日志文件例如大于 100M 后重新创建一个新的日志文件