c++日志框架

日志框架:一个经过专门设计的实用程序,用于规范应用程序的日志记录过程。日志框架可以自己编写(技术要牛才行哦),也可以由第三方(例如:log4cplus)提供。对于不同的日志框架,各自的组织在实现方式上也有所不同。

为什么使用日志框架

何为日志框架

虽然可以简单地“标准化”日志(例如:调用文件系统 API,将信息写入名为 log.txt 的文件),但是要成为一个严格意义上的框架,必须要超越标准化。也就是说,日志框架必须通过处理日志记录来标准化解决方案,从而暴露一个标准的 API。

没明白?那就再具体一些,设想一个日志框架,封装了三个主要部分:

20220901113006

当想要捕获程序的运行时信息时,首先要发出要记录的信息。然后,格式化这些信息。最后,决定将它输出到哪里。一般情况下,会输出到文件中,但是也可以将其输出到控制台、数据库,或者任何能够接收数据的地方。

如果有一系列代码,能够解决这些问题,那么就可以被看作是一个日志框架。

为什么不是cout

使用日志,只为成为更好的攻城狮。

也许有人会问:既然 C++ 中有 cout,为什么还要使用日志呢?

无法否认,在使用像 C++、Java、PHP 这样的编程语言时,我们会经常将消息打印到控制台,因为这是开发、测试和调试程序的一部分。但倘若我们正在处理一个服务端程序,却无法看到其内部发生了什么,这时该怎么办?唯一的可见性工具是日志文件,如果没有日志,我们就不能进行任何调试,也无法知道程序内部在做什么。

尽管 C++ 中有相当方便的 cout 输出流,可以在控制台上打印一些信息,或者可以通过其他方式将这些信息重定向到文件中,但这对于实际的应用程序来说根本不够。尤其对于复杂的 C++ 程序来说,像 log4cplus 或任何其他日志框架能够提供了更多的灵活性,而这是 cout 不可能完成的。

在编写代码时,使用日志框架是一种很好的实践。即使像《代码整洁之道》这样的书籍,也建议学习像 Log4j 这样的框架进行日志记录。所以,请尽可能的在生产代码中使用日志,而不是用 cout 来打印东西(这是不可接受的)。

诊断工具

绝大多数人都曾面临这样的困境——一旦程序出现问题,很长时间都找不出原因!

缺少日志,我们将不得不依赖于客户或支持团队,让他们描述在什么情况下发生了什么(很可能会存在一些误导)。随后我们需要通过开发环境重现问题,并进行各种调试,直至错误修复为止,然而这一般会耗费很长时间。但若有日志的帮助,我们便能迅速摆脱这种困境,可以很快地发现异常,并快速定位、解决问题!

检测模块的瓶颈

随着项目规模的增加,模块会越来越多,调优也变成了一场持久战。

通过记录某些操作的日期和时间,我们可以及时地检测模块的瓶颈,并针对性地对一些耗时操作做出优化。

了解用户行为

为了提高产品质量,提供个性化服务,就必须了解用户行为——他们做了什么,想要什么。

要搞清楚这些,当然要有数据,所以需要采集和分析用户的行为,而日志无疑是最主要的数据来源。

不要重新发明轮子

不要去重新发明轮子——《麦肯锡方法》

20220901113345

既然已经对日志框架有了明确的了解,那么应该使用现有的日志框架,还是构建自己的日志框架呢?其实,这是一个老生常谈的问题了——要不要重新发明轮子?引用莎士比亚戏剧《哈姆雷特》中的一句名言:

To be or not to be - that is the question.

现在我们正处于技术大爆发的时代,每一个技术领域中都有很多好的解决方案。因此,自己完全不需要、也不应该去写日志框架。就像不应该写版本控制工具或 Bug 跟踪管理工具一样,其他人已经把这些东西搞出来了,而且搞的很好,Git、SVN,Bugzilla、Trac……应有尽有,我们完全可以花很少的钱,甚至是免费使用它们。

所以,请避免重复劳动,不要去重新发明轮子。应该坚持在自己的领域解决问题,将时间花在刀刃上。话虽如此,但我们还是需要了解关于轮子的一些细节(谁造的?怎么创造的?如何使用?),以从中接受灵感,并把自己的知识加进去。

