C++_Spdlog 日志库

使用说明

1.	日志生产和日志消费
     auto console         = spdlog::stdout_color_mt("console"); //创建控制台日志器 将其注册到 spdlog 的全局注册表中,
	 auto rotating_logger = spdlog::rotating_logger_mt();  // 创建文件日志器。 将其注册到 spdlog 的全局注册表中,
	 auto async_file = spdlog::basic_logger_mt
	 
	 日志消息最终被封装成async_msg,并加入mpmc_queue这个队列
	 线程池的工作线程将不断从队列中取出async_msg消息,并根据异步日志的类型不同的处理
	Loggers :是 Spdlog 最基本的组件,负责记录日志消息。在 Spdlog 中,一个 Logger 对象代表着一个日志记录器 
	Sinks :决定了日志消息的输出位置。在 Spdlog 中,一个 Sink 对象代表着一个输出位置
	Async Logger :是 Spdlog 的异步记录器,它负责将日志消息异步地写入到目标 Sink 中
	    daily_file_sink是通过被动触发日志文件的分割的,当用户写入日志的时候,
	        daily_file_sink会通过日志消息的时间和计算的需要转轮的时间判断是否需要将日志输出到新的文件中
	    rotating_file_sink
		
		
		
2、日志级别

    spdlog::set_level(spdlog::level::debug);// 设置日志级别
    trace = SPDLOG_LEVEL_TRACE // 最低级(用来记录代码执行轨迹)
    debug = SPDLOG_LEVEL_DEBUG // (用来记录debug信息)
    info = SPDLOG_LEVEL_INFO // 在上面的测试例子中用过
    warn = SPDLOG_LEVEL_WARN
    err = SPDLOG_LEVEL_ERROR
    critical = SPDLOG_LEVEL_CRITICAL
    off = SPDLOG_LEVEL_OFF // 最高级
 
3.日志格式	
	格式化器 Formatters :负责将日志消息转换为特定格式。在 Spdlog 中,一个 Formatter 对象代表着一个消息格式器
      日期、时间、时区  进程号 、 线程号、文件名称、行号、函数名称、程序性能和调用频率 等调试信息
// 设置日志格式
    spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v");	  

原理说明

同步模式下, Spdlog 将日志消息直接写入目标 Sink,不使用内存队列
异步模式下,日志消息被加入到一个内存队列中,然后异步地写入目标 Sink
  spdlog的异步模式就是经典的生产者,消费者模式,
    前端通过logger的打印日志的log方法将日志消息写入队列,
     线程池中的后端线程不断从队列中取出异步消息,
	 根据异步消息调用async_logger本身的方法进行处理,
	这样就实现了异步的日志写入
   Spdlog 中,异步模式由 Async Logger 实现。Async Logger 在后台运行一个线程,负责从内存队列中获取日志消息,并将其写入目标 Sink 中。	
日志消息
    三种异步日志消息,log,flush,terminate	
      async_logger::flush_生成一个flush类型的async_msg,投递到线程池
类
    logger,async_logger,
	sink,base_sink,
	thread_pool和mpmc_blocking_queue 

组件

   Loggers     负责记录日志消息,
   Sinks       决定了日志消息的输出位置,
   Formatters  负责将日志消息转换为特定格式,
   Async Logger 异步地将日志消息写入到目标 Sink 中,
   Registry    用于管理这些组件 所有的 Loggers、Sinks、Formatters 和 Async Logger 都在一个全局注册表中注册,Registry 用于管理这些组件			

执行过程

spdlog 的流程如下:
   获取一个 Logger 对象。
   使用该 Logger 对象记录一个日志消息,该消息包括日志级别、时间戳、线程 ID、文件名和行号等信息。
   将日志消息传递给 Formatter,将消息转换为特定格式。
   将格式化后的消息传递给 Async Logger。
   Async Logger 将消息写入目标 Sink,完成日志记录。
   Spdlog 的流程非常简单,但是每个组件都扮演着重要的角色。

	spdlog通过让logger组合sink,sink组合formatter,并通过合理的职责划分和接口定义,实现了良好的可拓展性,
	   用户只需要继承sink接口,实现sink_it_和flush方法就可以实现自定义sink的实现,
	   实现formatter类的format函数就能实现自定义格式化器

源码阅读

梳理类和函数的调用链-分析协作机制
    自由创建线程安全和非线程安全(单线程)的日志,其设置在 基类base_skin 中
	sink都会继承 base_sink,通过模板参数 Mutex 传入锁
 
  Async Logger 可以配置多个 Sink,每个 Sink 都会有一个独立的内存队列。
         Async Logger 在后台运行一个线程,负责从内存队列中获取日志消息,并将其写入目标 Sink 中	  
  Spdlog 提供了两种内存队列实现:unbounded 和 bounded

logger类有几种构造函数,包括:
   • explicit logger(std::string name)              : 使用指定的名称和空接收器列表构造一个日志记录器。
   • logger(std::string name, sink_ptr single_sink) : 使用指定的名称和一个接收器构造一个日志记录器。
   • logger(std::string name, sinks_init_list sinks): 使用指定的名称和初始接收器列表构造一个日志记录器。

扩展

 自定义 sink 的通用步骤:
     继承 base_sink:创建一个新类,继承 base_sink<Mutex>,选择合适的互斥锁(如 std::mutex 用于多线程,null_mutex 用于单线程)。
	 实现 sink_it_:定义日志消息的处理逻辑,如格式化、加工或输出到目标(如文件、网络)。
	 实现 flush_:确保缓冲区数据被刷新到目标,完成输出。
	 管理资源:在构造函数中初始化资源(如文件句柄、缓冲区),在析构函数中清理。
	 提供工厂函数:为多线程和单线程模式定义便捷的创建函数(如 compressed_file_logger_mt/st)。

产品设计

1. registry帮我们创建了默认的logger和默认的sink,方便我们直接使用。让使用者易于上手,不必先了解logger、sink等概念
   简单-易用-懂用户
2.spdlog几乎为所有类型的sink都提供了类似的logger创建函数   
3. Factory::create。模板参数Factory都默认为 spdlog::synchronous_factory 还可以是 spdlog::async_factory
4.先看 synchronous_factory::create的实现,它是一个模板函数,接受两个模板参数Sink和SinkArgs
         模板来让synchronous_factory::create支持所有sink,先把logger构造出来后,再传进registry的initialize_logger方法中
	async_logger 中所使用的线程池的创建	 

使用和创建的原理

registry的代码主要在registry.h、registry-inl.h
Spdlog使用工厂方法创建logger, 并通过registry单例来管理logger
    用户每次使用时,可能需要先创建logger对象,然后通过logger对象来接收用户log消息
	    而库中 registry类,维护:
           1)一个默认的全局logger对象;
           2)一个logger对象的全局注册表,logger name就是其唯一标识;
           3)一个线程池,用于异步写log;
           4)一个周期工作线程,用于执行定时任务;
