跳到主要内容

Boost.Log、zlog

阅读量

0

阅读人次

0

Boost.Log

动机

如今,应用程序迅速增长,变得复杂且难以测试和调试。在大多数情况下,应用程序都在远程站点上运行,一旦程序出错,开发人员几乎没有机会监视其执行并找出失败的原因。此外,如果应用程序的行为严重依赖于异步附带事件(例如设备反馈或其他进程活动),那么即使是本地调试也可能会出现问题。

这是日志记录可以提供帮助的地方。该应用程序将有关其执行的所有基本信息存储到日志中,当出现问题时,该信息可用于分析程序行为并进行必要的更正。日志记录还有其他非常有用的应用程序,例如收集统计信息和突出显示事件(即表明已发生某种情况或该应用程序遇到了一些问题)。实践证明,这些任务对于许多实际工业应用而言至关重要。

该库旨在使应用程序开发人员的日志记录变得更加容易。它提供了各种现成的工具以及用于扩展库的公共接口。该库的主要目标是:

  • 简单,一个小的示例代码片段应足以使人感觉到该库并准备使用其基本功能。
  • 可扩展性,用户应该能够扩展该库的功能,以将信息收集和存储到日志中。
  • 性能,该库对用户应用程序的性能影响应尽可能小。

如何阅读文档

本文档面向新手和有经验的库用户。但是,希望用户熟悉常用的Boost组件,例如shared_ptrmake_shared(请参阅Boost.SmartPtr)和function(Boost.Function)。根据需要,文档的某些部分将引用其他Boost库。

如果您是第一次使用该库,建议您先阅读“设计概述”部分,以便查看该库的功能和体系结构。 “安装和教程”部分将帮助您开始尝试该库。本教程通过示例代码片段概述了库的功能。一些教程步骤以两种形式呈现:简单和高级。简单表单通常描述完成任务的最常见和最简单的方法,建议新用户阅读它。高级形式通常提供扩展的方式来执行相同的操作,但具有深入的解释和进行一些额外的自定义的能力。该表格对于经验丰富的用户可能会派上用场,如果简单的方法不能满足您的需求,则通常应该阅读该表单。

除本教程外,还有详细功能描述章节。本部分描述了库提供的其他工具,但本教程未涵盖。最好根据具体情况阅读本章。

最后但并非最不重要的一点是,有一个参考资料给出了库组件的正式描述。

为了使本文档中的代码段简单,假设定义了以下名称空间别名:

namespace logging = boost::log;
namespace sinks = boost::log::sinks;
namespace src = boost::log::sources;
namespace expr = boost::log::expressions;
namespace attrs = boost::log::attributes;
namespace keywords = boost::log::keywords;

安装和兼容性

支持的编译器和平台

配置和构建库

定义

以下是在整个文档中将广泛使用的一些术语的定义:

  • 日志记录 从用户的应用程序中收集的一个信息,可以将其放入日志中。在一个简单的情况下,日志记录在由日志库处理后将在日志文件中表示为一行文本。

  • 属性

    “属性”是一条元信息,可用于专门化日志记录。在Boost.Log中,属性由具有特定接口的函数对象表示,该函数对象在调用时返回实际的属性值。

  • 属性值 属性值是从属性获取的实际数据。该数据将附加到特定的日志记录,并由库进行处理。值可以具有不同的类型(整数,字符串和更复杂的类型,包括用户定义的类型)。属性值的一些示例:当前时间戳值,文件名,行号,当前作用域名称等。属性值封装在类型擦除包装器中,因此属性的实际类型在接口中不可见。值的实际(擦除)类型有时称为存储类型。

  • (属性)值访问 一种处理属性值的方法。此方法涉及应用于属性值的函数对象(访问者)。访问者应该知道属性值的存储类型,以便对其进行处理。

  • (属性)值提取 一种在调用方尝试获取对存储值的引用时处理属性值的方法。调用者应该知道属性值的存储类型,以便能够提取它。

  • 日志接收器(sink) 从用户的应用程序中收集所有日志记录后将其馈送到的目标。接收器定义了将在何处以及如何存储或处理日志记录。

  • 日志来源(source) 用户应用程序将日志记录放入的入口点。在一个简单的情况下,它是一个对象(记录器logger),它维护一组属性,这些属性将用于应用户的请求形成日志记录。但是,可以肯定地创建一个源,该源将在某些附带事件上发出日志记录(例如,通过截取和解析另一个应用程序的控制台输出)。

  • 日志过滤器 谓词,它接受日志记录并告诉该记录是应通过还是应舍弃。谓词通常基于记录附带的属性值来形成其决策。

  • 日志格式化器(formatter) 从日志记录生成最终文本输出的函数对象。一些日志接收器(sink),例如二进制记录接收器可能不需要,但几乎所有基于文本的接收器都将使用格式化程序来组合其输出。

  • 测试核心 维护源和接收器之间的连接并将过滤器应用于记录的全局实体。它主要在初始化日志库时使用。

  • i18n 国际化。操纵宽字符的能力。

  • TLS 线程本地存储。具有一个变量的概念,该变量对于每个尝试访问它的线程都具有独立的值。

  • RTTI 运行时类型信息。这是dynamic_cast和typeid运算符正常运行所需的C ++语言支持数据结构。

设计概述

Boost.Log的设计具有很高的模块化和可扩展性。 它支持窄字符和宽字符日志记录。 窄字符记录器和宽字符记录器都提供类似的功能,因此在大多数文档中,仅会描述窄字符接口。

该库由三个主要层组成:日志数据收集层,处理收集的数据层以及将前两层互连的中央集线器。 设计如下图所示。

箭头显示了日志记录信息流的方向-从应用程序的左侧部分到最后的存储(如果有)右侧。该存储是可选的,因为日志处理的结果可能包含一些操作,而实际上并未将信息存储在任何地方。例如,如果您的应用程序处于紧急状态,它可以发出一条特殊的日志记录,该记录将被处理,以便用户在系统任务栏上的应用程序图标上看到一条错误消息,作为工具提示通知,并听到警报声。 这是一个非常重要的库功能:它与收集,处理日志数据以及实际上由什么组成数据日志记录正交。这不仅允许将该库用于经典日志记录,还可以向应用程序用户指示一些重要事件并累积统计数据。

  • 记录源

    回到图中,您的应用程序在记录器的帮助下在左侧发出日志记录-特殊对象,这些对象提供流以格式化最终将被记录到日志中的消息。该库提供了许多不同的记录器类型,您可以自己制作更多的记录器,从而扩展现有记录器。记录器设计为多种不同功能的组合,可以以任何组合方式相互组合。您可以简单地开发自己的功能并将其添加到库中。您将能够像使用其他记录器一样使用构造的记录器-将其嵌入到您的应用程序类中,或者创建并使用记录器的全局实例。两种方法都有其好处。将记录器嵌入某个类提供了一种区分日志和该类的不同实例的方法。另一方面,在函数式编程中,通常更方便的做法是在某个位置放置一个全局记录器并对其进行简单访问。

    一般来说,该库不需要使用记录器来写入日志。更为通用的术语“日志源”表示通过构造日志记录来启动日志记录的实体。其他日志源可能包括子应用程序的捕获控制台输出或从网络接收的数据。但是,记录器是最常见的日志源。

  • 属性和属性值

    为了启动日志记录,日志源必须将与日志记录关联的所有数据传递到日志记录核心。该数据或更准确地说是数据采集的逻辑用一组命名的属性表示。基本上,每个属性都是一个函数,其结果称为“属性值”,并且实际上会在进一步的阶段进行处理。属性的一个示例是返回当前时间的函数。其返回值-特定时间点-是属性值。

    有三种属性集:

    • 全局
    • 特定于线程
    • 特定于日志源

    您可以在图中看到,前两个集合由日志记录核心维护,因此无需通过日志源传递即可启动日志记录。参与全局属性集的属性将附加到曾经创建的任何日志记录中。显然,特定于线程的属性仅附加到由在集合中注册它们的线程所创建的记录。特定于源的属性集由启动日志记录的源维护,这些属性仅附加到通过该特定源进行的记录上。

    当源启动日志记录时,将从所有三个属性集的属性中获取属性值。然后,这些属性值形成一组命名的属性值,并对其进行进一步处理。您可以向集合添加更多属性值;这些值将仅附加到特定的日志记录,并且不会与日志记录源或日志记录核心关联。您可能会注意到,同名属性可能出现在多个属性集中。此类冲突是基于优先级解决的:全局属性优先级最低,特定于源的属性最高;发生冲突时,将低优先级属性从考虑中丢弃。

  • 记录核心和过滤

    组成一组属性值后,日志记录核心将决定是否将在接收器中处理此日志记录。这称为过滤。有两层过滤可用:全局过滤首先在日志核心内部应用,并允许快速擦除不需要的日志记录;然后针对每个接收器分别应用接收器特定的过滤。特定于接收器的过滤允许将日志记录定向到特定接收器。请注意,此时并不重要的是哪个日志记录源发出了记录,过滤仅依赖于附加到记录的属性值集。

    必须提到的是,对于给定的日志记录(log record),过滤仅执行一次。显然,只有在过滤开始之前附加到记录的那些属性值才能参与过滤。过滤完成后,通常会将某些属性值(例如日志记录消息)附加到记录中。这样的值不能在过滤器中使用,它们只能由格式化程序和接收器自己使用。

  • 接收器和格式化

    如果日志记录通过了至少一个接收器的过滤,则认为该记录是可消耗的。如果接收器支持格式化输出,那么这就是进行日志消息格式化的时候。格式化的消息以及组成的一组属性值将传递到接受记录的接收器。请注意,格式化是针对每个接收器执行的,因此每个接收器都可以以其自己的特定格式输出日志记录。

    您可能已经在上图中注意到,接收器由两部分组成:前端和后端。进行此划分是为了将接收器的常用功能(例如过滤,格式化和线程同步)提取到单独的实体(前端)中。库提供了接收器前端,最有可能的用户将不必重新实现它们。另一方面,后端是扩展库的最可能的地方之一。是接收器后端执行日志记录的实际处理。可以有一个将日志记录存储到文件中的接收器。可能有一个接收器,通过网络将日志记录发送到远程日志处理节点;可能存在前面提到的接收器,它将记录消息放入工具提示通知中-您可以为它命名。库已经提供了最常用的接收器后端。

    除了上述主要功能外,该库还提供了各种各样的辅助工具,例如属性,对格式化程序和过滤器的支持(表示为lambda表达式),甚至是库初始化的基本帮助器。您可以在“详细功能描述”部分中找到它们的描述。但是,对于新用户,建议从“教程”部分开始发现该库。