因此,当需要一个日志框架时,应该使用现有的框架,而不是去重新发明轮子。那么,问题来了,日志框架都有哪些呢?

有哪些

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。

这么多框架,应该选择哪一个呢?

由于每个人的需求和技术栈都不一样,所以很难直接回答这个问题,但是有一些选择标准可供参考。

易用性

易于使用的框架,能让你事半功倍。

易用性,是优秀框架的一个重要特性。所以无论使用什么框架,都应优先考虑这一点。

对于日志框架而言,一旦被选定,在后期开发过程中,项目组中的每个人都会频繁使用。如果易用性不好的话,那绝对是一个噩梦!所以,在选择日志框架时,应尽量找那些由简单、直观的 API 实现的方案。一个简单日志框架,衡量标准可参考:对于缺乏经验的团队成员,应该在查看文档和简单示例之后,就能够快速上手。

性能(效率)

功能决定现在,性能决定未来。

另一个要考虑的重要因素是性能影响。正如上面提到的,我们会经常调用日志,这可能会对程序的性能产生巨大的影响。

想象一下,一个日志框架,需要花费很长的时间才能启动,在每次调用时阻塞并执行文件 I/O,缺少缓冲机制……对于这样的框架,你会用吗?

因此,在评估日志框架时,可以参考网上的一些文章来比较,也可以自己做一些比较性实验(基准测试),例如:吞吐量——测量每秒可以完成多少次方法调用(越高越好);采样——测量每次调用执行究竟花费了多少时间(越低越好)。

对现有代码的影响

影响越小,越容易维护。

在以后的使用中,日志将无处不在。就像会影响运行时性能一样,它们也会影响源码的可维护性。

从应用程序的角度来看,日志所处的位置比较尴尬。之所以这么说,是因为我们尽力隔离依赖性,提供良好的接口,并最小化耦合,在编程时考虑的是单一职责原则。然后日志出现了,它到处都是,与周围的代码无关。与这些优秀的设计原则相比,日志显得有些背道而驰。

社区支持

每一个成功框架的背后,都有一个伟大的社区。

框架的生命力源于不断地完善和发展,如果没有强大的社区做支撑,这个框架便失去了源动力。

因此,在选择框架时,这一点非常重要——所选的框架是否有专门的团队做支撑?在像 Stack Overflow 这样的问答网站上,它是否有很强的存在感?确保一点,如果在使用过程中遇到了问题,你能有办法快速解决。倘若选择了一个不知名的框架,当遇到了 Bug 时,那么可能会浪费大量的时间来解决问题。

完整性

完整的框架,铸就完美的生产力。

在最前面,我们将日志的功能分为三个主要部分:日志记录、格式化和输出地,所以要确保所选的日志框架彻底解决了这些问题。

日志记录和输出地比较基础,几乎所有日志框架都有这些概念。话虽如此,但对于好的框架来说,应该巧妙地将日志记录与输出地分开,并且还应该有多种可选的输出地。在理想情况下,最好能够自定义输出地。

一般情况下,格式化日志文件会整齐地排列,并具有很好的可读性。但在 DevOps 的世界里,这些远远不够。具体来说,日志文件需要被格式化为可解析的数据。通过将日志输出作为数据处理,可以很容易地聚合、搜索和可视化日志,从而能够在生产支持方面助你一臂之力。所以,要确保日志框架拥有这种能力。

发展前景

只有前途光明,方能大行其道。

无法绕过这一点——在选择日志框架时,不仅要考虑它的现状,还应该注重它的发展前景。

像上面提到的 C++ 日志框架,每一个都非常优秀且特点鲜明。但有一些却获得了更多的关注度,例如 log4cplus、glog,为什么如此呢?因为它们有很强大的“基因”和“后台”,一个是著名的 Log4j 的衍生品,另一个则是 Google 的“亲儿子”。

一个框架的发展前景,取决于众多因素——关注度、用户基数、社区活跃度……要想大行其道,这些几乎都不能少。

为什么选择 Log4Qt

对于很多人来说,第一选择会是 Log4cpp、log4cplus、log4cxx、Log4Qt 中的一个,因为它们均移植自 Java 中著名的日志框架——Log4j,并且保持了 API 上的一致。

20220901114357

通过使用 Log4j,我们可以:

  • 控制日志的输出格式;
  • 通过定义日志信息的级别,来更加细致地控制日志的生成过程;
  • 控制日志信息的输出位置,例如:文件、控制台、数据库等;
  • ……

最不可思议的是,这些都可以通过配置文件来灵活地控制,而无需修改源代码。

使用 Log4j 系列的最大好处在于,只要熟练掌握其中一个,其它的几个都可以信手拈来。即使是不同的语言环境,配置也不会有太大区别,并且使用方式也相当简单。

衍生品这么多,其实选择哪个都无所谓,因为它们是一家,而我们主要是学习 Log4j 的思想。

Log4Qt简介

Log4Qt 是 Apache Log4j 的 Qt 移植版,主要用于记录日志。

由于 Log4Qt 是基于 Qt 编写的,所以它也继承了 Qt 的跨平台特性。也就是说,可以将其用于 Windows、Linux、以及 MacOS 平台上。

由于 Log4Qt 的开发在 2009 年就终止了,所以其官网提供的源码仅支持 Qt4:

但值得庆祝的是,有人提供了一个兼容 Qt5 的版本:

for Qt5:点击这里下载

这个升级版很棒,不但可以将 Log4Qt 源码添加至项目中,而且还可以将其编译为库,并且它还同时支持 CMake 和 qmake。

最重要的是,它还在持续升级,并且在老版本(for Qt4)的基础上添加了很多新 Feature。

Log4Qt 分层架构

Log4Qt API 设计为分层结构,其中每一层提供了执行不同任务的不同对象,这种设计为未来的发展提供了很好的可扩展性。

Log4Qt 对象分为:

  • 核心对象:使用框架必不可少的强制性对象。
  • 支持对象:帮助核心对象完成重要的任务。

20220901114809

核心对象:

  • Logger 对象:处于最上层,负责捕获日志信息。
  • Appender 对象:负责将日志信息输出到各种目的地,例如:控制台、文件、数据库等。
  • Layout 对象:用于控制日志输出格式,该层有助于以可读形式记录信息。

支持对象:

  • Level 对象:定义日志信息的优先级:TRACE < DEBUG < INFO < WARN < ERROR < FATAL。
  • LogManager:负责从配置文件或配置类中读取初始配置参数。
  • Filter 对象:用于分析日志信息,并进一步决定是否需要记录信息。
  • ObjectRenderer:用于向传递的各种 Logger 对象提供字符串表示(在 Log4Qt 中,尚未用到)。

使用配置文件的方式配置Log4Qt

Log4qt的功能强大,使用配置也非常简单。只需要编写简单的配置文件就可以完成个性化log输出的配置。在网上可以找到好多log4j的配置文件。这里要强调一点,虽然Log4qt是Log4j的分支,但是由于平台的不同,Log4qt不完全支持log4j的配置文件。

在log4qt/helper目录下创建一个“log4qt_init_helper_by_config.h“文件,内容如下(简化代码):

#ifndef LOG4QT_HELPER_BY_CONFIG_H
#define LOG4QT_HELPER_BY_CONFIG_H
//启动log4qt库,使用配置文件的方式配置log4qt
extern void SetupLog4QtByConfigWithConfigFileAbsPath(QString config_file_abs_path);
extern void ShutDownLog4QtByConfig();//关闭log4qt库
#endif // LOG4QT_HELPER_H

在log4qt/helper目录下创建一个”log4qt_init_helper_by_config.cpp“文件。内容如下(简化代码):

#include "log4qt_init_helper_by_config.h"
void SetupLog4QtByConfigWithConfigFileAbsPath(QString config_file_abs_path)
{
if (QFile::exists(config_file_abs_path)) {
Log4Qt::PropertyConfigurator::configure(config_file_abs_path);
} else {
qDebug() << "Can't find log4qt-config-file path:" << config_file_abs_path;
}
}
void ShutDownLog4QtByConfig()
{
auto logger = Log4Qt::Logger::rootLogger();
logger->removeAllAppenders();
logger->loggerRepository()->shutdown();
}

以上编写好了加载log4qt配置文件的代码,下面正式介绍配置文件的编写。配置文件如下,相关配置的说明直接以注视的形式写在配置文件中,配置文件“log4qt.properties”如下所示:

