使用全局变量和函子实现日志记录

Implementing logging using globals and functors

本文关键字:实现 日志 记录 全局变量      更新时间:2023-10-16

我想实现具有以下特征的C++日志记录:

  • 它必须可用于所有源代码,而无需每个函数都有一个额外的参数(我认为这需要一个全局参数)
  • 日志记录调用
  • 可以指定严重性级别(信息、调试、警告等),并且可以在运行时设置日志记录工具以忽略低于特定严重性级别的调用
  • 可以在运行时将日志接收器设置为控制台或文件。

不需要的东西是:

  • 在运行时支持多个日志接收器(即,所有日志接收器都转到控制台或文件)
  • 支持多线程日志记录
  • 能够传递cout样式的表达式(例如 "foo=" << foo ) 在记录器调用中。我只会通过一个std::string.

找到了这个答案,它似乎可以满足我的需求,但它在我的脑海中有点超出。我认为我的困惑集中在函子上。(我读了维基百科的文章,但它显然没有沉入。

以下是我理解的部分:

  • 使用宏(例如.LOG_DEBUG)方便地指定严重性级别并调用记录器。
  • 使用 #ifdef NDEBUG 来防止编译日志记录调用(尽管我需要能够在运行时设置日志记录)。
  • 使用宏调用记录器的基本
  • 原理,以便它可以在调用记录器的位置自动且不可见地添加__FILE____LINE__等信息。
  • LOG 宏包含一个以 static_cast<std::ostringstream&> 开头的表达式。我认为这纯粹与评估 cout 样式的格式字符串有关,我不打算支持它。

这就是我挣扎的地方:

Logger& Debug() {
  static Logger logger(Level::Debug, Console);
  return logger;
}

阅读有关operator()的信息,看起来class Logger用于创建"函子"。每个记录器函子都使用一个级别和一个 LogSink 实例化 (?)。(你"实例化"一个函子吗?LogSink 被描述为"使用预格式化消息的后端",但我不知道它会是什么样子或它是如何"写入"的。静态记录器对象在什么时候实例化?是什么导致它被实例化?

这些宏定义...

#define LOG(Logger_, Message_)                   
  Logger_(                                       
    static_cast<std::ostringstream&>(            
       std::ostringstream().flush() << Message_  
    ).str(),                                     
    __FUNCTION__,                                
    __FILE__,                                    
    __LINE__                                     
  );
#define LOG_DEBUG(Message_) LOG(Debug(), Message_)

。还有这行代码...

LOG_DEBUG(my_message);

。预处理为:

Debug()(my_message, "my_function", "my_file", 42);

执行此操作时会发生什么?

格式化字符串如何以及在哪里实际写入"日志接收器"?

(注意:有人建议我看看 log4cpp - 我发现它比我需要的更大、更难理解,更不用说我将第三方库带入我们的环境所面临的政治问题)


更新:

为了理解上述解决方案的工作原理,我尝试编写一个最小完整的工作程序。我故意删除了以下内容:

  • 涉及STD::Ostringstream的"魔力"
  • #ifdef NDEBUG
  • 记录器级别类枚举
  • LogSink ctor 参数(现在我只写到 std::cout)

以下是完整的源文件:

#include <iostream>
#include <string>
class Logger {
public:
    Logger(int l);
    void operator()(std::string const& message,
                    char const* function,
                    char const* file,
                    int line);
private:
    int _level;
};
Logger::Logger(int l) :
    _level(l)
{ }
#define LOG(Logger_, Message_)  
    Logger_(                    
        Message_,               
        __FUNCTION__,           
        __FILE__,               
        __LINE__                
    )
#define LOG_DEBUG(Message_)     
    LOG(                        
        Debug(),                
        Message_                
    )
Logger& Debug() {
    static Logger logger(1);
    return logger;
}
// Use of Logger class begins here
int main(int argc, char** argv) {
    LOG_DEBUG("Hello, world!");
    return 0;
}

编译时:

$ c++ main.cpp
Undefined symbols for architecture x86_64:
  "Logger::operator()(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, char const*, char const*, int)", referenced from:
      _main in main-c81cf6.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

我看到没有定义一个函数来接受这四个参数并将它们写入std::cout,但是我需要定义的函数的名称是什么?

(到目前为止,我同意我应该使用 Boost::Log,但函子显然是一个我不完全理解的主题。

该函数Debug返回一个 Logger 对象(该对象是在第一次调用此函数时创建的)。

这个Logger对象似乎operator()()为它定义(从宏定义来看),这确实使它成为一个函子。顺便说一下,函子没什么特别的 - 它通过定义任何operator()()为它定义的类型。但是,您的分析似乎不正确。相反

LOG_DEBUG(my_message);

将扩展到

LOG(Debug(), Message_)

而那成

Debug()(Message_, __FUNCTION__, __FILE__, __LINE__);

在这里Debug()将返回一个已定义operator()()的对象,并且该对象将用于调用。

一些 QnA

为什么 Logger& Debug() 签名没有指定四个参数?

因为它不需要。Debug() 只是返回使用特定参数(日志级别和输出设备)创建的(静态)记录器对象。

静态记录器对象在什么时候实例化?是什么导致它被实例化?

当第一次调用 Debug() 函数时,它会初始化它的静态对象。这是静态函数变量的基础。

最后,但并非最不重要的。我个人觉得不值得努力编写自己的记录器。这是乏味且非常无聊的,除非您真的需要一些特别的东西。虽然我对 Boost.Log 和 log4cpp 并不疯狂,但我(事实上)肯定会使用其中之一而不是滚动我自己的记录器。即使是次优日志记录也比在自己的解决方案上花费数周要好。