教程

在本节中,我们将逐步介绍开始使用该库的基本步骤。 阅读后,您应该能够初始化该库并将日志添加到您的应用程序中。 位于libs/log/examples目录中的示例中也提供了本教程的代码。 随时编译并查看结果。

简单Logging

对于那些不想阅读大量详细手册而只需要一个简单的日志记录工具的人,这里您可以:

#include <boost/log/trivial.hpp>

int main(int, char*[]) {
BOOST_LOG_TRIVIAL(trace) << "A trace severity message";
BOOST_LOG_TRIVIAL(debug) << "A debug severity message";
BOOST_LOG_TRIVIAL(info) << "An informational severity message";
BOOST_LOG_TRIVIAL(warning) << "A warning severity message";
BOOST_LOG_TRIVIAL(error) << "An error severity message";
BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message";

return 0;
}

BOOST_LOG_TRIVIAL宏接受严重性级别,并生成支持插入运算符的类似流的对象。 作为此代码的结果,日志消息将被打印在控制台上。 如您所见,该库的使用模式与std::cout的操作非常相似。 但是,该库具有一些优点:

  1. 除了记录消息外,输出中的每个日志记录还包含时间戳记,当前线程标识符和严重性级别。
  2. 同时写入来自不同线程的日志是安全的,日志消息不会被破坏。
  3. 如稍后所示,可以应用过滤。

必须指出,该宏以及该库提供的其他类似宏并不是库提供的唯一接口。 可以根本不使用任何宏来发布日志记录。

带过滤器的简单Logging

尽管严重性级别可用于提供信息,但通常您将希望应用过滤器以仅输出重要记录,而忽略其余记录。 可以通过在库核心中设置全局过滤器来轻松实现,如下所示:

void init() {
logging::core::get()->set_filter (
logging::trivial::severity >= logging::trivial::info
);
}

int main(int, char*[]) {
init();

BOOST_LOG_TRIVIAL(trace) << "A trace severity message";
BOOST_LOG_TRIVIAL(debug) << "A debug severity message";
BOOST_LOG_TRIVIAL(info) << "An informational severity message";
BOOST_LOG_TRIVIAL(warning) << "A warning severity message";
BOOST_LOG_TRIVIAL(error) << "An error severity message";
BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message";

return 0;
}

现在,如果我们运行此代码示例,则前两个日志记录将被忽略,而其余四个将传递到控制台。

重要

请记住,仅当日志记录通过过滤时才执行流表达式。不要在流表达式中指定关键业务调用,因为如果记录被过滤掉,这些调用可能不会被调用。

关于过滤器设置表达式必须说几句话。由于我们正在设置全局过滤器,因此我们必须获取日志记录核心实例。这就是logging::core::get()所做的,它返回一个指向core单例的指针。日志记录core的set_filter方法设置全局过滤功能。

此示例中的过滤器构建为Boost.Phoenix lambda表达式。在我们的例子中,该表达式由单个逻辑谓词组成,其左参数是一个占位符,描述了要检查的属性,而右参数是要进行检查的值。严重性关键字是库提供的占位符。该占位符标识模板表达式中的严重性属性值;该值的名称应为“ Severity”,类型为“ severity_level”。如果进行平凡的日志记录,则库自动提供此属性;用户只需在日志记录语句中提供其值即可。占位符与排序运算符一起创建一个函数对象,日志核心将调用该函数对象来过滤日志记录。结果,只有严重级别不低于info的日志记录才会通过过滤器,并最终出现在控制台上。

可以构建更复杂的过滤器,将这样的逻辑谓词相互组合,甚至可以定义自己的函数(包括C ++ 11 lambda函数)作为过滤器。在以下各节中,我们将返回到过滤。

设置接收器(Sinks)

有时,简单的日志记录不能提供足够的灵活性。例如,人们可能想要更复杂的日志处理逻辑,而不是简单地在控制台上打印它。 为了进行自定义,您必须构建日志记录接收器并将其注册到日志记录core。通常应该只在应用程序的启动代码中的某个位置执行此操作。

注意:必须提到的是,在前面的部分中,我们没有初始化任何接收器,无论如何,简单的日志记录还是可以工作的。 这是因为库包含一个默认接收器,当用户没有设置任何接收器时,该默认接收器用作后备。 此接收器始终以固定格式将日志记录打印到控制台,在前面的示例中已经看到过。 默认接收器主要用于允许立即使用简单日志记录,无需任何库初始化。 将任何接收器添加到日志记录核心后,将不再使用默认接收器。 您仍然可以使用简单的日志记录宏。

经日志记录到文件

首先,这是初始化记录到文件的方式:

void init() {
logging::add_file_log("sample.log");

logging::core::get()->set_filter
(
logging::trivial::severity >= logging::trivial::info
);
}

添加的部分是对add_file_log函数的调用。 顾名思义,该函数将初始化一个记录接收器,该记录接收器将日志记录存储到文本文件中。 该功能还接受许多自定义选项,例如文件周转间隔和大小限制。 例如:

void init() {
logging::add_file_log
(
keywords::file_name = "sample_%N.log",//文件名模式
keywords::rotation_size = 10 * 1024 * 1024,//每隔10 MiB周转一次文件......
keywords::time_based_rotation = sinks::file::rotation_at_time_point(0, 0, 0), //或者在0点进行周转
keywords::format = "[%TimeStamp%]: %Message%" //日志记录格式
);

logging::core::get()->set_filter
(
logging::trivial::severity >= logging::trivial::info
);
}

您可以看到选项以命名形式传递给了函数。库的许多其他地方也采用这种方法。 您会习惯的。 参数的含义大部分是不言而喻的,并在本手册中进行了说明(有关文本文件接收器的信息,请参见此处)。此部分和其他便利初始化功能在本节中介绍。

注意

您可以注册多个接收器。 每个接收器将独立于其他接收器发送和接收日志记录。

接收器深度:更多接收器

如果您不想详细介绍,可以跳过本节并继续阅读下一节。 否则,如果您需要对接收器配置进行更全面的控制,或者要使用比通过帮助函数可用的接收器更多的接收器,则可以手动注册接收器。

在最简单的形式中,上一节中对add_file_log函数的调用几乎与此等效:

void init() {
// Construct the sink
typedef sinks::synchronous_sink< sinks::text_ostream_backend > text_sink;
boost::shared_ptr< text_sink > sink = boost::make_shared< text_sink >();

// Add a stream to write log to
sink->locked_backend()->add_stream(
boost::make_shared< std::ofstream >("sample.log"));

// Register the sink in the logging core
logging::core::get()->add_sink(sink);
}

好的,您可能注意到的关于接收器的第一件事是它们由两类组成:前端和后端。前端(上面的代码段中是 synchronous_sink类模板)负责所有接收器的各种常见任务,例如线程同步模型,过滤以及基于文本的接收器的格式化。后端(上面的text_ostream_backend类)实现了特定于接收器的所有内容,例如在这种情况下写入文件。该库提供了许多可以立即使用的前端和后端。

上面的synchronous_sink类模板指示接收器是同步的,也就是说,它允许多个线程同时记录,并且在发生争用时将阻塞。这意味着后端text_ostream_backend完全不必担心多线程。还有其他可用的接收器前端,您可以在此处阅读有关它们的更多信息。

text_ostream_backend类将格式化的日志记录写入兼容STL的流中。上面我们使用了文件流,但是我们可以使用任何类型的流。例如,将输出添加到控制台可能如下所示:

#include <boost/core/null_deleter.hpp>

// We have to provide an empty deleter to avoid destroying the global stream object
boost::shared_ptr< std::ostream > stream(&std::clog, boost::null_deleter());
sink->locked_backend()->add_stream(stream);

text_ostream_backend支持添加多个流。在这种情况下,其输出将复制到所有添加的流中。将输出复制到控制台和文件可能很有用,因为库的所有筛选,格式化和其他开销对于接收器的每个记录仅发生一次。

注意

请注意,注册多个不同的接收器与向多个目标流注册一个接收器之间的区别。尽管前者允许独立定制每个接收器的输出,但如果不需要这种定制,后者将可以更快地工作。此功能特定于此特定后端。

该库提供了许多后端,这些后端提供了不同的日志处理逻辑。例如,通过指定syslog后端,您可以通过网络将日志记录发送到syslog服务器,或者通过设置Windows NT事件日志后端,您可以使用标准Windows工具监视应用程序的运行时间。

在此需要注意的最后一件事是用于访问接收器后端的locked_backend成员函数调用。它用于获取对后端的线程安全访问,并且由所有接收器前端提供。该函数将智能指针返回到后端,并且只要后端存在,它就会被锁定(这意味着即使另一个线程尝试进行日志记录并且日志记录已传递到接收器,也不会在您释放后端之前记录它)。唯一的例外是unlocked_sink前端,它根本不同步,只是将一个未锁定的指针返回到后端。

创建loggers并写入logs

专用记录器对象

现在我们已经定义了日志的存储位置和存储方式,现在该继续尝试日志了。 为此,必须创建一个日志记录源。 在我们的例子中,这将是一个记录器对象,它很简单:

src::logger lg;

注意

好奇的读者可能会注意到我们没有为trivial logging创建任何日志记录器。实际上,logger由库提供,并且在后台使用。

与sinks不同,sources无需在任何地方注册,因为它们直接与logging core交互。还要注意,该库提供了两种版本的loggers:线程安全的和非线程安全的。对于非线程安全的记录器,不同线程可以通过不同的记录器实例写入日志是安全的,因此,每个写入日志的线程都应该有一个单独的记录器。可以同时从不同的线程访问线程安全的对应对象,但这将涉及锁定,并且在密集日志记录的情况下可能会使速度变慢。线程安全记录器类型的名称中带有_mt后缀。

无论线程安全如何,该库提供的所有记录器都是默认的,并且可复制构造并支持交换,因此使记录器成为您的类的成员应该没有问题。正如您稍后将看到的那样,这种方法可以为您带来更多好处。

该库为许多loggers提供了不同的功能,例如severity和通道支持。这些功能可以相互组合,以构建更复杂的记录器。有关更多详细信息,请参见此处

全局记录器对象

如果您不能将logger放入您的类中(假设您没有类),该库提供了一种声明全局记录器的方法,如下所示:

BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT(my_logger, src::logger_mt)

此处,my_logger是用户定义的标记名,以后将用于检索记录器实例,而logger_mt是记录器类型。库提供的或用户定义的任何looger类型都可以参与此类声明。但是,由于该logger将只有一个实例,因此通常需要将多线程应用程序中的线程安全logger用作全局记录器。

提示

还有其他宏可用于更复杂的情况。详细说明在本节中。

稍后,您可以像以下方式获取记录器:

src::logger_mt &lg = my_logger::get();

lg将在整个应用程序中引用记录器的唯一实例,即使该应用程序包含多个模块也是如此。 get函数本身是线程安全的,因此不需要在其周围进行额外的同步。

写入日志

无论您使用哪种类型的logger(类成员或全局,线程安全与否),要将日志记录写入记录器中,您都可以编写如下内容:

logging::record rec = lg.open_record();
if (rec) {
logging::record_ostream strm(rec);
strm << "Hello, World!";
strm.flush();
lg.push_record(boost::move(rec));
}

在这里,open_record函数调用确定要构造的记录是否至少要由一个接收器使用。 在此阶段应用过滤。 如果要使用记录,该函数将返回一个有效的记录对象,并且可以填充记录消息字符串。 之后,可以通过调用push_record完成记录处理。

当然,上述语法可以轻松地包装在宏中,实际上,鼓励用户编写自己的宏,而不是直接使用C ++记录器接口。 上面的日志记录可以这样写:

BOOST_LOG(lg) << "Hello, World!";

看起来短一些,不是吗? 该库定义了BOOST_LOG宏以及其他类似的宏。 它自动提供类似于STL的流,以便使用普通的插入表达式格式化消息。 编写,编译和执行所有代码后,您应该可以看到“ Hello,World!”。 记录在“ sample.log”文件中。 您可以在这里找到本节的完整代码。

向日志添加更多信息:属性

在前面的部分中,我们多次提到了属性和属性值。在这里,我们将发现如何使用属性将更多数据添加到日志记录。

每个日志记录可以附加许多命名的属性值。属性可以表示有关发生日志记录的条件的任何基本信息,例如代码中的位置,可执行模块名称,当前日期和时间,或与您的特定应用程序和执行环境有关的任何数据。属性可以充当值生成器,在这种情况下,它将为所涉及的每个日志记录返回一个不同的值。属性生成值后,后者将独立于创建者,并且可以由过滤器,格式化程序使用和接收(sink)。但是,为了使用属性值,必须知道其名称和类型,或者至少知道它可能具有的一组类型。库中实现了许多常用的属性,您可以在文档中找到其值的类型。

除此之外,如“设计概述”部分所述,属性还有三个可能的范围:特定于源,特定于线程和全局。进行日志记录时,这三个集合的属性值将合并为一个集合,并传递到接收器。这意味着该属性的来源对接收器没有影响。任何属性都可以在任何范围内注册。注册后,将为属性赋予唯一名称,以便可以搜索它。如果碰巧在多个作用域中找到了相同的命名属性,则在任何进一步的处理(包括过滤和格式化)中都会考虑来自最特定作用域的属性。这种行为使得可以使用在本地记录器中注册的属性覆盖全局或线程作用域的属性,从而减少线程干扰。

下面是属性注册过程的描述。

常用属性

有些属性几乎可以在任何应用程序中使用。日志记录计数器和时间戳是不错的选择。可以通过单个函数调用添加它们:

logging::add_common_attributes();

通过此调用属性“ LineID”,“ TimeStamp”,“ ProcessID”和“ ThreadID”被全局注册。 “ LineID”属性是一个计数器,对于创建的每个记录,该计数器都会递增,第一个记录将获得标识符1。“ TimeStamp”属性始终产生当前时间(即,创建日志记录的时间,而不是写入其的时间) 到一个接收器sink)。 最后两个属性标识发出每个日志记录的进程和线程。

注意

在单线程构建中,未注册“ ThreadID”属性。

提示

默认情况下,启动应用程序时,库中未注册任何属性。在开始写入日志之前,应用程序必须在库中注册所有必要的属性。这可以作为库初始化的一部分来完成。一个好奇的读者可能会想知道当时简单的日志记录如何工作。答案是,默认接收器实际上不使用除严重性级别以外的任何属性值来构成其输出。这样做是为了避免对简单的日志记录进行任何初始化。一旦使用了过滤器或格式化程序以及非默认接收器,就必须注册所需的属性。

add_common_attributes函数是此处描述的几个便捷帮助函数之一。

某些属性会在记录器构造中自动注册。例如,severity_logger注册一个特定于源的属性“ Severity”,该属性可用于为不同的日志记录增加重点级别。例如:

// We define our own severity levels
enum severity_level {
normal,
notification,
warning,
error,
critical
};

void logging_function() {
// The logger implicitly adds a source-specific attribute 'Severity'
// of type 'severity_level' on construction
src::severity_logger< severity_level > slg;

BOOST_LOG_SEV(slg, normal) << "A regular message";
BOOST_LOG_SEV(slg, warning) << "Something bad is going on but I can handle it";
BOOST_LOG_SEV(slg, critical) << "Everything crumbles, shoot me now!";
}

提示

您可以通过为该类型定义运算符<<来为严重性级别定义自己的格式设置规则。 库格式化程序将自动使用它。 有关更多详细信息,请参见本节。

BOOST_LOG_SEV宏的行为与BOOST_LOG非常相似,不同之处在于它为记录器的open_record方法使用了一个附加参数。 BOOST_LOG_SEV宏可以用以下等效项代替:

void manual_logging() {
src::severity_logger< severity_level > slg;

logging::record rec = slg.open_record(keywords::severity = normal);
if (rec) {
logging::record_ostream strm(rec);
strm << "A regular message";
strm.flush();
slg.push_record(boost::move(rec));
}
}

您可以在此处看到open_record可以接受命名参数。 库提供的某些记录器类型支持这些附加参数,并且用户在编写自己的记录器时当然可以使用此方法。

更多的 Attributes

让我们看看之前使用的add_common_attributes函数的定义。 它可能看起来像这样:

void add_common_attributes() {
boost::shared_ptr< logging::core > core = logging::core::get();
core->add_global_attribute("LineID", attrs::counter< unsigned int >(1));
core->add_global_attribute("TimeStamp", attrs::local_clock());

// other attributes skipped for brevity
}

这里counterlocal_clock组件是属性类,它们是从公共属性接口属性派生的。 该库提供了许多其他属性类,包括在值获取时调用某些函数对象的function属性。 例如,我们可以通过类似的方式注册named_scope属性:

core->add_global_attribute("Scope", attrs::named_scope());

这将使我们能够为应用程序创建的每个日志记录在日志中存储作用域名称。 使用方法如下:

void named_scope_logging() {
BOOST_LOG_NAMED_SCOPE("named_scope_logging");

src::severity_logger< severity_level > slg;

BOOST_LOG_SEV(slg, normal) << "Hello from the function named_scope_logging!";
}

Logger-specific的属性的作用不亚于全局属性。 严重级别和通道名称是在source level上最明显的候选对象。 没有什么可以阻止您向记录器添加更多属性,如下所示:

void tagged_logging() {
src::severity_logger< severity_level > slg;
slg.add_attribute("Tag", attrs::constant< std::string >("My tag value"));

BOOST_LOG_SEV(slg, normal) << "Here goes the tagged record";
}

现在,通过此记录器创建的所有日志记录都将被标记为特定属性。 稍后可以在过滤和格式化中使用此属性值。

属性的另一个好用法是能够标记应用程序不同部分制作的日志记录,以突出显示与单个流程相关的活动。 甚至可以实施一种粗糙的性能分析工具来检测性能瓶颈。 例如:

void timed_logging() {
BOOST_LOG_SCOPED_THREAD_ATTR("Timeline", attrs::timer());

src::severity_logger< severity_level > slg;
BOOST_LOG_SEV(slg, normal) << "Starting to time nested functions";

logging_function();

BOOST_LOG_SEV(slg, normal) << "Stopping to time nested functions";
}

现在,从logging_function函数或它调用的任何其他函数创建的每个日志记录都将包含“时间轴”属性,该属性自注册该属性以来经过了高精度的持续时间。 根据这些读数,您将能够检测出代码的哪些部分需要更多或更少的时间来执行。 离开函数timed_logging的范围后,“时间轴”属性将被取消注册。

有关库提供的属性的详细说明,请参见“属性”部分。 此部分的完整代码在此处。

定义属性占位符

正如我们将在接下来的部分中看到的,定义描述应用程序使用的特定属性的关键字很有用。 该关键字将能够参与过滤和格式化表达式,例如我们在上一节中使用的严重性占位符。 例如,要为前面的示例中使用的某些属性定义占位符,我们可以这样编写:

BOOST_LOG_ATTRIBUTE_KEYWORD(line_id, "LineID", unsigned int)
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", severity_level)
BOOST_LOG_ATTRIBUTE_KEYWORD(tag_attr, "Tag", std::string)
BOOST_LOG_ATTRIBUTE_KEYWORD(scope, "Scope", attrs::named_scope::value_type)
BOOST_LOG_ATTRIBUTE_KEYWORD(timeline, "Timeline", attrs::timer::value_type)

每个宏定义一个关键字。 第一个参数是占位符名称,第二个参数是属性名称,最后一个参数是属性值类型。 定义后,可以在模板表达式和库的其他一些上下文中使用占位符。 有关定义属性关键字的更多详细信息,请参见此处。

日志记录格式

如果您尝试运行前几节中的示例,则可能已经注意到,只有日志记录消息被写入文件中。 当未设置格式化器(formatter)时,这是库的默认行为。 即使您将属性添加到日志记录核心或记录器,属性值也不会到达输出,除非您指定将使用这些值的格式化程序。

返回到先前教程部分中的示例之一:

void init() {
logging::add_file_log
(
keywords::file_name = "sample_%N.log",
keywords::rotation_size = 10 * 1024 * 1024,
keywords::time_based_rotation = sinks::file::rotation_at_time_point(0, 0, 0),
keywords::format = "[%TimeStamp%]: %Message%"
);

logging::core::get()->set_filter
(
logging::trivial::severity >= logging::trivial::info
);
}

在add_file_log函数的情况下,format参数允许指定日志记录的格式。 如果您希望手动设置接收器,接收器前端为此提供了set_formatter成员函数。

如将进一步描述的,可以通过多种方式指定格式。

Lambda样式格式化程序

您可以使用lambda样式的表达式创建格式化程序,如下所示:

void init() {
logging::add_file_log
(
keywords::file_name = "sample_%N.log",
// This makes the sink to write log records that look like this:
// 1: <normal> A normal severity message
// 2: <error> An error severity message
keywords::format =
(
expr::stream
<< expr::attr< unsigned int >("LineID")
<< ": <" << logging::trivial::severity
<< "> " << expr::smessage
)
);
}

在此,stream是用于格式化记录的流的占位符。其他插入参数(例如attr和message)是用于定义应在流中存储什么的操纵器。 我们已经在过滤表达式中看到了严重性占位符,这里在格式化程序中使用了它。 这是一个很好的统一:您可以在过滤器和格式化程序中使用相同的占位符。 attr占位符与严重性占位符相似,因为它也代表属性值。 区别在于,severity占位符表示名称为“ Severity”的特定属性,并且类型trivial::severity_level和attr可用于表示任何属性。 否则,两个占位符是等效的。 例如,可以用以下内容替换严重性:

expr::attr< logging::trivial::severity_level >("Severity")

提示

如上一节所示,可以为用户属性定义占位符,例如严重性。 作为模板表达式中更简单的语法的另一个好处,此类占位符允许将有关属性(名称和值类型)的所有信息集中在占位符定义中。 这使编码不太容易出错(您不会误拼属性名称或指定错误的值类型),因此是定义新属性并在模板表达式中使用它们的推荐方法。

还有其他格式化程序操纵器,它们提供对日期,时间和其他类型的高级支持。 一些操纵器接受其他自定义其行为的参数。 这些参数中的大多数已命名,并且可以Boost.Parameter样式传递。

对于更改,让我们看看手动初始化接收器时是如何完成的

void init() {
typedef sinks::synchronous_sink< sinks::text_ostream_backend > text_sink;
boost::shared_ptr< text_sink > sink = boost::make_shared< text_sink >();

sink->locked_backend()->add_stream(
boost::make_shared< std::ofstream >("sample.log"));

sink->set_formatter
(
expr::stream
// line id will be written in hex, 8-digits, zero-filled
<< std::hex << std::setw(8) << std::setfill('0') << expr::attr< unsigned int >("LineID")
<< ": <" << logging::trivial::severity
<< "> " << expr::smessage
);

logging::core::get()->add_sink(sink);
}

您可以看到可以在表达式中绑定格式更改操纵符; 格式化日志记录时,这些操纵器将影响后续的属性值格式,就像使用流一样。 在“详细功能描述”部分中描述了更多操纵器。

Boost.Format样式的格式化器

或者,您可以使用类似于Boost.Format的语法来定义格式器。 与上述相同的格式化程序可以编写如下:

void init() {
typedef sinks::synchronous_sink< sinks::text_ostream_backend > text_sink;
boost::shared_ptr< text_sink > sink = boost::make_shared< text_sink >();

sink->locked_backend()->add_stream(
boost::make_shared< std::ofstream >("sample.log"));

// This makes the sink to write log records that look like this:
// 1: <normal> A normal severity message
// 2: <error> An error severity message
sink->set_formatter
(
expr::format("%1%: <%2%> %3%")
% expr::attr< unsigned int >("LineID")
% logging::trivial::severity
% expr::smessage
);

logging::core::get()->add_sink(sink);
}

format 占位符接受格式字符串,该格式字符串具有要格式化的所有参数的位置说明。 请注意,目前仅支持位置格式。 相同的格式规范可以与add_file_log和类似的函数一起使用。

专业格式化器

该库为多种类型提供了专门的格式化程序,例如日期,时间和命名范围。 这些格式化程序提供了对格式化值的扩展控制。 例如,可以使用与Boost.DateTime兼容的格式字符串来描述日期和时间格式:

void init()
{
logging::add_file_log
(
keywords::file_name = "sample_%N.log",
// This makes the sink to write log records that look like this:
// YYYY-MM-DD HH:MI:SS: <normal> A normal severity message
// YYYY-MM-DD HH:MI:SS: <error> An error severity message
keywords::format =
(
expr::stream
<< expr::format_date_time< boost::posix_time::ptime >("TimeStamp", "%Y-%m-%d %H:%M:%S")
<< ": <" << logging::trivial::severity
<< "> " << expr::smessage
)
);
}

相同的格式化程序也可以在Boost.Format样式格式化程序的上下文中使用。

字符串模板作为格式化程序

在某些情况下,文本模板可以用作格式器。 在这种情况下,将调用库初始化支持代码,以解析模板并重构适当的格式化程序。 使用此方法时,有许多注意事项需要注意,但是这里只需简要描述模板格式就足够了。

void init() {
logging::add_file_log
(
keywords::file_name = "sample_%N.log",
keywords::format = "[%TimeStamp%]: %Message%"
);
}

在这里,format参数接受这种格式模板。 模板可以包含许多用百分号(%)括起来的占位符。 每个占位符必须包含要插入的属性值名称,而不是占位符。 %Message%占位符将替换为日志记录消息。

注意

set_formatter方法中的接收器后端不接受文本格式模板。 为了将文本模板解析为格式化程序功能,必须调用parse_formatter函数。 有关更多详细信息,请参见此处。

自定义格式化函数

您可以将自定义格式化程序添加到支持格式化的接收器后端。 格式化程序实际上是一个支持以下签名的函数对象:

void (logging::record_view const& rec, logging::basic_formatting_ostream< CharT >& strm);

CharT是目标字符类型。 每当日志记录视图rec通过过滤并将存储在日志中时,将调用格式化程序。

提示

记录视图(record views)与记录非常相似。 显着的区别是视图是不可变的,并且实现了浅复制。 格式化程序和接收器仅在记录视图上运行,这可防止它们修改记录,而该记录仍可被其他线程中的其他接收器使用。

格式化的记录应通过插入与STL兼容的输出流strm中来组成。 这是自定义格式化程序功能用法的示例:

void my_formatter(logging::record_view const& rec, logging::formatting_ostream& strm)
{
// Get the LineID attribute value and put it into the stream
strm << logging::extract< unsigned int >("LineID", rec) << ": ";

// The same for the severity level.
// The simplified syntax is possible if attribute keywords are used.
strm << "<" << rec[logging::trivial::severity] << "> ";

// Finally, put the record message to the stream
strm << rec[expr::smessage];
}

void init()
{
typedef sinks::synchronous_sink< sinks::text_ostream_backend > text_sink;
boost::shared_ptr< text_sink > sink = boost::make_shared< text_sink >();

sink->locked_backend()->add_stream(
boost::make_shared< std::ofstream >("sample.log"));

sink->set_formatter(&my_formatter);

logging::core::get()->add_sink(sink);
}

重新过滤

在前面的部分中,我们已经涉及到过滤,但是几乎没有涉及到接口。 既然我们能够向日志记录添加属性并设置接收器,则可以构建所需的复杂过滤。 让我们考虑这个例子:

BOOST_LOG_ATTRIBUTE_KEYWORD(line_id, "LineID", unsigned int)
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", severity_level)
BOOST_LOG_ATTRIBUTE_KEYWORD(tag_attr, "Tag", std::string)

void init() {
// Setup the common formatter for all sinks
logging::formatter fmt = expr::stream
<< std::setw(6) << std::setfill('0') << line_id << std::setfill(' ')
<< ": <" << severity << ">\t"
<< expr::if_(expr::has_attr(tag_attr))
[
expr::stream << "[" << tag_attr << "] "
]
<< expr::smessage;

// Initialize sinks
typedef sinks::synchronous_sink< sinks::text_ostream_backend > text_sink;
boost::shared_ptr< text_sink > sink = boost::make_shared< text_sink >();

sink->locked_backend()->add_stream(
boost::make_shared< std::ofstream >("full.log"));

sink->set_formatter(fmt);

logging::core::get()->add_sink(sink);

sink = boost::make_shared< text_sink >();

sink->locked_backend()->add_stream(
boost::make_shared< std::ofstream >("important.log"));

sink->set_formatter(fmt);

sink->set_filter(severity >= warning || (expr::has_attr(tag_attr) && tag_attr == "IMPORTANT_MESSAGE"));

logging::core::get()->add_sink(sink);

// Add attributes
logging::add_common_attributes();
}

在此示例中,我们初始化两个接收器-一个接收器用于完整的日志文件,另一个接收器仅用于重要消息。两个接收器都将以相同的日志记录格式写入文本文件,我们首先对其进行初始化并将其保存到fmt变量中。格式化程序类型是带有格式化程序调用签名的类型擦除的函数对象;在很多方面,可以将其视为与boost::functionstd::function相似,但它永远不会为空。过滤器也有类似的功能对象。

值得注意的是,格式化程序本身在此处包含一个过滤器。如您所见,该格式包含一个条件部分,该条件部分仅在日志记录包含“ Tag”属性时出现。 has_attr谓词检查记录是否包含“ Tag”属性值,并控制是否将其放入文件中。我们使用attribute关键字为谓词指定属性的名称和类型,但是也可以在has_attr调用站点中指定它们。条件格式化程序将在此处更详细地说明。

进一步进行两个接收器的初始化。第一个接收器没有任何过滤器,这意味着它将每个日志记录保存到文件中。我们在第二个接收器上调用set_filter以仅保存严重性不小于警告或具有值为“ IMPORTANT_MESSAGE”的“ Tag”属性的日志记录。如您所见,过滤器语法与通常的C ++非常相似,尤其是在使用属性关键字时。

与格式化程序一样,也可以将自定义函数用作过滤器。从根本上讲,过滤器功能必须支持以下签名:

bool (logging::attribute_value_set const& attrs);

调用过滤器时,attrs将包含一组完整的属性值,这些属性值可用于决定是否应传递或禁止日志记录。 如果过滤器返回true,则将构建记录并由接收器进一步处理。 否则,该记录将被丢弃。

Boost.Phoenix在构造过滤器方面非常有用。 由于其绑定实现与属性占位符兼容,因此它允许从attrs集合中自动提取属性值。 可以通过以下方式修改前面的示例:

bool my_filter(logging::value_ref< severity_level, tag::severity > const& level,
logging::value_ref< std::string, tag::tag_attr > const& tag)
{
return level >= warning || tag == "IMPORTANT_MESSAGE";
}

void init()
{
// ...

namespace phoenix = boost::phoenix;
sink->set_filter(phoenix::bind(&my_filter, severity.or_none(), tag_attr.or_none()));

// ...
}

宽字符记录

详细功能说明

核心设施

Logging sources

基本的loggers

#include <boost/log/sources/basic_logger.hpp>

该库提供的最简单的loggins sourceslogger及其线程安全版本logger_mt(相应地,对于宽字符logging,是wloggerwlogger_mt)。 这些loggers仅提供在其内部存储特定于源的属性的能力,当然还可以形成日志记录。 当不需要诸如严重性级别检查之类的高级功能时,应该使用这种类型的记录器。 它很可能用作收集应用程序统计信息和注册应用程序事件(例如通知和警报)的工具。 在这种情况下,通常将记录器与范围属性结合使用,以将所需数据附加到通知事件。 以下是用法示例:

class network_connection {
src::logger m_logger;
logging::attribute_set::iterator m_remote_addr;
public:
void on_connected(std::string const& remote_addr) {
// Put the remote address into the logger to automatically attach it
// to every log record written through the logger
m_remote_addr = m_logger.add_attribute("RemoteAddress",
attrs::constant< std::string >(remote_addr)).first;

// The straightforward way of logging
if (logging::record rec = m_logger.open_record()) {
rec.attribute_values().insert("Message",
attrs::make_attribute_value(std::string("Connection established")));
m_logger.push_record(boost::move(rec));
}
}
void on_disconnected() {
// The simpler way of logging: the above "if" condition is wrapped
// into a neat macro
BOOST_LOG(m_logger) << "Connection shut down";

// Remove the attribute with the remote address
m_logger.remove_attribute(m_remote_addr);
}
void on_data_received(std::size_t size) {
// Put the size as an additional attribute
// so it can be collected and accumulated later if needed.
// The attribute will be attached only to this log record.
BOOST_LOG(m_logger) << logging::add_value("ReceivedSize", size) << "Some data received";
}
void on_data_sent(std::size_t size) {
BOOST_LOG(m_logger) << logging::add_value("SentSize", size) << "Some data sent";
}
};

上面的代码片段中的 network_connection类表示一种在与网络相关的应用程序中实现简单日志记录和统计信息收集的方法。 该类的每种方法都有效地标记了一个相应的事件,可以在sink级别上对其进行跟踪和收集。 此外,为简单起见,此处未显示的该类的其他方法也能够编写日志。 请注意,曾经在network_connection对象的连接状态下进行的每个日志记录都将隐式标记为远程站点的地址。

具有严重性级别支持的loggers

#include <boost/log/sources/severity_feature.hpp>
#include <boost/log/sources/severity_logger.hpp>

根据某种程度的严重性或重要性将某些日志记录与其他日志记录区分开的能力是最常要求的功能之一。提供了此功能的类模板severity_loggerseverity_logger_mt(以及与它们对应的wseverity_loggerwseverity_logger_mt宽字符)。

记录器自动注册特殊的特定于源的属性“ Severity”,可以以紧凑高效的方式为每条记录设置此属性,并且可以将命名参数的严重性传递给构造函数和/或open_record方法。如果传递给logger的构造函数,严重性参数将设置严重性级别的默认值,如果open_record参数中未提供默认值,则将使用该默认值。传递给open_record方法的严重性参数设置要创建的特定日志记录的级别。严重性级别的类型可以作为记录器类模板的模板参数提供。默认类型为int。

此属性的实际值及其含义完全由用户定义。但是,建议将等于零的值级别用作其他值的基点。这是因为默认构造的记录器对象将其默认严重性级别设置为零。还建议为整个应用程序定义相同的严重性级别,以避免以后在书面日志中造成混淆。以下代码段显示了strict_logger的用法。

// We define our own severity levels
enum severity_level
{
normal,
notification,
warning,
error,
critical
};

void logging_function()
{
// The logger implicitly adds a source-specific attribute 'Severity'
// of type 'severity_level' on construction
src::severity_logger< severity_level > slg;

BOOST_LOG_SEV(slg, normal) << "A regular message";
BOOST_LOG_SEV(slg, warning) << "Something bad is going on but I can handle it";
BOOST_LOG_SEV(slg, critical) << "Everything crumbles, shoot me now!";
}
void default_severity()
{
// The default severity can be specified in constructor.
src::severity_logger< severity_level > error_lg(keywords::severity = error);

BOOST_LOG(error_lg) << "An error level log record (by default)";

// The explicitly specified level overrides the default
BOOST_LOG_SEV(error_lg, warning) << "A warning level log record (overrode the default)";
}

或者,如果您更喜欢不使用宏进行记录:

void manual_logging()
{
src::severity_logger< severity_level > slg;

logging::record rec = slg.open_record(keywords::severity = normal);
if (rec)
{
logging::record_ostream strm(rec);
strm << "A regular message";
strm.flush();
slg.push_record(boost::move(rec));
}
}

而且,当然,严重性记录器还提供与基本记录器相同的功能。

全局Loggers

#include <boost/log/sources/global_logger_storage.hpp>

有时候需要一个logger对象来写日志是不方便的。这个问题经常出现在没有明显的地方可以存储logger的函数式代码中。另一个问题仍然存在的领域是可以从日志中受益的通用库。在这种情况下,拥有一个或多个全局记录器会更方便,以便在需要时在每个地方轻松访问它们。在这方面 std::cout 是这种logger的一个很好的例子。

该库提供了一种声明全局logger的方法,可以像std::cout一样访问这些logger。事实上,此功能可用于任何logger,包括用户定义的logger。声明了一个全局logger后,可以确保从应用程序代码的任何位置对该logger实例进行线程安全访问。该库还保证全局looger实例即使跨越模块边界也是唯一的。这甚至允许在可能被编译到不同模块中的只有头文件的组件中使用日志记录。

人们可能想知道为什么需要一些特殊的东西来创建全局记录器。为什么不在命名空间范围内声明一个记录器变量并在需要的任何地方使用它?虽然从技术上讲这是可能的,但由于以下原因,声明和使用全局记录器变量很复杂:

C++ 标准未指定命名空间范围变量的初始化顺序。这意味着通常您不能在初始化的这个阶段(即在 main 之前)使用记录器。

命名空间范围变量的初始化不是线程安全的。您最终可能会两次初始化同一个记录器或使用未初始化的记录器。

在仅标头的库中使用命名空间范围变量非常复杂。要么必须声明具有外部链接的变量并仅在单个翻译单元中定义它(即在单独的 .cpp 文件中,这与“header-only”论点相悖),或者定义具有内部链接的变量,或作为匿名命名空间中的特殊情况(这很可能会破坏 ODR 并在不同翻译单元中使用标头时产生意外结果)。还有其他特定于编译器的和标准的技巧来解决这个问题,但它们不是很简单和可移植。

在大多数平台上,命名空间范围变量对于它们被编译到的模块是本地的。也就是说,如果变量 a 具有外部链接并被编译到模块 X 和 Y 中,则这些模块中的每一个都有自己的变量 a 副本。更糟糕的是,在其他平台上,这个变量可以在模块之间共享。 全局记录器存储旨在消除所有这些问题。

声明全局logger的最简单方法是使用以下宏:

BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT(my_logger, src::severity_logger_mt< >)

my_logger 参数为Log提供了一个可用于获取Logger实例的名称。 此名称充当已声明logger的标记。 第二个参数表示Logger类型。 在多线程应用程序中,当可以从不同线程访问logger时,用户通常希望使用Logger的线程安全版本。

如果需要将参数传递给记录器构造函数,还有另一个宏:

BOOST_LOG_INLINE_GLOBAL_LOGGER_CTOR_ARGS(
my_logger,
src::severity_channel_logger< >,
(keywords::severity = error)(keywords::channel = "my_channel"))

最后一个宏参数是传递给记录器构造函数的 Boost.Preprocessor 参数序列。 但是,在使用非常量表达式和对对象的引用作为构造函数参数时要小心,因为这些参数只计算一次,而且通常很难确定完成的确切时间。 记录器是根据应用程序中知道记录器声明的任何部分的第一个请求构建的。 由用户确保所有参数此时都具有有效状态。

本节的第三个宏提供了最大的初始化灵活性,允许用户实际定义创建记录器的逻辑。

BOOST_LOG_INLINE_GLOBAL_LOGGER_INIT(my_logger, src::severity_logger_mt)
{
// Do something that needs to be done on logger initialization,
// e.g. add a stop watch attribute.
src::severity_logger_mt< > lg;
lg.add_attribute("StopWatch", attrs::timer());
// The initializing routine must return the logger instance
return lg;
}

BOOST_LOG_INLINE_GLOBAL_LOGGER_CTOR_ARGS宏一样,初始化代码仅在Logger的第一个请求时调用一次。

谨防单一定义规则 (ODR) 问题。 无论您选择哪种logger声明方式,您都应该确保在所有出现时以完全相同的方式声明 logger,并且声明中涉及的所有符号名称都解析为相同的实体。 后者包括在 BOOST_LOG_INLINE_GLOBAL_LOGGER_INIT 宏的初始化程序中使用的名称,例如对外部变量、函数和类型的引用。该库试图在一定程度上保护自己免受 ODR 违规,但通常如果违反规则,行为是未定义的。