#设置储存log文件的根目录
logpath=.

log4j.reset=true
log4j.Debug=WARN
log4j.threshold=NULL
#设置是否监听QDebug输出的字符串
log4j.handleQtMessages=true
#在运行中,是否监视此文件配置的变化
log4j.watchThisFile=false

#设置根Logger的输出log等级为All
#设置Log输出的几种输出源(appender):console, daily, rolling
log4j.rootLogger=ALL, console, daily

#设置终端打印记录器
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=STDOUT_TARGET
log4j.appender.console.layout=org.apache.log4j.TTCCLayout
log4j.appender.console.layout.dateFormat=dd.MM.yyyy hh:mm:ss.zzz
log4j.appender.console.layout.contextPrinting=true
log4j.appender.console.threshold=ALL

#设置一个每日储存一个log文件的记录器
log4j.appender.daily=org.apache.log4j.DailyFileAppender
log4j.appender.daily.file=${logpath}/propertyconfigurator.log
log4j.appender.daily.appendFile=true
log4j.appender.daily.datePattern=_yyyy_MM_dd
log4j.appender.daily.keepDays=90
log4j.appender.daily.layout=${log4j.appender.console.layout}
log4j.appender.daily.layout.dateFormat=${log4j.appender.console.layout.dateFormat}
log4j.appender.daily.layout.contextPrinting=${log4j.appender.console.layout.contextPrinting}

# 配置一个滚动文件记录器
log4j.appender.rolling=org.apache.log4j.RollingFileAppender
log4j.appender.rolling.file= ${logpath}/propertyconfigurator_rolling.log
log4j.appender.rolling.appendFile=true
log4j.appender.rolling.maxFileSize= 20MB
log4j.appender.rolling.maxBackupIndex= 10
log4j.appender.rolling.layout=${log4j.appender.console.layout}
log4j.appender.rolling.layout.dateFormat=${log4j.appender.console.layout.dateFormat}
log4j.appender.rolling.layout.contextPrinting=${log4j.appender.console.layout.contextPrinting}

# 给“LoggerObjectPrio”这个类的Logger定义log输出等级为Error,
# 给“LoggerObjectPrio”这个类的Logger定义log输出源:daily, console
log4j.logger.LoggerObjectPrio=ERROR, rolling
#设置为false,表示“LoggerObjectPrio”这个类的logger不继承的rootLogger输出源(appender)
log4j.additivity.LoggerObjectPrio=false

至此,使用配置文件配置log4qt的方式就完成了,在主要数分别调用,启动函数和停止函数即可。需要说明的一点是,一般这个配置文件储存在软件主体所在的目录。log文件储存的位置由配置文件配置。

使用代码的方式配置Log4Qt

另一种为使用代码配置log4qt的方法。使用代码的配置方法的优点是,比配置文件更加灵活且可定制性强。比如,在配置log文件储存位置的时候。使用配置文件只能在软件运行前配置好。而使用代码配置的时候,可以在软件启动后,获取一个动态的目录作为log文件的储存位置。当然各有优点,根据你的实际需求,选择其中任意一种配置方法都可以。

在log4qt/helper目录下创建一个“log4qt_init_helper_by_coding.h“文件,内容如下(简化代码):

#ifndef LOG4QT_HELPER_BY_CODING_H
#define LOG4QT_HELPER_BY_CODING_H

//layouts
#include "log4qt/ttcclayout.h"
//appenders
#include "log4qt/consoleappender.h"
#include "log4qt/rollingfileappender.h"

extern void SetupLog4QtByCodingWithLogSavingDirAbsPath(QString log_saving_dir_abs_path);
extern void ShutDownLog4QtByCoding();
#endif // LOG4QT_HELPER_H

在log4qt/helper目录下创建一个“log4qt_init_helper_by_coding.cpp“文件,内容如下(简化代码):

#include "log4qt_init_helper_by_coding.h"

