一、前言
C++ 中比較不錯的日志工具有 log4cxx
,log4qt
等,但是它們都不能和 qDebug()
, qInfo()
等有機的結合在一起,所以在 Qt 中使用總覺得不夠舒服,感謝 Qt 提供了 qInstallMessageHandler()
這個函數,使用這個函數可以安裝自定義的日志輸出處理函數,把日志輸出到文件,控制台等,具體的使用可以查看 Qt 的幫助文檔。
本文主要是介紹使用 qInstallMessageHandler()
實現一個簡單的日志工具,例如調用 qDebug() << "Hi"
,輸出的內容會同時輸出到日志文件和控制台,並且日志文件如果不是當天創建的,會使用它的創建日期備份起來,涉及到的文件有:
- main.cpp: 使用示例
- LogHandler.h: 自定義日志相關類的頭文件
- LogHandler.cpp: 自定義日志相關類的實現文件
另外實現功能:
-
單個日志文件例如大於 5M 后重新創建一個新的日志文件;
-
刪除超過 30 天的日志;
-
使用鎖確保多線程安全。
后期考慮實現功能:
- 日志的相關配置數據例如輸出目錄寫到配置文件;
- 日志可以選擇存放在服務器。
二、代碼實現
2.1 LogHandler.h
#ifndef LOGHANDLER_H
#define LOGHANDLER_H
#include <iostream>
#include <QDebug>
#include <QDateTime>
#include <QMutexLocker>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QTimer>
#include <QTextStream>
#include <QTextCodec>
const int g_logLimitSize = 5;
struct LogHandlerPrivate {
LogHandlerPrivate();
~LogHandlerPrivate();
// 打開日志文件 log.txt,如果日志文件不是當天創建的,則使用創建日期把其重命名為 yyyy-MM-dd.log,並重新創建一個 log.txt
void openAndBackupLogFile();
void checkLogFiles(); // 檢測當前日志文件大小
void autoDeleteLog(); // 自動刪除30天前的日志
// 消息處理函數
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg);
QDir logDir; // 日志文件夾
QTimer renameLogFileTimer; // 重命名日志文件使用的定時器
QTimer flushLogFileTimer; // 刷新輸出到日志文件的定時器
QDate logFileCreatedDate; // 日志文件創建的時間
static QFile *logFile; // 日志文件
static QTextStream *logOut; // 輸出日志的 QTextStream,使用靜態對象就是為了減少函數調用的開銷
static QMutex logMutex; // 同步使用的 mutex
};
class LogHandler {
public:
void installMessageHandler(); // 給Qt安裝消息處理函數
void uninstallMessageHandler(); // 取消安裝消息處理函數並釋放資源
static LogHandler& Get() {
static LogHandler m_logHandler;
return m_logHandler;
}
private:
LogHandler();
LogHandlerPrivate *d;
};
#endif // LOGHANDLER_H
2.2 LogHandler.cpp
#include "LogHandler.h"
/************************************************************************************************************
* *
* LogHandlerPrivate *
* *
***********************************************************************************************************/
// 初始化 static 變量
QMutex LogHandlerPrivate::logMutex;
QFile* LogHandlerPrivate::logFile = nullptr;
QTextStream* LogHandlerPrivate::logOut = nullptr;
LogHandlerPrivate::LogHandlerPrivate() {
logDir.setPath("log"); // TODO: 日志文件夾的路徑,為 exe 所在目錄下的 log 文件夾,可從配置文件讀取
QString logPath = logDir.absoluteFilePath("today.log"); // 獲取日志的路徑
// ========獲取日志文件創建的時間========
// QFileInfo::created(): On most Unix systems, this function returns the time of the last status change.
// 所以不能運行時使用這個函數檢查創建時間,因為會在運行時變化,於是在程序啟動時保存下日志文件的最后修改時間,
logFileCreatedDate = QFileInfo(logPath).lastModified().date(); // 若日志文件不存在,返回nullptr
// 打開日志文件,如果不是當天創建的,備份已有日志文件
openAndBackupLogFile();
// 十分鍾檢查一次日志文件創建時間
renameLogFileTimer.setInterval(1000 * 2); // TODO: 可從配置文件讀取
renameLogFileTimer.start();
QObject::connect(&renameLogFileTimer, &QTimer::timeout, [this] {
QMutexLocker locker(&LogHandlerPrivate::logMutex);
openAndBackupLogFile(); // 打開日志文件
checkLogFiles(); // 檢測當前日志文件大小
autoDeleteLog(); // 自動刪除30天前的日志
});
// 定時刷新日志輸出到文件,盡快的能在日志文件里看到最新的日志
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 文件
// 4. 檢查日志文件超過 LOGLIMIT_NUM 個,刪除最早的
// 備注:log.txt 始終為當天的日志文件,當第二天,會執行第3步,將使用 log.txt 的創建日期重命名它
// 如果日志所在目錄不存在,則創建
if (!logDir.exists()) {
logDir.mkpath("."); // 可以遞歸的創建文件夾
}
QString logPath = logDir.absoluteFilePath("today.log"); // log.txt的路徑
// [[1]] 程序每次啟動時 logFile 為 nullptr
if (logFile == nullptr) {
logFile = new QFile(logPath);
logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) ? new QTextStream(logFile) : nullptr;
if (logOut != nullptr)
logOut->setCodec("UTF-8");
// [[2]] 如果文件是第一次創建,則創建日期是無效的,把其設置為當前日期
if (logFileCreatedDate.isNull()) {
logFileCreatedDate = QDate::currentDate();
}
}
// [[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); // 刪除重新創建,改變創建時間
// 重新創建 log.txt
logFile = new QFile(logPath);
logOut = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) ? new QTextStream(logFile) : nullptr;
logFileCreatedDate = QDate::currentDate();
if (logOut != nullptr)
logOut->setCodec("UTF-8");
}
}
// 檢測當前日志文件大小
void LogHandlerPrivate::checkLogFiles() {
// 如果 protocal.log 文件大小超過5M,重新創建一個日志文件,原文件存檔為yyyy-MM-dd_hhmmss.log
if (logFile->size() > 1024*g_logLimitSize) {
logFile->flush();
logFile->close();
delete logOut;
delete logFile;
QString logPath = logDir.absoluteFilePath("today.log"); // 日志的路徑
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) : NULL;
logFileCreatedDate = QDate::currentDate();
if (logOut != nullptr)
logOut->setCodec("UTF-8");
}
}
// 自動刪除30天前的日志
void LogHandlerPrivate::autoDeleteLog()
{
QDateTime now = QDateTime::currentDateTime();
// 前30天
QDateTime dateTime1 = now.addDays(-30);
QDateTime dateTime2;
QString logPath = logDir.absoluteFilePath("today.log"); // 日志的路徑
QDir dir(logPath);
QFileInfoList fileList = dir.entryInfoList();
foreach (QFileInfo f, fileList ) {
// "."和".."跳過
if (f.baseName() == "")
continue;
dateTime2 = QDateTime::fromString(f.baseName(), "yyyy-MM-dd");
if (dateTime2 < dateTime1) { // 只要日志時間小於前30天的時間就刪除
dir.remove(f.absoluteFilePath());
}
}
}
// 消息處理函數
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) {
}
// 給Qt安裝消息處理函數
void LogHandler::installMessageHandler() {
QMutexLocker locker(&LogHandlerPrivate::logMutex); // 類似C++11的lock_guard,析構時自動解鎖
if (nullptr == d) {
d = new LogHandlerPrivate();
qInstallMessageHandler(LogHandlerPrivate::messageHandler); // 給 Qt 安裝自定義消息處理函數
}
}
// 取消安裝消息處理函數並釋放資源
void LogHandler::uninstallMessageHandler() {
QMutexLocker locker(&LogHandlerPrivate::logMutex);
qInstallMessageHandler(nullptr);
delete d;
d = nullptr;
}
2.3 main.cpp
#include "LogHandler.h"
#include <QApplication>
#include <QDebug>
#include <QTime>
#include <QPushButton>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// [[1]] 安裝消息處理函數
LogHandler::Get().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]] 取消安裝自定義消息處理,然后啟用
LogHandler::Get().uninstallMessageHandler();
qDebug() << "........"; // 不寫入日志
LogHandler::Get().installMessageHandler();
int ret = app.exec(); // 事件循環結束
// [[4]] 程序結束時釋放 LogHandler 的資源,例如刷新並關閉日志文件
LogHandler::Get().uninstallMessageHandler();
return ret;
}
如果想實現日志存放在服務器,可以參考:Qt 打印日志系統,實現打印日志按日期、大小保存,過期刪除,窗口實時顯示日志,網絡傳輸日志遠程調試
2.4 運行效果
控制台輸出:
Hello
當前時間是: "16:29:42"
"God bless you!"
........
退出
日志文件(exe 所在目錄的 log 目錄下的 log.txt):
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): 退出
注意:
Release 版本默認不包含文件名、函數名和行數信息,需要在 .pro 文件中加入一行代碼,重新 make 運行后生效。
DEFINES += QT_MESSAGELOGCONTEXT
三、其他 C++ 日志框架
C++ 中的日志框架有很多,其中比較著名的有:
-
log4cxx:Java 社區著名的 Log4j 的 C++ 移植版,用於為 C++ 程序提供日志功能,以便開發者對目標程序進行調試和審計。
-
log4cplus:一個簡單易用的 C++ 日志記錄 API,它提供了對日志管理和配置的線程安全、靈活和任意粒度控制(也基於 Log4j)。
-
Log4cpp:一個 C++ 類庫,可以靈活地記錄到文件、syslog、IDSA 和其他目的地(也基於 Log4j)。
-
google-glog:一個 C++ 語言的應用級日志記錄框架,提供了 C++ 風格的流操作和各種輔助宏。
-
Pantheios:一個類型安全、高效、泛型和可擴展性的 C++ 日志 API 庫(號稱 C++ 領域速度最快的日志庫)。
-
POCO:還提供了一個 好的日志支持文檔。
-
ACE:ACE 也有日志支持。
-
Boost.Log:設計的非常模塊化,並且可擴展。
-
Easylogging++:輕量級高性能 C++ 日志庫(只有一個頭文件)。
-
G3log:一個開源、支持跨平台的異步 C++ 日志框架,支持自定義日志格式。基於 g2log 構建,提升了性能,支持自定義格式。
-
Plog:可移植、簡單和可擴展的 C++ 日志庫。
-
spdlog:一個快速的 C++ 日志庫,只包含頭文件,兼容 C++11。
-
……
其中 log4cplus、glog 較為流行,一個是著名的 Log4j 的衍生品,另一個則是 Google 的“親兒子”。
包括 log4qt,也是 Log4j 的衍生品,可以參考:DevBean豆子大神的github
參考: