9 Fast DDS-日志记录(Logging)

9 Fast DDS-日志记录(Logging)

  • [9.1 模块结构(Module Structure)](#9.1 模块结构(Module Structure))
  • [9.2 日志条目规范(Log Entry Specification)](#9.2 日志条目规范(Log Entry Specification))
  • [9.3 日志线程(Logging Thread)](#9.3 日志线程(Logging Thread))
  • [9.4 日志消息(Logging Messages)](#9.4 日志消息(Logging Messages))
  • [9.5 模块配置(Module Configuration)](#9.5 模块配置(Module Configuration))
    • [9.5.1 日志条目(Log Entry)](#9.5.1 日志条目(Log Entry))
      • [9.5.1.1 时间戳(Timestamp)](#9.5.1.1 时间戳(Timestamp))
      • [9.5.1.2 类别(Category)](#9.5.1.2 类别(Category))
      • [9.5.1.3 详细级别(Verbosity Level)](#9.5.1.3 详细级别(Verbosity Level))
      • [9.5.1.4 消息(Message)](#9.5.1.4 消息(Message))
      • [9.5.1.5 文件上下文(File Context)](#9.5.1.5 文件上下文(File Context))
      • [9.5.1.6 函数名称(Function Name)](#9.5.1.6 函数名称(Function Name))
    • [9.5.2 注册消费者(Register Consumers)](#9.5.2 注册消费者(Register Consumers))
    • [9.5.3 重置配置(Reset Configuration)](#9.5.3 重置配置(Reset Configuration))
    • [9.5.4 XML 配置(XML Configuration)](#9.5.4 XML 配置(XML Configuration))
  • [9.6 过滤器(Filters)](#9.6 过滤器(Filters))
    • [9.6.1 类别过滤(Category Filtering)](#9.6.1 类别过滤(Category Filtering))
    • [9.6.2 文件名过滤(File Name Filtering)](#9.6.2 文件名过滤(File Name Filtering))
    • [9.6.3 内容过滤(Content Filtering)](#9.6.3 内容过滤(Content Filtering))
    • [9.6.4 重置日志过滤器(Reset Logging Filters)](#9.6.4 重置日志过滤器(Reset Logging Filters))
  • [9.7 消费者(Consumers)](#9.7 消费者(Consumers))
    • [9.7.1 StdoutConsumer](#9.7.1 StdoutConsumer)
    • [9.7.2 StdoutErrConsumer](#9.7.2 StdoutErrConsumer)
    • [9.7.3 FileConsumer](#9.7.3 FileConsumer)
  • [9.8 禁用日志模块(Disable Logging Module)](#9.8 禁用日志模块(Disable Logging Module))
  • [9.9 禁用旧日志宏(Old Log macros disable)](#9.9 禁用旧日志宏(Old Log macros disable))

eProsima Fast DDS 提供了一个可扩展的内置日志模块,该模块公开了以下主要功能:

  • 三种不同的日志级别:Log::Kind::InfoLog::Kind::WarningLog::Kind::Error(参见《日志消息》)。
  • 根据不同的标准进行消息过滤:类别、内容或源文件(参见《过滤器》)。
  • 输出到 STDOUT、STDERR 和/或日志文件(参见《消费者》)。

本节将专门解释 Fast DDS 日志模块的使用、配置和可扩展性。

9.1 模块结构(Module Structure)

日志模块提供以下类:

  • Log 是日志模块的核心类。此单例不仅负责日志操作(参见《日志消息》),还提供了用于设置不同日志配置方面(参见《模块配置》)以及在不同级别进行日志过滤(参见《过滤器》)的配置 API。它包含零个或多个 LogConsumer 对象。单例的消费线程使用《日志消息》中定义的宏,将添加到日志队列中的日志条目按顺序提供给日志消费者(参见《日志线程》)。

    警告Log API 暴露了成员函数 Log::QueueLog()。但是,此函数不应直接使用。要将消息添加到日志队列,请使用《日志消息》中描述的方法。

  • LogConsumer 是所有日志消费者的基类(参见《消费者》)。它包含了派生类应重载以消费日志条目的成员函数。

  • OStreamConsumer 派生自 LogConsumer。它定义了如何消费日志条目以输出到 std::ostream 对象。它包含一个派生类必须重载的成员函数,以定义所需的 std::ostream 对象。

    • StdoutConsumer 派生自 OStreamConsumer。它将 STDOUT 定义为输出的 std::ostream 对象(参见 StdoutConsumer)。
    • StdoutErrConsumer 派生自 OStreamConsumer。它定义了一个 Log::Kind 阈值,使得如果 Log::Kind 等于或高于选定的阈值,则输出将定义为 STDERR;否则,将 STDOUT 定义为输出(参见 StdoutErrConsumer)。
    • FileConsumer 派生自 OStreamConsumer。它将用户指定的文件定义为输出的 std::ostream 对象(参见 FileConsumer)。

日志模块类图

可以通过创建继承自 LogConsumer 和/或 OStreamConsumer 的新消费者类来进一步扩展该模块。要启用自定义消费者,只需按照《注册消费者》中的说明进行操作。

9.2 日志条目规范(Log Entry Specification)

StdoutConsumerStdoutErrConsumerFileConsumer(eProsima Fast DDS 内置消费者)创建的日志条目遵循以下结构:

复制代码
<Timestamp> [<Category> <Verbosity Level>] <Message> (<File Name>:<Line Number>) -> Function <Function Name>

此类日志条目的示例如下:

复制代码
2020-05-27 11:45:47.447 [DOCUMENTATION_CATEGORY Error] This is an error message (example.cpp:50) -> Function main

注意

文件名和行号以及函数名称仅在启用时才会显示。详情请参见《模块配置》。

9.3 日志线程(Logging Thread)

对《日志消息》中介绍的宏的调用,仅仅是将日志条目添加到一个准备消费的队列中。在创建时,日志模块会生成一个线程,每当有条目添加到队列中时,该线程就会被唤醒。唤醒后,此线程会将队列中的所有条目提供给所有已注册的消费者。一旦工作完成,该线程将恢复空闲状态。这种策略可以防止模块在执行日志记录操作时阻塞应用程序线程。然而,有时应用程序可能希望等待日志记录例程完成后再继续其操作。日志模块通过成员函数 Log::Flush() 提供了此功能。此外,还可以通过成员函数 Log::KillThread() 完全消除该线程及其资源。另外,可以通过成员函数 Log::SetThreadConfig() 配置此日志线程的某些设置。

复制代码
// Block current thread until the log queue is empty.
Log::Flush();

// Stop the loggin thread and free its resources.
Log::KillThread();

// Configure ThreadSettings for the logging thread
Log::SetThreadConfig(eprosima::fastdds::rtps::ThreadSettings{-1, 0, 0, -1});

警告

即使之前已通过 Log::KillThread() 杀死了日志线程,调用《日志消息》中介绍的任何宏都会重新生成日志线程。

9.4 日志消息(Logging Messages)

消息的日志记录由三个专用宏处理,每个宏对应一个可用的详细级别(参见《详细级别》):

  • EPROSIMA_LOG_INFO:以 Log::Kind::Info 详细级别记录消息。
  • EPROSIMA_LOG_WARNING:以 Log::Kind::Warning 详细级别记录消息。
  • EPROSIMA_LOG_ERROR:以 Log::Kind::Error 详细级别记录消息。

所述宏恰好接受两个参数:一个类别(category)和一个消息(message),并生成一个日志条目,显示消息本身以及根据模块配置的一些元信息(参见《日志条目规范》和《日志条目》)。

复制代码
EPROSIMA_LOG_INFO(DOCUMENTATION_CATEGORY, "This is an info message");
EPROSIMA_LOG_WARNING(DOCUMENTATION_CATEGORY, "This is an warning message");
EPROSIMA_LOG_ERROR(DOCUMENTATION_CATEGORY, "This is an error message");

存在一些以前版本使用的旧日志宏:logInfologWarninglogError。只要用户在包含 Log 模块之前没有通过 CMake 选项 ENABLE_OLD_LOG_MACROS 或宏 ENABLE_OLD_LOG_MACROS_ 手动禁用它们,这些宏仍然可用。更多信息请参见《禁用旧日志宏》部分。

警告

请注意,当 CMake 选项 LOG_NO_INFOLOG_NO_WARNINGLOG_NO_ERROR 分别设置为 ON 时,相应的消息级别将被停用。有关如何启用和禁用各个日志宏的更多信息,请参阅《禁用日志模块》。

9.5 模块配置(Module Configuration)

日志模块提供了多种配置选项。日志条目的不同组成部分(参见《日志条目规范》)可以按照《日志条目》中的说明进行配置。此外,日志模块允许注册多个日志消费者,使应用程序能够将日志输出定向到不同的目标(参见《注册消费者》)。另外,一些日志功能可以使用 eProsima Fast DDS XML 配置文件进行配置(参见《XML 配置》)。

9.5.1 日志条目(Log Entry)

日志条目的所有不同组件总结在下表中(请参阅各组件的部分以获取进一步说明):

组件 可选性 默认状态
时间戳(Timestamp) 启用
类别(Category) 启用
详细级别(Verbosity Level) 启用
消息(Message) 启用
文件上下文(File Context) 禁用
函数名称(Function Name) 启用

9.5.1.1 时间戳(Timestamp)

日志时间戳遵循 ISO 8601 本地时间戳标准,即 YYYY-MM-DD hh:mm:ss.sss。此组件无法进一步配置或禁用。

9.5.1.2 类别(Category)

日志条目在通过《日志消息》中介绍的宏生成日志时会被分配一个类别。类别组件可用于过滤日志条目,以便只消费过滤器中指定的那些类别(参见《过滤器》)。此组件无法进一步配置或禁用。

9.5.1.3 详细级别(Verbosity Level)

eProsima Fast DDS 日志模块提供了由 Log::Kind 枚举定义的三个详细级别,如下所示:

  • Log::Kind::Error:用于记录错误消息。
  • Log::Kind::Warning:用于记录错误和警告消息。
  • Log::Kind::Info:用于记录错误、警告和信息消息。

日志模块的详细级别默认为 Log::Kind::Error,这意味着只有使用 EPROSIMA_LOG_ERROR 记录的消息才会被消费。可以使用成员函数 Log::SetVerbosity()Log::GetVerbosity() 分别设置和获取详细级别。

复制代码
// Set log verbosity level to Log::Kind::Info
Log::SetVerbosity(Log::Kind::Info);

// Get log verbosity level
Log::Kind verbosity_level = Log::GetVerbosity();

警告

将 CMake 选项 LOG_NO_INFOLOG_NO_WARNINGLOG_NO_ERROR 中的任何一个设置为 ON,都会完全禁用相应的详细级别。对于单配置生成器(Single-Config generators),如果不在 Debug 模式下,LOG_NO_INFO 默认设置为 ON

9.5.1.4 消息(Message)

此组件构成日志条目的主体。它是在通过《日志消息》中介绍的宏生成日志时指定的。消息组件可用于过滤日志条目,以便只消费其消息模式与过滤器匹配的条目(参见《过滤器》)。此组件无法进一步配置或禁用。

9.5.1.5 文件上下文(File Context)

此组件以文件名和行号的形式指定日志条目的来源(参见《日志消息》中带有此组件的日志条目示例)。这对于调试时跟踪代码流程很有用。可以使用成员函数 Log::ReportFilenames() 启用/禁用文件上下文组件。

复制代码
// Enable file name and line number reporting
Log::ReportFilenames(true);

// Disable file name and line number reporting
Log::ReportFilenames(false);

9.5.1.6 函数名称(Function Name)

此组件以函数名称的形式指定日志条目的来源(参见《日志消息》中带有此组件的日志条目示例)。这对于调试时跟踪代码流程很有用。可以使用成员函数 Log::ReportFunctions() 启用/禁用函数名称组件。

复制代码
// Enable function name reporting
Log::ReportFunctions(true);

// Disable function name reporting
Log::ReportFunctions(false);

9.5.2 注册消费者(Register Consumers)

eProsima Fast DDS 日志模块支持零个或多个消费者,用于记录使用《日志消息》中描述的方法在日志队列中注册的条目。要注册消费者,Log 类暴露了成员函数 Log::RegisterConsumer()

复制代码
// Create a FileConsumer consumer that logs entries in "archive.log"
std::unique_ptr<FileConsumer> file_consumer(new FileConsumer("archive.log"));
// Register the consumer. Log entries will be logged to STDOUT and "archive.log"
Log::RegisterConsumer(std::move(file_consumer));

可以使用成员函数 Log::ClearConsumers() 清空消费者列表。

复制代码
// Clear all the consumers. Log entries are discarded upon consumption.
Log::ClearConsumers();

注意

使用 Fast DDS XML 配置文件也可以注册和配置消费者。详情请参阅《XML 配置》。
警告

Log::ClearConsumers() 会清空消费者列表。所有日志条目都将被丢弃,直到通过 Log::RegisterConsumer() 注册新的消费者,或直到调用 Log::Reset()

9.5.3 重置配置(Reset Configuration)

可以使用成员函数 Log::Reset() 将日志模块的配置重置为默认设置。

警告

重置模块的配置会导致:

  • 将详细级别设置为 Log::Kind::Error
  • 禁用文件上下文组件。
  • 启用函数名称组件。
  • 清除所有过滤器。
  • 清除所有消费者,并根据 CMake 选项 LOG_CONSUMER_DEFAULT 重置默认消费者。

9.5.4 XML 配置(XML Configuration)

eProsima Fast DDS 允许使用 XML 配置文件注册和配置日志消费者。详情请参阅《日志配置文件》。

9.6 过滤器(Filters)

eProsima Fast DDS 日志模块允许在消费日志时进行日志条目过滤,从而可以将应用程序的执行输出限制在特定的关注区域。除了详细级别之外,Fast DDS 还提供了三种不同的过滤可能性。

值得一提的是,过滤器按照上述特定的顺序应用,这意味着文件名过滤仅应用于模式匹配类别过滤器的条目,而内容过滤仅应用于模式匹配类别和文件名过滤器的条目。

9.6.1 类别过滤(Category Filtering)

日志条目可以在消费时根据其类别组件使用正则表达式进行过滤。每当有条目准备消费时,都会使用 std::regex_search() 应用类别过滤器。要设置类别过滤器,可使用成员函数 Log::SetCategoryFilter()

复制代码
// Set filter using regular expression
Log::SetCategoryFilter(std::regex("(CATEGORY_1)|(CATEGORY_2)"));

// Would be consumed
EPROSIMA_LOG_ERROR(CATEGORY_1, "First log entry");
// Would be consumed
EPROSIMA_LOG_ERROR(CATEGORY_2, "Second log entry");
// Would NOT be consumed
EPROSIMA_LOG_ERROR(CATEGORY_3, "Third log entry");

上一个示例将产生以下输出:

复制代码
2020-05-27 15:07:05.771 [CATEGORY_FILTER_1 Error] First log entry -> Function main
2020-05-27 15:07:05.771 [CATEGORY_FILTER_2 Error] Second log entry -> Function main

9.6.2 文件名过滤(File Name Filtering)

日志条目可以在消费时根据其文件上下文组件使用正则表达式进行过滤。每当有条目准备消费时,都会使用 std::regex_search() 应用文件名过滤器。要设置文件名过滤器,可使用成员函数 Log::SetFilenameFilter()

复制代码
// Filename: example.cpp

// Enable file name and line number reporting
Log::ReportFilenames(true);

// Set filter using regular expression so filename must match "example"
Log::SetFilenameFilter(std::regex("example"));
// Would be consumed
EPROSIMA_LOG_ERROR(CATEGORY, "First log entry");

// Set filter using regular expression so filename must match "other"
Log::SetFilenameFilter(std::regex("other"));
// Would NOT be consumed
EPROSIMA_LOG_ERROR(CATEGORY, "Second log entry");

上一个示例将产生以下输出:

复制代码
2020-05-27 15:07:05.771 [CATEGORY Error] First log entry (example.cpp:50) -> Function main

注意

即使文件上下文条目组件被禁用,文件名过滤器也会被应用。

9.6.3 内容过滤(Content Filtering)

日志条目可以在消费时根据其消息组件使用正则表达式进行过滤。每当有条目准备消费时,都会使用 std::regex_search() 应用内容过滤器。要设置内容过滤器,可使用成员函数 Log::SetErrorStringFilter()

复制代码
// Set filter using regular expression so message component must match "First"
Log::SetErrorStringFilter(std::regex("First"));
// Would be consumed
EPROSIMA_LOG_ERROR(CATEGORY, "First log entry");
// Would NOT be consumed
EPROSIMA_LOG_ERROR(CATEGORY, "Second log entry");

上一个示例将产生以下输出:

复制代码
2020-05-27 15:07:05.771 [CATEGORY Error] First log entry -> Function main

9.6.4 重置日志过滤器(Reset Logging Filters)

可以使用成员函数 Log::Reset() 重置日志模块的过滤器。

警告

重置模块的过滤器会导致:

  • 将详细级别设置为 Log::Kind::Error
  • 禁用文件上下文组件。
  • 启用函数名称组件。
  • 清除所有过滤器。
  • 清除所有消费者,并根据 CMake 选项 LOG_CONSUMER_DEFAULT 重置默认消费者。

9.7 消费者(Consumers)

消费者是接收 Log::Entry 并相应地生成日志输出的类。eProsima Fast DDS 提供了三种不同的日志消费者,将日志条目输出到不同的流:

  • StdoutConsumer :将日志条目输出到 STDOUT
  • StdoutErrConsumer :根据给定的阈值,将日志条目输出到 STDOUTSTDERR
  • FileConsumer:将日志条目输出到用户指定的文件。

9.7.1 StdoutConsumer

StdoutConsumer 将日志条目按照《日志条目规范》中规定的格式输出到 STDOUT 流。如果 CMake 选项 LOG_CONSUMER_DEFAULT 设置为 STDOUT,则它是日志模块的默认消费者。可以使用《注册消费者》和《重置配置》中说明的方法进行注册和注销。

复制代码
// Create a StdoutConsumer consumer that logs entries to stdout stream.
std::unique_ptr<StdoutConsumer> stdout_consumer(new StdoutConsumer());

// Register the consumer.
Log::RegisterConsumer(std::move(stdout_consumer));

9.7.2 StdoutErrConsumer

StdoutErrConsumer 使用 Log::Kind 阈值来过滤日志条目的输出。那些 Log::Kind 等于或比给定阈值更严重的日志条目将输出到 STDERR。其他日志条目输出到 STDOUT。如果 CMake 选项 LOG_CONSUMER_DEFAULT 设置为 AUTOSTDOUTERR 或未设置,则它是日志模块的默认且唯一的日志消费者。默认情况下,阈值设置为 Log::Kind::WarningStdoutErrConsumer::stderr_threshold() 允许用户修改默认阈值。

此外,如果 CMake 选项 LOG_CONSUMER_DEFAULT 设置为 STDOUTERR,日志模块将使用此消费者作为默认日志消费者。

复制代码
// Create a StdoutErrConsumer consumer that logs entries to stderr only when the Log::Kind is equal to ERROR
std::unique_ptr<StdoutErrConsumer> stdouterr_consumer(new StdoutErrConsumer());
stdouterr_consumer->stderr_threshold(Log::Kind::Error);

// Register the consumer
Log::RegisterConsumer(std::move(stdouterr_consumer));

9.7.3 FileConsumer

FileConsumer 为日志模块提供了记录到文件的日志记录功能。希望保存持久执行日志记录的应用程序可以使用此消费者指定一个日志文件。此外,应用程序可以根据 std::fstream::open() 定义的行为,选择文件流应处于"写入"还是"追加"模式。

复制代码
// Create a FileConsumer consumer that logs entries in "archive_1.log", opening the file in "write" mode.
std::unique_ptr<FileConsumer> write_file_consumer(new FileConsumer("archive_1.log", false));

// Create a FileConsumer consumer that logs entries in "archive_2.log", opening the file in "append" mode.
std::unique_ptr<FileConsumer> append_file_consumer(new FileConsumer("archive_2.log", true));

// Register the consumers.
Log::RegisterConsumer(std::move(write_file_consumer));
Log::RegisterConsumer(std::move(append_file_consumer));

9.8 禁用日志模块(Disable Logging Module)

设置详细级别会导致当日志条目的级别重要性低于所设置的级别时,该条目不会被添加到日志队列中。这种检查在调用《日志消息》中定义的宏时执行。然而,可以在构建时完全禁用每个宏(从而分别禁用每个详细级别)。

EPROSIMA_LOG_INFO 可以通过以下任一方式完全禁用:

  • 将 CMake 选项 LOG_NO_INFO 设置为 ON(对于单配置生成器,如果 CMAKE_BUILD_TYPE 不是 Debug,则默认为 ON)。
  • 将宏 HAVE_LOG_NO_INFO 定义为 1

EPROSIMA_LOG_WARNING 可以通过以下任一方式完全禁用:

  • 将 CMake 选项 LOG_NO_WARNING 设置为 ON
  • 将宏 HAVE_LOG_NO_WARNING 定义为 1

EPROSIMA_LOG_ERROR 可以通过以下任一方式完全禁用:

  • 将 CMake 选项 LOG_NO_ERROR 设置为 ON
  • 将宏 HAVE_LOG_NO_ERROR 定义为 1

应用上述任何一种方法都会在配置时将宏设置为空,从而允许编译器优化掉对该宏的调用。这样做是为了让库中所有的调试消息在不是为调试目的构建时,在构建阶段就被优化掉,从而防止它们影响性能。

INTERNAL_DEBUG CMake 选项会激活日志宏的编译,因此宏的参数会被编译。然而:

  • 它不会激活日志的警告和错误消息,即这些消息不会被写入日志队列。

EPROSIMA_LOG_INFO 有一个特殊的行为,以简化与多配置能力 IDE 的协作。如果 CMake 选项 LOG_NO_INFOOFF,或者 C++ 定义 HAVE_LOG_NO_INFO0,则日志仅在 Debug 配置下启用。在这种情况下,将 FASTDDS_ENFORCE_LOG_INFO 设置为 ON 将启用 EPROSIMA_LOG_INFO,即使在非 Debug 配置下也是如此。当在链接了以 Release 模式编译的 Fast DDS 的外部应用程序中使用 Fast DDS 的日志模块时,这尤其有用。在这种情况下,希望使用所有三个日志级别的应用程序只需在包含任何 Fast DDS 头文件之前添加以下代码:

复制代码
#define HAVE_LOG_NO_INFO 0
#define FASTDDS_ENFORCE_LOG_INFO 1

警告

如果 CMake 选项 EPROSIMA_BUILD 设置为 ONINTERNAL_DEBUG 可能会自动设置为 ON

9.9 禁用旧日志宏(Old Log macros disable)

在 2.8.2 版本之前,Fast DDS 项目使用了日志宏 logInfologWarninglogError,这些宏可能与其他库冲突。这些日志宏已被格式更具体的新宏所取代(例如 EPROSIMA_LOG_INFO)。为了禁用旧宏的编译,可以使用 CMake 选项 ENABLE_OLD_LOG_MACROS = ON,或者在包含日志模块 #include <fastdds/dds/log/Log.hpp> 之前定义 ENABLE_OLD_LOG_MACROS_ 0

警告

这些宏将在 Fast DDS 的未来版本中被弃用。建议使用新格式的宏。