void SetupLog4QtByCodingWithLogSavingDirAbsPath(QString log_saving_dir_abs_path)
{
QString absPath = log_saving_dir_abs_path;
auto rootLogger = Log4Qt::Logger::rootLogger();

// Create a layout
auto *layout = new Log4Qt::TTCCLayout();
layout->setName(QStringLiteral("My Layout"));
layout->setDateFormat("dd.MM.yyyy hh:mm:ss.zzz");
layout->activateOptions();

// Create a console appender
Log4Qt::ConsoleAppender *consoleAppender =
new Log4Qt::ConsoleAppender(layout, Log4Qt::ConsoleAppender::STDOUT_TARGET);
consoleAppender->setName(QStringLiteral("My console Appender"));
consoleAppender->activateOptions();
rootLogger->addAppender(consoleAppender);

//Create a rolling file appender
Log4Qt::RollingFileAppender *rollingFileAppender =
new Log4Qt::RollingFileAppender(layout, absPath + "/basic.log", true);
rollingFileAppender->setName(QStringLiteral("My rolling file appender"));
//default is 10 MB (10 * 1024 * 1024).
rollingFileAppender->setMaximumFileSize(20 * 1024 * 1024);
rollingFileAppender->setThreshold(Log4Qt::Level::Value::INFO_INT);//设置子输出等级过滤
rollingFileAppender->activateOptions();
rootLogger->addAppender(rollingFileAppender);

//设置根logger允许所以等级的消息被输出(子输出过滤是在根输出过滤的基础上)
rootLogger->setLevel(Log4Qt::Level::ALL_INT);
Log4Qt::LogManager::setHandleQtMessages(true);//设置监听qt自带的log输出
}

void ShutDownLog4QtByCoding()
{
auto logger = Log4Qt::Logger::rootLogger();
logger->removeAllAppenders();
logger->loggerRepository()->shutdown();
}

在main函数中加入如下内容

#include "ui/demodialog.h"
#include <QApplication>
#include <QStandardPaths>
#include <QDir>

#include "log4qt_init_helper_by_coding.h"
#include "log4qt/logger.h" //每个使用log4qt的类都需要包含此头文件

//在类的cpp文件中,使用此静态方法声明logger(此方法比较通用)
//第二个参数写类名字,因此,输出的log条目中包含其对应的类名
LOG4QT_DECLARE_STATIC_LOGGER(logger, Main)

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

//使用QStandardPaths获取标准个人程序数据储存目录,需要先设置以下名称
QCoreApplication::setApplicationName("Log4qtDemo");
QCoreApplication::setApplicationVersion("0.0.1");
QCoreApplication::setOrganizationName("OrgName");
QCoreApplication::setOrganizationDomain("name.org");

/*
//使用config 配置Log4Qt
//把源代码目录中的"log4qt.properties"文件复制到编译好的可执行文件所在的目录
//QString configFileAbsPath = QCoreApplication::applicationFilePath() + QStringLiteral(".log4qt.properties");//配置文件包括应用程序名称
QString configFileAbsPath = QCoreApplication::applicationDirPath() +"/"+ QStringLiteral("log4qt.properties");//配置文件不包括应用程序名称
SetupLog4QtByConfigWithConfigFileAbsPath(configFileAbsPath);
*/

//使用纯代码配置Log4Qt
QStringList path_list2 = QStandardPaths::standardLocations(QStandardPaths::DataLocation);
QString std_base_path = path_list2[0];
QString my_log_path = std_base_path + "/logs";
// QString logSavingDirAbsPath = QCoreApplication::applicationDirPath();
qDebug() << "my_log_path = " << my_log_path;
SetupLog4QtByCodingWithLogSavingDirAbsPath(my_log_path);

//可以使用以下三种方式编写Log输出条目
//1.log4qt基本的logger输出
logger()->debug() << "example ####11##### logger()->debug()";
logger()->error() << "example ####11##### logger()->error()";

//2.log4qt基本的宏定义输出
l4qError() << "example ####22##### l4qError() ";
l4qError(QStringLiteral("example ####22##### l4qError() %1"), 10);

//3.使用qt平台的Log库输出,(Log4Qt会监听qt的log的输出,并统一输出到Log文件中)
qDebug() << "example ####33##### qDebug()\n\n\n";

DemoDialog w;
w.show();
int ret = a.exec();

//ShutDownLog4QtByConfig();//exec()执行完成后,才关闭logger
ShutDownLog4QtByCoding();//exec()执行完成后,才关闭logger
return ret;
}