使用示例:
 1. spdlog::trace("hello trace");	
     // spdlog中存在一个默认logger可以直接使用,不需要我们进行配置
	 
	 
 2.通过工厂模式接口配置logger	  通过单例registry将logger找到,换句话说,每一个logger在创建时,都是向registry注册 
    namespace spdlog {
     template <typename Factory>
     SPDLOG_INLINE std::shared_ptr<logger> stdout_color_mt(const std::string &logger_name,
                                                           color_mode mode) {
         return Factory::template create<sinks::stdout_color_sink_mt>(logger_name, mode);
     }

     auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("console");
	 
	//这里是通过工厂模式的接口,有mt(异步)和st(同步)的接口。
     SPDLOG_INLINE std::shared_ptr<logger> get(const std::string &name) {
         return details::registry::instance().get(name);
     }
     //从这里可以看出,单例registry可以get到logger  

spdlog.h

details::registry::instance().initialize_logger(std::move(logger));	 
    return  details::registry::instance().get(name) 
details::registry::instance().flush_every(interval);

Factory::template create<>	

工程模式

 工厂类  synchronous_factory 
      模板函数 basic_logger_mt,并显式指定使用异步工厂类 spdlog::async_factory 来 创建并注册一个异步logger
    通过统一的create接口创建logger对象
     async_factory	
     async_factory	
	
     ( synchronous_factory 和 async_factory )的create方法用于在创建logger之后返回logger给用户之前,将其注册进registry
	 synchronous_factory.h

单例模式

registry提供全局唯一的注册表、默认的logger对象、缺省全局配置、后端线程池等唯一性资源,需要确保registry对象的唯一性	
  details::registry是spdlog中用来管理所有日志器的注册表;initialize_logger 将新建的日志器加入注册表,便于统一管理
  
//构造函数 
 registry::registry(): formatter_(new pattern_formatter())	

 // 单例模式惯用法实现registry
 class  registry
 {
 public:
     registry(const registry &) = delete;
     registry &operator=(const registry &) = delete; // 删除拷贝操作:= delete 拷贝构造函数和赋值运算符
     ...
     static registry &instance();
 private:
     registry(); // 构造函数私有化:防止外部创建实例
     ~registry();
 ...
  
  registry &registry::instance()
 {
     static registry s_instance;
     return s_instance;
 }
 
使用单例 
auto& instance = Singleton::getInstance();
instance.doSomething();

注册

先要通过register_logger将其加入注册表;如果后面想要获取,可以通过get接口得到。
1.logger对象注册表 loggers_
  std::unordered_map<std::string,std::shared_ptr<logger>> loggers_;
     default_logger_ = std::make_shared<spdlog::logger>(default_logger_name,std::move(color_sink));
     loggers_[default_logger_name] = default_logger_;

std::shared_ptr<logger> registry::get(const std::string &logger_name){
  std::lock_guard<std::mutex> lock(logger_map_mutex_);
  suto found = loggers_.find(logger_name);
  return found == logger_.end()?nullptr:found->second;
 }
 
registry::register_logger_(std::shared_ptr<logger> new_logger){
  auto logger_name = new_logger->name();
  throw_if_exists_(logger_name);
  loggers_[logger_name] = std::move(new_logger);
  
} 	
//先检测注册表中是否存在对应名称的logger对象,如果存在,就抛出异常;如果不存在,就加入。因此,需要十分注意:不能重复注册同一个logger名称的logger对象
//initialize_logger 中设置预置日志等级,只会更新单个logger对象的日志等级,而set_levels中设置,则会更新所有已注册logger对象的日志等级。
//initialize_logger 将新建的日志器加入注册表
void registry::initialize_logger(std::shared_ptr<logger> new_logger){
    std::lock_guard<std::mutex> lock(logger_matp_mutex_);
	new_logger->set_formatter(formatter_->clone());
    ^^^^…………………………………………^^^
	register_logger_(std::move(new_logger));

   }

手动创建记录器

 // 使用 spdlog 工厂方法创建的 logger 无需手动注册即可根据名称获取,手动创建的 logger 需要注册
  auto sink = std::make_shared<spdlog::sinks::stdout_sink_mt>();
  auto my_logger = std::make_shared<spdlog::logger>("mylogger", sink);
   
  // 可选择注册记录器。只有当你想用spdlog::get(“mylogger”)访问它时,才需要这样做spdlog::get("mylogger")
  spdlog::register_logger(my_logger);
  
  
  spdlog::drop("my_logger");//全局注册中删除指定 logger
  spdlog::drop_all();// 删除所有注册的 logger

   
  spdlog::flush_every(std::chrono::seconds(5));// 定期为所有注册的logger隔5秒刷新
  logger->flush_on(spdlog::level::warn);//遇到 warn 就立即刷新
  logger->flush()// logger 将依次在每个 sink 上调用 flush	  手动刷新 

版本

header-only version和compiled version两个版本	
C++ 的 header-only 总是把大量的实现代码放进头文件里,这种做法能成立
    是倚仗 C++ 的两个特性。
	一、 模板类和模板函数 的定义必须在头文件当中
	二、 非模板类和非模板函数以 inline 函数的形式,可以存在于头文件
	
	  C++中,通常的做法是将类的声明放在头文件中,定义放在源文件中
	  模板不是普通的类或函数,而是用于生成特定类型类或函数的“蓝图”。
	  模板的实例化是在编译时进行的,因此如果模板定义不在编译单元中可见,编译器就无法为其生成正确的代码。如果模板定义放在.h头文件中
	
	C语言的关键假设:C 语言当中不会出现,因为原则上 C 的头文件中只存在「声明」而没有「定义
    C++ 引入 inline 和模板,则打破了 C 的关键假设。C++ 在机制上允许多个目标文件中存在重复的符号定义,然后又通过「首次出现」这样的机制试图解决重复定义的解析问题

1.SPDLOG_COMPILED_LIB和SPDLOG_HEADER_ONLY两个宏定义
  核心机制:条件编译与宏控制  
     header-only  使用 inline  模板代码完全在头文件中展开,用户代码实例化时生成具体实现
	 Compiled模式  通过显式实例化减少编译时间(如预实例化常用类型)
    
2.代码
    header-only version,跟
	     async(async_logger及其它的工厂方法)相关的代码主要在async.h、async_logger.h、async_logger-inl.h三个文件中
    	header-only version,代码会被分在xxx.h和xxx-inl.h文件中,
		          基本上xxx.h只有函数和类的声明,
				  而实现都已inline的方式写在了xxx-inl.h中(此处inl就是inline的意思)
    compiled version的.cpp代码都在src文件夹下
	         async.cpp文件中就直接通过#include<xxx-inl.h>的方式“抄”过来
3.跨平台
   pdlog是支持多平台的,
      处理这部分差异的相关代码基本都在os.h和os-inl.h中了。也是通过宏定义实现,

参考

pdlog一个非常好用的C++日志库(六): 源码分析之registry类	 https://blog.csdn.net/haokan123456789/article/details/143895004
spdlog一个非常好用的C++日志库(五): 源码分析之线程池thread_pool https://blog.csdn.net/haokan123456789/article/details/140218937	
spdlog一个非常好用的C++日志库(一): 简介与使用 https://blog.csdn.net/haokan123456789/article/details/139841002   
杂谈:一个 C++ Header-only 版本冲突的案例分析 https://zhuanlan.zhihu.com/p/684965383 
 第三部分:Spdlog 日志库的实现原理 https://www.cnblogs.com/listenwind666/p/17262275.html	
 https://zhuanlan.zhihu.com/p/1897269661474719177
posted @ 2025-05-07 18:10  辰令  阅读(416)  评论(0)    收藏  举报