【Qt 学习之路】C++日志库 - 18种方法选型(读这一篇就够了)
文章目录
- 1、前言
- 2、归纳
- 3、典型
- 3.1、qInstallMessageHandler
- 3.1.1、简介
- 3.1.2、示例
- 3.1.3、思路
- 3.1.4、使用
- 3.1.5、案例
- 3.2、Glog
- 3.2.1、语法
- 3.2.2、下载
- 3.2.3、配置
- 3.2.4、使用
- 3.2.5、相关
- 3.3、Easylogging++
- 3.3.1、要领
- 3.3.2、兼容
- 3.3.3、注意
- 3.3.4、示例
1、前言
最近有时间研究下C++ 的记录日志的方法,包括各个方法的简述、使用特点、适用场景、推荐指数等。本文调研常见的 18 种 记录日志方法(包含:qInstallMessageHandler、Glog、Easylogging++、Qslog、Log4qt、Log4cpp、Log4cxx、Log4c、Log4cplus、Pantheios POCO、ACE、Boost.Log、G3log、Plog、spdlog、Rsyslog等),后续添加了笔者最推荐的 3种 典型方法的要领思路、使用方法和示例等,倾注了很多精力。希望此文不要沉底,请多多支持,谢谢~
2、归纳
序号 名称 简述使用特点 适用场景 官网 最新更新 推荐指数 1 qInstallMessageHandler Qt 亲儿子,可安装自定义的日志输出处理函数,把日志输出到文件,控制台等,可查看 Qt 的帮助文档。Release 版本默认不包含文件名、函数名和行数信息,需要在 .pro 文件中加入代码。 Qt5.0 及以上版本 跨平台开发场景 地址 随 Qt 不断升级 ⭐⭐⭐⭐⭐ 2 Glog 谷歌 亲儿子,一个 C++ 语言的应用级日志记录框架,提供了 C++ 风格的流操作和各种辅助宏 常常与Breakpad结合跨平台使用 地址 2022-04-05 ⭐⭐⭐⭐⭐ 3 Easylogging++ 巨简单。轻量级高性能 C++ 日志库,只需要一个头文件,无需任何外部依赖。支持文件配置,提供了强大的自定义日志格式的能力,还提供对第三方库,STL容器的支持 几乎所有主流平台和编译器都支持 地址 2023-07-20 ⭐⭐⭐⭐ 4 Log4cplus 一个基于 Log4j简单易用的 C++ 日志记录 API,提供了对日志管理和配置的线程安全、灵活和任意粒度控制 使用 C++ 跨平台开发场景 地址 2023-11-17 ⭐⭐⭐⭐ 5 Qslog 基于Qt的轻量级开源日志库,使用Qt的QDebug类的易于使用的记录器。QsLog是在麻省理工学院许可下以开源形式发布的 适用于Qt的多库集成场景 地址 2023-06-21 ⭐⭐⭐⭐ 6 Log4cpp LGPL的开源项目,是基于 Log4j的 C++ 类库,可以灵活地记录到文件、syslog、IDSA 和其他目的地 适用 C++ 跨平台开发场景 地址 2023-03-12 ⭐⭐⭐ 7 Pantheios 号称 C/C++ 领域速度最快的程序诊断日志库,一类型安全、高效、泛型和可扩展性的 C++ 日志 API 库 适用于对速度有特别追求的场景 地址 2020-07-05 ⭐⭐⭐ 8 Plog Plog 是一个 C++ 日志库,其设计尽可能简单、小巧且灵活,非常小。它是作为现有大型库的替代方案而创建的,并提供一些独特的功能,如CSV 日志格式和宽字符串支持 适用于对系统空间有要求的场景 地址 2023-08-20 ⭐⭐⭐ 9 Rsyslog Linux原生日志系统Rsyslog,可用于接受来自各种来源的输入,做处理后将结果输出到不同的目的地 适用于纯Linux场景 地址 2023-02-13 ⭐⭐⭐ 10 ACE 是一个很强大的跨平台的网络通信中间件,ACE日志输出的本身格式很类似log4cpp的日志格式 适用于中间件跨平台场景 地址 2023-12-14 ⭐⭐⭐ 11 spdlog 主打高性能与易用性。一个快速的 C++ 日志库,只包含头文件,是一个高速异步日志库,支持多线程和旋转文件日志 适用于高负载的系统 地址 2023-07-09 ⭐⭐⭐ 12 G3log 一个开源、支持跨平台的异步 C++ 日志框架,支持自定义日志格式。基于 g2log 构建,提升了性能,支持自定义格式 适用于对日志记录格式有特别要求的场景 地址 2023-12-07 ⭐⭐⭐ 13 Log4qt 用Trolltech Qt Framework的Apache Software Foundation Log4j包的C ++端口,它旨在供开源和商业Qt项目使用 适用于老版本Qt4 地址 2009-03-01 ⭐⭐ 14 Poco.Log 提供了好的日志支持文档。Poco库借助了C++库强大的网络和系统支持,易于集成到其他应用程序中,支持多个日志记录器和过滤器 适用于服务端和客户端的日志模块 地址 2023-12-05 ⭐⭐ 15 Log4c 基本上都是一些纯c的东西,移植性比Log4cxx、Log4cpp要好 使用 C 较多的场景 地址 2016-01-30 ⭐⭐ 16 log.c C 语言的日志功能模块,代码简洁,就一个 .c 和 .h 文件,一共 200 行,设计优雅 使用 C 较多的场景 地址 2016-01-30 ⭐⭐ 17 Boost.Log 由Boost库提供的日志记录工具,支持多个不同的后端日志器,可以定制多种记录格式 适用于任何Boost项目 地址 2023-12-14 ⭐ 18 Log4cxx Java 社区著名的 Log4j 的 C++ 移植版,用于为 C++ 程序提供日志功能,以便开发者对目标程序进行调试和审计,需要依赖于APR,不能和Qt的qDebug和qInfo等联调 适用于仅在Linux的Apache环境项目 地址 2023-05-01 ⭐ 3、典型
3.1、qInstallMessageHandler
3.1.1、简介
qInstallMessageHandler 位于 - Global Qt Declarations下边,属于全局函数。
QtMessageHandler qInstallMessageHandler(QtMessageHandler handler)
qInstallMessageHandler 的作用 主要是是对 打印消息的控制,能控制打印的输出,调试,信息,警告,严重,致命错误等五个等级。
Installs a Qt message handler which has been defined previously. Returns a pointer to the previous message handler.
The message handler is a function that prints out debug messages, warnings, critical and fatal error messages. The Qt library (debug mode) contains hundreds of warning messages that are printed when internal errors (usually invalid function arguments) occur. Qt built in release mode also contains such warnings unless QT_NO_WARNING_OUTPUT and/or QT_NO_DEBUG_OUTPUT have been set during compilation. If you implement your own message handler, you get total control of these messages.
The default message handler prints the message to the standard output under X11 or to the debugger under Windows. If it is a fatal message, the application aborts immediately.
Only one message handler can be defined, since this is usually done on an application-wide basis to control debug output.
To restore the message handler, call qInstallMessageHandler(0).
3.1.2、示例
官方示例
#include #include #include void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { QByteArray localMsg = msg.toLocal8Bit(); switch (type) { case QtDebugMsg: fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; case QtInfoMsg: fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; case QtWarningMsg: fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; case QtCriticalMsg: fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; case QtFatalMsg: fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); abort(); } } int main(int argc, char **argv) { qInstallMessageHandler(myMessageOutput); QApplication app(argc, argv); ... return app.exec(); }
3.1.3、思路
- 读取日志配置文件,设置文件输出等级,可以用做在正式项目中调试与日常关键信息打印。
- Log消息输出方法–输出文本。
- 消息模块注册
3.1.4、使用
枚举变量–设置日志等级 0~4 5个等级
enum { Fatal = 0, Critical, Warning, Info, Debug } LogLeaver;
int LogType = 4; //日志等级初始化设置 //初始化读取配置文件 void ReadLogInit() { QFile file ("./log_conf.ini"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)){//判断文件是否可执行 return; } while (!file.atEnd()) { QByteArray strBuf = file.readLine(); if(strBuf == "[LOG_CONFIG]\n") { strBuf = file.readLine(); LogType = strBuf.mid(strBuf.size()-1,1).toInt(); } } } //配置文件格式 [LOG_CONFIG] LogLeaver=4
void message_output(QtMsgType type,const QMessageLogContext &context,const QString &msg) { //加锁:避免对文件的同时读写 static QMutex mutex; mutex.lock(); //读写消息 QByteArray localMsg = msg.toLocal8Bit(); //输出的字符串 QString strOutStream = ""; //case 生成要求格式日志文件,加日志等级过滤 switch (type) { case QtDebugMsg: if(LogType == Debug) { strOutStream = QString("%1 %2 %3 %4 [Debug] %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg)); } break; case QtInfoMsg: if(LogType >= Info) { strOutStream = QString("%1 %2 %3 %4 [Info]: %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg)); } break; case QtWarningMsg: if(LogType >= Warning) { strOutStream = QString("%1 %2 %3 %4 [Warning]: %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg)); } break; case QtCriticalMsg: if(LogType >= Critical) { strOutStream = QString("%1 %2 %3 %4 [Critical]: %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg)); } break; case QtFatalMsg: if(LogType >= Fatal) { strOutStream = QString("%1 %2 %3 %4 [Fatal]: %5 \n").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(QString(context.file)).arg(context.line).arg(QString(context.function)).arg(QString(localMsg)); } abort(); } //每天生成一个新的log日志文件,文件名 yyyyMMdd.txt QString strFileName = QString("%1.txt").arg(QDateTime::currentDateTime().date().toString("yyyyMMdd")); QFile logfile(strFileName); logfile.open(QIODevice::WriteOnly | QIODevice::Append); if(strOutStream != "") { QTextStream logStream(&logfile); logStream ReadLogInit();//读取日志等级 qInstallMessageHandler(message_output);//安装消息处理函数,依靠回调函数,重定向,全局处理 QApplication a(argc, argv); qInfo() 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 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() 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 fromUnicode(msg); //msg.toLocal8Bit(); #else QByteArray localMsg = msg.toLocal8Bit(); #endif std::cout return; } // 输出到日志文件, 格式: 时间 - [Level] (文件名:行数, 函数): 消息 QString fileName = context.file; int index = fileName.lastIndexOf(QDir::separator()); fileName = fileName.mid(index + 1); (*LogHandlerPrivate::logOut) } // 给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; } QApplication app(argc, argv); // [[1]] 安装消息处理函数 LogHandler::Get().installMessageHandler(); // [[2]] 输出测试,查看是否写入到文件 qDebug() qDebug() google::INFO = 0, google::WARNING = 1, google::ERROR = 2, google::FATAL = 3, }; FLAGS_logtostderr = true; FLAGS_colorlogtostderr = true;//是否启用不同颜色显示(如果终端支持) google::InitGoogleLogging(argv[0]);//使用glog之前必须先初始化库,仅需执行一次,括号内为程序名 //google::SetLogDestination(google::GLOG_INFO, "E:\\logs\\INFO_");//INFO级别的日志都存放到logs目录下且前缀为INFO_ LOG(INFO)