为了缓解 ODR 问题,可以将记录器声明与其初始化例程分开。 该库提供了以下宏来实现这一点:

  • BOOST_LOG_GLOBAL_LOGGER提供Logger声明。 它可以在头文件中使用,类似于上面描述的BOOST_LOG_INLINE_GLOBAL_LOGGER*宏。
  • BOOST_LOG_GLOBAL_LOGGER_INITBOOST_LOG_GLOBAL_LOGGER_DEFAULTBOOST_LOG_GLOBAL_LOGGER_CTOR_ARGS 定义Logger初始化程序。 它们的语义和用法类似于相应的BOOST_LOG_INLINE_GLOBAL_LOGGER*宏,但有一个例外:这些宏应该在单个.cpp 文件中使用。

例如:

// my_logger.h
// ===========

BOOST_LOG_GLOBAL_LOGGER(my_logger, src::severity_logger_mt)


// my_logger.cpp
// =============

#include "my_logger.h"

BOOST_LOG_GLOBAL_LOGGER_INIT(my_logger, src::severity_logger_mt)
{
src::severity_logger_mt< > lg;
lg.add_attribute("StopWatch", attrs::timer());
return lg;
}

不管你用什么宏来声明logger,你都可以通过logger标签的静态get函数来获取logger实例:

src::severity_logger_mt< >& lg = my_logger::get();

记录器的进一步使用与相应类型的常规记录器对象相同。

需要注意的是,不建议在应用程序的反初始化阶段使用全局记录器。 与应用程序中的任何其他全局对象一样,全局记录器可能会在您尝试使用它之前被销毁。 在这种情况下,最好有一个专用的记录器对象,只要需要,它就可以保证可用。

接收器(sink)前端

接收器后端

文本流后端

#include <boost/log/sinks/text_ostream_backend.hpp>

文本输出流接收器后端是开箱即用的库提供的最通用的后端。 后端在basic_text_ostream_backend类模板中实现(为窄字符和宽字符支持提供了text_ostream_backendwtext_ostream_backend便利类型定义)。 它支持将日志记录格式化为字符串并放入一个或几个流。 每个附加的流都具有相同的格式化结果,因此,如果您需要为不同的流以不同的方式格式化日志记录,则需要创建多个接收器-每个接收器都有自己的格式化程序。

后端还提供了在调试应用程序时可能有用的功能。 使用auto_flush方法,可以告诉接收器在写入每个日志记录后自动刷新所有附加流的缓冲区。 当然,这将降低日志记录性能,但是如果应用程序崩溃,则很有可能不会丢失最后的日志记录。

void init_logging() {
boost::shared_ptr< logging::core > core = logging::core::get();

// Create a backend and attach a couple of streams to it
boost::shared_ptr< sinks::text_ostream_backend > backend =
boost::make_shared< sinks::text_ostream_backend >();
backend->add_stream(
boost::shared_ptr< std::ostream >(&std::clog, boost::null_deleter()));
backend->add_stream(
boost::shared_ptr< std::ostream >(new std::ofstream("sample.log")));

// Enable auto-flushing after each log record written
backend->auto_flush(true);

// Wrap it into the frontend and register in the core.
// The backend requires synchronization in the frontend.
typedef sinks::synchronous_sink< sinks::text_ostream_backend > sink_t;
boost::shared_ptr< sink_t > sink(new sink_t(backend));
core->add_sink(sink);
}

文本文件后端

#include <boost/log/sinks/text_file_backend.hpp>

虽然可以使用文本流后端将日志写入文件,但是库还提供了一个特殊的接收后端,该接收后端具有一组适合基于文件的日志的扩展特性。功能包括:

  • 基于文件大小和/或时间的日志文件周转
  • 灵活的日志文件命名
  • 将周转后的文件放置到文件系统中的特殊位置
  • 删除最旧的文件,以释放文件系统上的更多空间
  • 与文本流后端类似,文件接收后端也支持auto-flush功能

后端称为text_file_backend。

Lambda表达式

正如在教程中指出的那样,可以将过滤器和格式化程序指定为Lambda表达式,并使用占位符表示属性值。 本节将描述可用于构建更复杂的Lambda表达式的占位符。

还有一种方法可以以字符串模板的形式指定过滤器。 这对于从应用程序设置进行初始化很有用。 这里描述了库的这一部分。

通用属性占位符

#include <boost/log/expressions/attr_fwd.hpp>
#include <boost/log/expressions/attr.hpp>

attr占位符表示模板表达式中的属性值。 给定记录视图或一组属性值,占位符将在调用时尝试从参数中提取指定的属性值。 可以使用以下伪代码对此进行粗略描述:

logging::value_ref< T, TagT > val = expr::attr< T, TagT >(name)(rec);

其中val是对所提取值的引用,name和T是属性值的名称和类型,TagT是可选标记(我们将在稍后返回),rec是日志记录视图或属性值集。 T可以是带有可能的预期类型值的Boost.MPL类型序列; 如果值的类型与序列中的一种匹配,则提取将成功。

attr占位符可用于Boost.Phoenix表达式,包括bind表达式。

bool my_filter(logging::value_ref< severity_level, tag::severity > const& level,
logging::value_ref< std::string, tag::tag_attr > const& tag)
{
return level >= warning || tag == "IMPORTANT_MESSAGE";
}

void init()
{
// ...

namespace phoenix = boost::phoenix;
sink->set_filter(phoenix::bind(&my_filter, severity.or_none(), tag_attr.or_none()));

// ...
}

占位符可以在过滤器和格式化程序中使用:

sink->set_filter
(
expr::attr< int >("Severity") >= 5 &&
expr::attr< std::string >("Channel") == "net"
);

sink->set_formatter
(
expr::stream
<< expr::attr< int >("Severity")
<< " [" << expr::attr< std::string >("Channel") << "] "
<< expr::smessage
);

对set_filter的调用将注册一个由两个基本子过滤器组成的复合过滤器:第一个检查严重性级别,第二个检查通道名称。 对set_formatter的调用将安装一个格式化程序,该格式化程序将组成一个字符串,其中包含严重性级别和通道名称以及消息文本。

自定义后备策略
属性标签和自定义格式运算符

属性

实用工具

字符串字面量

#include <boost/log/utility/string_literal.hpp>

整个库中的多个地方都使用了字符串字面量。但是,此组件可以在用户代码中的库外部成功使用。它仅是header-only,不需要链接库二进制文件。如果不需要修改存储的字符串,则字符串字面量可以显着提高性能。同样重要的是,由于字符串字面量不会动态分配内存,因此在使用字符串字面量而不是常规字符串时,更容易维护异常安全性。

该功能在basic_string_literal类模板中实现,该模板通过字符和字符特征进行参数设置,类似于std::basic_string。还提供了两个便捷的typedef:string_literalwstring_literal,分别用于窄字符和宽字符类型。为了简化通用代码中字符串字面量的构造,还有一个str_literal函数模板,该模板接受字符串字面量并返回相应字符类型的basic_string_literal实例。

字符串字面量支持类似于STL字符串的接口,但字符串修改功能除外。但是,可以分配或清除字符串字面量,只要仅涉及字符串字面量即可。还支持关系和流输出运算符。

简化的库初始化工具

提供库的这一部分是为了简化日志记录初始化,并提供用于开发用户特定的初始化机制的基本工具。 众所周知,设置功能和首选项可能因应用程序而异,因此该库不会尝试为此任务提供通用解决方案。 提供的工具主要用于快速支持日志记录设置和一套工具,以实现更详尽,更适合用户的需求。

本节中描述的某些功能将需要单独的库二进制文件,其名称基于“ boost_log_setup”子字符串。 此二进制文件取决于主库。

便利函数
#include <boost/log/utility/setup/console.hpp>
#include <boost/log/utility/setup/file.hpp>
#include <boost/log/utility/setup/common_attributes.hpp>

该库提供了许多简化某些常见初始化过程的功能,例如接收器和常用属性注册。 这不是很多功能。 但是,它为新手节省了几分钟的学习库的时间。

登录到应用程序控制台是查看运行中的日志库的最简单方法。 为此,可以使用一个函数调用来初始化库,如下所示:

int main(int, char*[]) {
// Initialize logging to std::clog
logging::add_console_log();

// Here we go, we can write logs right away
src::logger lg;
BOOST_LOG(lg) << "Hello world!";

return 0;
}

扩展此库

自己写接收器(sink)

编写自己的sources

#include <boost/log/sources/threading_models.hpp>
#include <boost/log/sources/basic_logger.hpp>

您可以通过开发自己的source来扩展该库,并且可以通过这种方式来收集日志数据。 基本上,您有两种选择的开始方式:您可以开发新的logger功能,也可以设计一种全新的source。 如果您所需要做的只是调整现有loggers的功能,则第一种方法很好。 如果提供的记录器收集日志的整个机制不适合您的需求,则第二种方法是合理的。

创建一个新的logger功能

库提供的每个记录器都包含许多可以相互组合的功能。每个功能负责记录器功能的一个独立方面。例如,提供将严重性级别分配给日志记录的功能的记录器包括严重性功能。您可以实现自己的功能,并将其与库提供的功能一起使用。

logger feature应遵循以下基本要求:

  • logger feature应该是类模板。它应该至少具有一种模板参数类型(将其命名为BaseT)。
  • feature必须公开地继承BaseT模板参数。
  • feature必须是默认可构造的和拷贝可构造的。 该功能必须可以使用模板类型的单个参数构造。该功能本身可能不使用此参数,但应将此参数传递给BaseT构造函数。 这些要求允许由彼此衍生的许多功能组成记录器。功能层次结构的根类将是basic_logger类模板实例。此类实现记录器的大多数基本功能,例如存储特定于记录器的属性并提供日志消息格式的接口。层次结构的组成由basic_composite_logger类模板完成,该模板在一系列功能上实例化(不用担心,稍后将在示例中显示)。具有模板化参数的构造函数允许使用Boost.Parameter库使用命名参数初始化功能。

日志记录功能可能还包含内部数据。在这种情况下,为了维护记录器的线程安全性,该功能应遵循以下附加准则:

通常,不需要在每个功能中引入互斥锁或其他同步机制。此外,建议不要这样做,因为相同的功能可以在线程安全和非线程安全的记录器中使用。相反,功能应使用记录器的线程模型作为同步原语,类似于它们使用互斥锁的方式。可通过basic_logger类模板中定义的get_threading_model方法访问线程模型。 如果功能必须覆盖basic_logger类模板的受保护接口(或基本功能接口的相同部分)的* _unlocked方法,则应考虑以下有关此类方法的问题: 最终调用这些方法的公共方法由basic_composite_logger类模板实现。这些实现进行必要的锁定,然后将控制权传递给基本功能的相应_unlocked方法。 这些方法的线程安全要求以锁类型表示。这些类型可以在每个功能和basic_logger类模板中用作typedef。如果该功能公开了受保护的函数foo_unlocked,则还将公开类型foo_lock,该类型将表达foo_unlocked的锁定要求。 basic_composite_logger类模板中的相应方法foo将使用此typedef以便在调用foo_unlocked之前锁定线程模型。 功能构造函数不需要锁定,因此不需要锁定类型。 该功能可以实现副本构造函数。调用构造函数时,构造函数的参数已被共享锁锁定。自然,该功能有望将复制构造函数调用转发给BaseT类。 该功能不需要实现赋值运算符。分配将由basic_composite_logger类实例自动提供。但是,该功能可能提供了swap_unlocked方法,该方法将交换该功能的内容和方法参数,并在BaseT类中调用类似的方法。自动生成的赋值运算符将使用此方法以及副本构造函数。

为了说明所有这些冗长的建议,让我们实现一个简单的记录器功能。假设我们希望记录器能够标记单个日志记录。换句话说,记录器必须将一个属性临时添加到其属性集中,发出日志记录,然后自动删除该属性。使用范围属性可以实现某种类似的功能,尽管语法可能会使将其包装到一个整洁的宏中变得复杂:

// We want something equivalent to this
{
BOOST_LOG_SCOPED_LOGGER_TAG(logger, "Tag", "[GUI]");
BOOST_LOG(logger) << "The user has confirmed his choice";
}

zlog

在接触到的嵌入式Linux项目中,经常碰见在项目中使用 zlog 库,大概是其具有高性能、线程安全、灵活、概念清晰的纯C日志函数库的原因吧。虽然如果是我自己从零开始项目搭建的话,还是比较倾向于使用Boost.Log,不过项目中有用到还是得记录一下基本的一些用法,以便项目调试。

项目一般都使用zlog.conf文件进行日志格式以及文件大小的设置,具体路径会在zlog_init()调用的时候指定。

一个比较常用的zlog.conf内容如下:

[formats]

simple = "%m%n"

[rules]

my_cat.DEBUG >stdout; simple

[formats]定义日志输出格式。simple即为此条格式的名称,方便后续引用。

category指定不同类型的日志entry(上面的my_cat)。 在zlog源码中,category是一个(zlog_cateogory_t *)变量。 在的程序中,日志entry的不同类别会将它们彼此区分开。一般会在代码里使用zlog_get_category()得到一个category。

一般在产品量产后,日志文件都在存储在文件中,不会向终端输出。调试的时候为了方便,可以在[rules]后面加上一行:

# 所有日志以simple格式输出到stdout
*.* >stdout; simple

# my_cat类别下的所有等级日志输出到stdout
my_cat.* >stdout; simple

将Boost.Log输出至zlog

为了兼容老项目使用的zlog,而我们又希望在新代码中使用Boost.Log,那么我们可以将zlog作为Boost.Log的Sink来使用。

ZlogAdapter.h
#ifndef __ZLOGADAPTER_H__
#define __ZLOGADAPTER_H__

#include <boost/log/sinks.hpp>

class ZlogSinkBackend
: public boost::log::sinks::basic_formatted_sink_backend<char, boost::log::sinks::synchronized_feeding> {
public:
ZlogSinkBackend(const std::string &category);
void consume(const boost::log::record_view &record, string_type const &output);

private:
std::string m_category;
};

void zlogFormatter(boost::log::record_view const &record, boost::log::formatting_ostream &ostream);

#endif // __ZLOGADAPTER_H__
ZlogAdapter.cpp
#include "ZlogAdapter.h"
#include "BoostLog.h"
#include "zlog.h"
#include <boost/log/attributes.hpp>
#include <boost/log/expressions.hpp>

void zlogFormatter(boost::log::record_view const &record, boost::log::formatting_ostream &ostream) {
using namespace boost::log;
ostream << "[" << boost::log::extract<boost::log::attributes::current_thread_id::value_type>("ThreadID", record)
<< "] ";
auto &&category = record[AmassKeywords::category];
if (!category.empty()) ostream << " [" << category << "]";
ostream << record[expressions::smessage];
}

ZlogSinkBackend::ZlogSinkBackend(const std::string &category) : m_category(category) {
}

void ZlogSinkBackend::consume(const boost::log::record_view &record, string_type const &output) {
auto category = zlog_get_category(m_category.c_str());
zlog_level level = ZLOG_LEVEL_INFO;
if (auto logLevel = record[boost::log::trivial::severity]; logLevel) {
switch (*logLevel) {
case boost::log::trivial::severity_level::trace:
case boost::log::trivial::severity_level::debug:
level = ZLOG_LEVEL_DEBUG;
break;
case boost::log::trivial::severity_level::info:
level = ZLOG_LEVEL_INFO;
break;
case boost::log::trivial::severity_level::warning:
level = ZLOG_LEVEL_WARN;
break;
case boost::log::trivial::severity_level::error:
level = ZLOG_LEVEL_ERROR;
break;
case boost::log::trivial::severity_level::fatal:
level = ZLOG_LEVEL_FATAL;
break;
default:
break;
}
}
auto file = record[AmassKeywords::filename];
auto line = record[AmassKeywords::line];
auto func = record[AmassKeywords::function];

zlog(category, file->c_str(), file->size(), func->c_str(), func->size(), *line, level, "%s", output.c_str());
}
main.cpp
int main(int argc, char const *argv[]) {
// ... 初始化zlog
boost::log::removeConsoleLog(); // 删除 BoostLog.cpp 中添加的 console 输出
auto zlogBackend = boost::make_shared<ZlogSinkBackend>("my_cat");
using SynchronousZlogSink = boost::log::sinks::synchronous_sink<ZlogSinkBackend>;
auto sink = boost::make_shared<SynchronousZlogSink>(zlogBackend);
sink->set_formatter(&zlogFormatter);
boost::log::core::get()->add_sink(sink);
LOG(info) << "boost log output to zlog.";
// ...
return 0;
}