spdlog一个非常好用的C++日志库(五): 源码分析之registry类

目录

1.registry类意义

2.registry类实现

2.1.registry数据成员

2.2.registry函数成员

2.2.1.构造与析构

2.2.2.单例模式

2.2.3.全局注册表

2.2.4.initialize_logger初始化logger对象

2.2.5.全局格式器

2.2.6.预置日志等级

2.2.7.flush日志等级

2.2.8.默认logger

2.2.9.在已注册logger上应用函数

2.2.10.回溯最近的log消息

2.3.线程安全

2.4.错误处理

2.5.定时工作类periodic_worker

3.registry类应用

3.1.initialize_logger初始化logger对象

3.2.get查找logger对象

3.3.set_formatter设置格式器

3.4.set_pattern设置格式串

3.5.全局backtrace操作

3.6.全局level操作

3.7.flush_every定时冲刷

3.8.drop清除logger

3.9.关闭log功能

3.10.设置自动注册

3.11.默认logger

3.12.写log

4.知识点

4.1.std::enable_if, is_convertible_to_any_format_string


1.registry类意义

已经有了用于接收前端用户log消息的类logger,代表log消息的类是log_msg,后端写log消息到目标文件的类sink,格式化log消息为最终字符串的类formatter,解析pattern flag的类pattern_formatter等等。但似乎还存在一个问题:库的使用者,如何使用它们?

每次使用时,可能需要先创建logger对象,然后通过logger对象来接收用户log消息。然而,当用户希望创建多个logger对象时,谁来组织logger对象?谁来辨识logger name对应logger对象是否已经创建?如何通过logger name获取对应logger对象?能否有一个全局的默认logger,用户不需要做复杂设置?

这就可以用registry类,维护:
1)一个默认的全局logger对象;
2)一个logger对象的全局注册表,logger name就是其唯一标识;
3)一个线程池,用于异步写log;
4)一个周期工作线程,用于执行定时任务;

2.registry类实现

2.1.registry数据成员

registry是一个综合性的类,包含多种功能。其核心功能是提供全局logger对象注册表,主要数据成员,分为这几部分:
1)logger对象注册表loggers_,map存放;
2)缺省的logger对象default_logger_,便捷接口;
3)缺省的formatter格式器formatter_,通常为pattern_formatter,方便编译pattern字符串;
4)一个线程池tp_,用于异步写log;
5)线程安全,3个不同的锁用于保护数据;
6)异常处理,缺省的全局配置,以及错误回调;
7)杂项;

具体来说:

class SPDLOG_API registry
{
private:// logger_map_mutex_是loggers_的互斥锁, flusher_mutex_是periodic_flusher_的互斥锁std::mutex logger_map_mutex_, flusher_mutex_; std::recursive_mutex tp_mutex_;              // 线程池tp_的递归锁std::unordered_map<std::string, std::shared_ptr<logger>> loggers_; // logger对象注册表, 存放(logger name, logger对象共享指针)log_levels log_levels_;                      // 用户事先指定的log等级, map存放(logger name, log level)std::unique_ptr<formatter> formatter_;       // pattern 格式器, 默认为pattern_formatter实例spdlog::level::level_enum global_log_level_ = level::info; // 全局log等级值, 新logger对象缺省log等级level::level_enum flush_level_ = level::off; // flush等级值, 新logger对象缺省flush等级err_handler err_handler_;                    // 错误处理, 发生错误时回调std::shared_ptr<thread_pool> tp_;            // 线程池, 用于异步写log, 但registry并不是线程池创建者std::unique_ptr<periodic_worker> periodic_flusher_;        // 定时flushstd::shared_ptr<logger> default_logger_;     // 默认的logger对象bool automatic_registration_ = true;         // 自动注册logger对象选项size_t backtrace_n_messages_ = 0;            // 回溯消息个数, 每个logger对象都可以设置一个环形缓冲区
};

2.2.registry函数成员

2.2.1.构造与析构

构造函数创建一个默认的logger对象,将stdout作为输出目标。formatter_也是用默认的pattern_formatter类构造,用于所有注册的logger对象作为编译pattern字符串的对象。

// 构造函数
SPDLOG_INLINE registry::registry(): formatter_(new pattern_formatter())
{
#ifndef SPDLOG_DISABLE_DEFAULT_LOGGER // 控制是否关闭default logger对象// 针对不同的平台, 创建不同的sink, 作为logger输出目标// create default logger (ansicolor_stdout_sink_mt or wincolor_stdout_sink_mt  in windows).
#    ifdef _WIN32 // Windows平台auto color_sink = std::make_shared<sinks::wincolor_stdout_sink_mt>();
#    else         // non-Windows平台auto color_sink = std::make_shared<sinks::ansicolor_stdout_sink_mt>();
#    endifconst char *default_logger_name = ""; // default logger名称default_logger_ = std::make_shared<spdlog::logger>(default_logger_name,  std::move(color_sink));loggers_[default_logger_name] = default_logger_; // 加入注册表
#endif // SPDLOG_DISABLE_DEFAULT_LOGGER
}// 默认合成的析构函数
SPDLOG_INLINE registry::~registry() = default;

2.2.2.单例模式

registry为何要实现为单例?
因为registry提供全局唯一的注册表、默认的logger对象、缺省全局配置、后端线程池等唯一性资源,需要确保registry对象的唯一性。

registry类实现单例模式,是采用典型的惯用实现方法。我之前在这篇文章设计模式之单例模式中分析过,这种方式创建的单例模式并不是线程安全的。

// 单例模式惯用法实现registry
class SPDLOG_API registry
{
public:registry(const registry &) = delete;registry &operator=(const registry &) = delete;...static registry &instance();
private:registry();~registry();
...SPDLOG_INLINE registry &registry::instance()
{static registry s_instance;return s_instance;
}

2.2.3.全局注册表

作为一个单例模式,registry确保了自身全局唯一实例。而registry通过维护一个map结构的注册表loggers_,存放logger对象,索引key是logger name。当用户新建了一个logger对象,想要在注册表中搜到,先要通过register_logger将其加入注册表;如果后面想要获取,可以通过get接口得到。

public:// 注册一个logger对象到注册表void register_logger(std::shared_ptr<logger> new_logger);std::shared_ptr<logger> get(const std::string &logger_name);...
private:std::unordered_map<std::string, std::shared_ptr<logger>> loggers_; // logger对象注册表, 存放(logger name, logger对象)
  • 注册logger对象

注册logger对象的实现register_logger很简单,就是先检测注册表中是否存在对应名称的logger对象,如果存在,就抛出异常;如果不存在,就加入。因此,需要十分注意:不能重复注册同一个logger名称的logger对象。

// 注册logger对象
SPDLOG_INLINE void registry::register_logger(std::shared_ptr<logger> new_logger)
{std::lock_guard<std::mutex> lock(logger_map_mutex_); // 由于是public接口,先获得锁,再对loggers_进行操作register_logger_(std::move(new_logger));
}// private实现
SPDLOG_INLINE void registry::register_logger_(std::shared_ptr<logger> new_logger)
{auto logger_name = new_logger->name();throw_if_exists_(logger_name); // 确保logger name唯一loggers_[logger_name] = std::move(new_logger);
}// 如果logger_name对应项存在, 就抛出异常
SPDLOG_INLINE void registry::throw_if_exists_(const std::string &logger_name)
{if (loggers_.find(logger_name) != loggers_.end()){throw_spdlog_ex("logger with name '" + logger_name + "' already exists");}
}
  • 查找logger对象

get实现查找logger对象更简单,直接对map的结构loggers_根据key(logger name)进行查找。

// 根据logger_name查找logger对象
SPDLOG_INLINE std::shared_ptr<logger> registry::get(const std::string  &logger_name)
{std::lock_guard<std::mutex> lock(logger_map_mutex_); // 由于是public接口,先获得锁,再对loggers_进行操作auto found = loggers_.find(logger_name);return found == loggers_.end() ? nullptr : found->second;
}

2.2.4.initialize_logger初始化logger对象

当用户新建一个logger对象后,该如何设置其属性成员呢?
registry通过initialize_logger接口,为其提供默认初始化方式,无需为每个属性再单独设置。使用initialize_logger初始化的logger对象,会自动添加进全局注册表;如果是手动初始化的logger对象,则需要手动添加进全局注册表。

// 初始化一个logger对象new_logger
SPDLOG_INLINE void registry::initialize_logger(std::shared_ptr<logger> new_logger)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);new_logger->set_formatter(formatter_->clone());if (err_handler_) // 注意:err_handler_受互斥锁logger_map_mutex_保护{new_logger->set_error_handler(err_handler_);}// 判断用户是否有事先指定new_logger的log level, 如果有就按事先指定的设置; 如果没有, 就用默认的// set new level according to previously configured level or default levelauto it = log_levels_.find(new_logger->name());auto new_level = it != log_levels_.end() ? it->second : global_log_level_;new_logger->set_level(new_level);// 设置flush levelnew_logger->flush_on(flush_level_);// 设置环形缓冲区, 用于回溯最近的log消息if (backtrace_n_messages_ > 0){new_logger->enable_backtrace(backtrace_n_messages_);}// 自动注册进全局注册表if (automatic_registration_){register_logger_(std::move(new_logger));}
}

2.2.5.全局格式器

每个logger对象都拥有自己的格式器(formatter)成员,也就是说,能指定各自的pattern,从而决定输出的log消息格式。但如果想通过一个接口,为所有logger设置格式器,该怎么办?
此时,可以用到registry的全局格式器,接口set_formatter能为所有已注册的logger对象更新格式器。

// 为所有已注册的logger对象设置格式器
// Set global formatter. Each sink in each logger will get a clone of this object
SPDLOG_INLINE void registry::set_formatter(std::unique_ptr<formatter> formatter)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);formatter_ = std::move(formatter);// 为所有注册表loggers_中的logger对象更新formatterfor (auto &l : loggers_){l.second->set_formatter(formatter_->clone()); // 注意这类调用了formatter::clone深度克隆对象}
}

2.2.6.预置日志等级

  • 预置日志等级

spdlog支持事先为logger对象预置日志等级,待到新建的logger对象用initialize_logger()初始化时,为其指定log level。也就是说,此时尚未创建logger对象。

预置的日志等级log_levels_是log_levels类型,本质上一个unordered_map,key是logger name, value是log level枚举值。也就是说,spdlog只支持内置的日志等级(已经定义的那几个)。

using log_levels = std::unordered_map<std::string, level::level_enum>;

预置日志等级工作原理:通过环境变量或main启动参数,为logger对象指定log level,通过registry::set_level将其存放进map log_levels_中。当调用initialize_logger对logger对象进行初始化时,会从log_levels_中查找是否已经预置log level,如果已经预置,就直接用预置的日志等级初始化当前新建的logger对象;如果没有预置,就用缺省的global_log_level_来设置。

SPDLOG_INLINE void registry::initialize_logger(std::shared_ptr<logger> new_logger)
{...// 根据预置的log level或缺省的level, 来设置新logger对象的level// set new level according to previously configured level or default levelauto it = log_levels_.find(new_logger->name());auto new_level = it != log_levels_.end() ? it->second : global_log_level_;new_logger->set_level(new_level);...
}
  • 更新日志等级

程序启动后,或者logger对象加入注册表后,有没有办法根据修改的预置日志等级更新?
答案是有的,可以通过registry::set_levels为所有已注册logger对象更新预置日志等级。

initialize_logger中设置预置日志等级,只会更新单个logger对象的日志等级,而set_levels中设置,则会更新所有已注册logger对象的日志等级。

// 更新日志等级
// 当想为已注册logger对象更新时,指定levels即可;当想更新全局日志等级时,需要指定global_level
SPDLOG_INLINE void registry::set_levels(log_levels levels, level::level_enum  *global_level)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);log_levels_ = std::move(levels);auto global_level_requested = global_level != nullptr;global_log_level_ = global_level_requested ? *global_level :  global_log_level_;// 为所有已注册logger对象更新日志等级, 优先用预置的, 没有找到预置时, 再用全局的for (auto &logger : loggers_){auto logger_entry = log_levels_.find(logger.first);if (logger_entry != log_levels_.end()){logger.second->set_level(logger_entry->second);}else if (global_level_requested){logger.second->set_level(*global_level);}}
}

2.2.7.flush日志等级

spdlog支持单独flush(冲刷)指定level的日志消息(与记录日志的level分开)到目标文件,registry提供flush_on接口,设置(注册的)logger对象全局的flush_level_。

// 为每个已注册logger对象设置flush等级
// 当各个logger对象调用自身flush函数时, 会根据该flush等级与log消息的等级判断是否应该将文件立即冲刷到目标文件
SPDLOG_INLINE void registry::flush_on(level::level_enum log_level)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);for (auto &l : loggers_){l.second->flush_on(log_level);}flush_level_ = log_level;
}

flush_level_并不是单独工作,而是搭配定时器periodic_flusher_,定时调用flush_every,从而实现定时冲刷指定level的log消息。

// 指定一个周期性工作的线程, 定时flush所有loggertemplate<typename Rep, typename Period>void flush_every(std::chrono::duration<Rep, Period> interval){std::lock_guard<std::mutex> lock(flusher_mutex_);auto clbk = [this]() { this->flush_all(); };periodic_flusher_ = details::make_unique<periodic_worker>(clbk, interval);}// flush所有已注册logger对象的log消息到目标文件
// 要求logger对象必须是线程安全的
SPDLOG_INLINE void registry::flush_all()
{std::lock_guard<std::mutex> lock(logger_map_mutex_);for (auto &l : loggers_){l.second->flush();}
}

2.2.8.默认logger

为了方便用户spdlog,registry提供了默认logger对象default_logger_,无需用户再手动创建、设置。default_logger_的默认目标(sink)是控制台(stdout),可以很方便用户调试。

  • 获取默认的logger

获取默认的logger对象,有两种方式:
1)default_logger()获取共享指针管理的logger对象,线程安全;
2)通过shared_ptr<>::get()获取raw pointer,速度default_logger()比更快,但无法与set_default_logger()并发调用。

// 获取shared_ptr管理的默认logger, 线程安全
SPDLOG_INLINE std::shared_ptr<logger> registry::default_logger()
{std::lock_guard<std::mutex> lock(logger_map_mutex_);return default_logger_;
}// 获取指向默认logger的raw pointer, 非线程安全, 但速度更快
// Return raw ptr to the default logger.
// To be used directly by the spdlog default api (e.g. spdlog::info)
// This make the default API faster, but cannot be used concurrently with  set_default_logger().
// e.g do not call set_default_logger() from one thread while calling  spdlog::info() from another.
SPDLOG_INLINE logger *registry::get_default_raw()
{return default_logger_.get();
}
  • 更新默认的logger

如果用户不想使用默认的logger对象,或者说,在不改变接口情况下,想将其替换成用户自定logger对象,那么可以调用registry::set_default_logger。
set_default_logger的实现很简单,思路是:先从全局注册表loggers_移除现有的默认logger对象,然后将新对象new_default_logger添加进注册表,最后将default_logger_指向新对象new_default_logger。

// set default logger.
// default logger is stored in default_logger_ (for faster retrieval) and in the loggers_ map.
SPDLOG_INLINE void registry::set_default_logger(std::shared_ptr<logger> new_default_logger)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);// 先从注册表删除// remove previous default logger from the mapif (default_logger_ != nullptr){loggers_.erase(default_logger_->name());}// 再将新的new_default_logger加入注册表if (new_default_logger != nullptr){loggers_[new_default_logger->name()] = new_default_logger;}// 更新默认的loggerdefault_logger_ = std::move(new_default_logger);
}

2.2.9.在已注册logger上应用函数

有时,用户希望将已注册的logger对象作为参数,调用一个自定义函数。此时,可以用到apply_all接口。
实现思路是:将自定义函数作为函数参数,传入apply_all,对每个已注册的logger对象都调用fun()。

// 将已注册logger作为参数, 应用到函数fun
SPDLOG_INLINE void registry::apply_all(const std::function<void(const  std::shared_ptr<logger>)> &fun)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);for (auto &l : loggers_){fun(l.second);}
}

2.2.10.回溯最近的log消息

logger对象包含backtracer(环形队列)成员用于回溯最近的log消息,registry提供enable_backtrace接口,方便用户设置、开启该功能。

  • 开启回溯功能

开启回溯功能,实际上是将用户指定的消息条数转发给已注册logger,以开启并设置队列大小。

// 开启回溯功能
// 指定logger对象的环形缓冲区大小,设置了大小才能回溯最近的log消息
SPDLOG_INLINE void registry::enable_backtrace(size_t n_messages)
{std::lock_guard<std::mutex> lock(logger_map_mutex_); // 加锁确保线程安全backtrace_n_messages_ = n_messages;for (auto &l : loggers_){l.second->enable_backtrace(n_messages);}
}
  • 关闭回溯功能

关闭回溯功能,类似于开启,都是转发给已注册的logger,进行相应的设置。

// 关闭回溯功能
SPDLOG_INLINE void registry::disable_backtrace()
{std::lock_guard<std::mutex> lock(logger_map_mutex_);backtrace_n_messages_ = 0;for (auto &l : loggers_){l.second->disable_backtrace();}
}

2.3.线程安全

registry用了2把互斥锁(std::mutex) + 1把递归锁(std::recursive_mutex)。总原则: logger_map_mutex_保护几乎所有数据成员,flusher_mutex_和tp_mutex_分别只保护periodic_flusher_、tp_。

tp_mutex_为何用递归锁,而不用普通互斥锁?
因为tp_mutex_保护的是线程池tp_,registry为其提供了public接口tp_mutex()导致该锁可能会被多个线程访问,get_tp()接口导致可能多次加锁。由于线程池对象tp_创建后不再修改(除了释放),因此多线程环境下并发访问通常也是安全的,因此使用递归锁没有问题。

2.4.错误处理

logger对象本身包含错误回调err_handler_,registry的err_handler_存在意义就是为logger的错误回调提供缓存及用户接口。

  • 设置错误回调

可通过registry的set_error_handler接口,为每个logger对象更新错误回调。

SPDLOG_INLINE void registry::set_error_handler(err_handler handler)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);for (auto &l : loggers_){l.second->set_error_handler(handler);}err_handler_ = std::move(handler);
}
  • 什么时候调用错误回调?

对logger的错误回调,实际上包装到了用于异常处理的宏SPDLOG_LOGGER_CATCH中,当catch到异常时,就会回调err_handler_。如果用户通过registry::set_error_handler提供了自定义错误处理函数,那么就调用用户的;如果没有提供,就调用默认的logger::err_handler_,在控制台打印错误信息。

#ifndef SPDLOG_NO_EXCEPTIONS
#    define SPDLOG_LOGGER_CATCH(location)                                                                                        \catch (const std::exception &ex)                                                                                         \{                                                                                                                        \if (location.filename)                                                                                               \{                                                                                                                    \err_handler_(fmt_lib::format(SPDLOG_FMT_STRING("{} [{}({})]"),  ex.what(), location.filename, location.line));   \}                                                                                                                    \else                                                                                                                 \{                                                                                                                    \err_handler_(ex.what());                                                                                         \}                                                                                                                    \}                                                                                                                        \catch (...)                                                                                                              \{                                                                                                                        \err_handler_("Rethrowing unknown exception in logger");                                                              \throw;                                                                                                               \}
#else
#    define SPDLOG_LOGGER_CATCH(location)
#endif

例如,loger在将log消息转交给sink,将内容写到文件的时候,可能发生写失败的错误,抛出异常;在registry查找指定logger name不存在的时候,抛出异常。

// 在注册表查找logger name
SPDLOG_INLINE void registry::throw_if_exists_(const std::string &logger_name)
{if (loggers_.find(logger_name) != loggers_.end()){ // 不存在则抛出异常throw_spdlog_ex("logger with name '" + logger_name + "' already exists");}
}

common模块为抛出异常提供便捷接口,有2个重载版本,分别针对系统调用错误、非系统调用错误。

#    define SPDLOG_TRY try
#    define SPDLOG_THROW(ex) throw(ex)
#    define SPDLOG_CATCH_STD catch (const std::exception &) {}[[noreturn]] SPDLOG_API void throw_spdlog_ex(const std::string &msg, int  last_errno);
[[noreturn]] SPDLOG_API void throw_spdlog_ex(std::string msg);// 系统调用错误 抛出异常
SPDLOG_INLINE void throw_spdlog_ex(const std::string &msg, int last_errno)
{SPDLOG_THROW(spdlog_ex(msg, last_errno));
}// 非系统调用错误 抛出异常
SPDLOG_INLINE void throw_spdlog_ex(std::string msg)
{SPDLOG_THROW(spdlog_ex(std::move(msg)));
}

2.5.定时工作类periodic_worker

registry::flush_every中,创建了一个定时工作类periodic_worker对象。该类构造时,会启动一个线程,在其中利用锁+条件变量定时执行用户回调,从而实现用户自定义任务的定时执行。

// 定时工作者类periodic_worker, 为用户自定义任务fun提供定时回调功能
class SPDLOG_API periodic_worker
{
public:// 构造函数template<typename Rep, typename Period>periodic_worker(const std::function<void()> &callback_fun,  std::chrono::duration<Rep, Period> interval);// 禁用copy操作periodic_worker(const periodic_worker &) = delete;periodic_worker &operator=(const periodic_worker &) = delete;// stop the worker thread and join it~periodic_worker();private:bool active_;std::thread worker_thread_;std::mutex mutex_;std::condition_variable cv_;
};
  • 构造函数

注意到periodic_worker构造函数是一个函数模板,这是为什么?
因为用户传入的时间间隔参数(interval)的类型并不确定,可能是秒(chrono::seconds),也可能是毫秒(chrono::milliseconds),或者分钟(chrono::minutes)等等,为了兼容这些类型,因而使用模板来支持。

    template<typename Rep, typename Period>periodic_worker(const std::function<void()> &callback_fun,  std::chrono::duration<Rep, Period> interval) {active_ = (interval > std::chrono::duration<Rep, Period>::zero());if (!active_){return;}// for循环+互斥锁+条件变量 实现子线程主循环, 以定时执行callback_funworker_thread_ = std::thread([this, callback_fun, interval]() {for (;;){std::unique_lock<std::mutex> lock(this->mutex_);// wait_for等到超时, 返回cv_status::timeout(true)// 如果wait_for第三个参数如果返回true, 则不再等待; 如果返回false, 则继续等待// active_ 控制线程(循环)是否退出if (this->cv_.wait_for(lock, interval, [this] { return !this->active_; })){return; // active_ == false, so exit this thread}callback_fun();}});}
  • 析构函数

析构函数就是让子线程退出死循环,结束线程运行;母线程连接子线程以回收资源。

// stop the worker thread and join it
SPDLOG_INLINE periodic_worker::~periodic_worker()
{if (worker_thread_.joinable()) // 只有线程可连接情况下, 连接线程才有意义{{std::lock_guard<std::mutex> lock(mutex_);// 为什么active_使用bool类型, 而不用atomic<bool>类型?// 因为进入子线程后, 只有periodic_worker对象析构时才会写active, 其他情况只读, 不会造成线程安全问题active_ = false; }cv_.notify_one();worker_thread_.join();}
}

3.registry类应用

虽然registry是单例模式,但spdlog并没有打算让用户直接像下面这种方式,调用registry对库进行,而是提供了便捷的全局接口。实现方式,是在registry对应接口基础上,进行了包装,简化了调用。

3.1.initialize_logger初始化logger对象

比如,初始化一个logger对象,如果直接使用registry,就得这样:

details::registry::instance().initialize_logger(std::move(logger));

spdlog提供了便捷接口:

// 初始化、注册一个logger对象// Example:
//   auto mylogger = std::make_shared<spdlog::logger>("mylogger", ...);
//   spdlog::initialize_logger(mylogger);
SPDLOG_INLINE void initialize_logger(std::shared_ptr<logger> logger)
{details::registry::instance().initialize_logger(std::move(logger));
}

3.2.get查找logger对象

// 根据logger name查找已注册logger对象
SPDLOG_INLINE std::shared_ptr<logger> get(const std::string &name)
{return details::registry::instance().get(name);
}

3.3.set_formatter设置格式器

由于logger对象构建时,就必须有格式器,因此set_formatter只是更新其格式器。

// 为所有已注册logger对象设置新的格式器formatter
SPDLOG_INLINE void set_formatter(std::unique_ptr<spdlog::formatter> formatter)
{details::registry::instance().set_formatter(std::move(formatter));
}

3.4.set_pattern设置格式串

logger对象需要pattern,利用formatter根据pattern将log消息转换最终期望的log消息格式。便捷全局接口set_pattern,实际上利用set_formatter向logger传递一个pattern_formatter对象。其原理类似于logger::set_pattern实现。不同点在于:logger::set_pattern是针对单个logger对象,而set_pattern是针对所有已注册logger对象。

// 设置全局格式串
SPDLOG_INLINE void set_pattern(std::string pattern, pattern_time_type time_type)
{set_formatter(std::unique_ptr<spdlog::formatter>(new  pattern_formatter(std::move(pattern), time_type)));
}

3.5.全局backtrace操作

我们知道registry提供了开启、关闭backtrace(回溯)log消息功能。全局backtrace接口,也只是利用registry单例进行转发。还有一个比较特殊的,那就是如何将回溯栈的log消息转档(dump)?

SPDLOG_INLINE void enable_backtrace(size_t n_messages)
{details::registry::instance().enable_backtrace(n_messages);
}SPDLOG_INLINE void disable_backtrace()
{details::registry::instance().disable_backtrace();
}

转档回溯栈,实际上是通过logger::dump_backtrace对环形队列的每个元素(log消息),调用自定义函数,将其写到指定目标。

// 转档回溯栈
// default_logger_raw()实际上是registry::default_logger_所指logger对象, 全局唯一
SPDLOG_INLINE void dump_backtrace()
{default_logger_raw()->dump_backtrace();
}// 转发给私有实现
SPDLOG_INLINE void logger::dump_backtrace()
{dump_backtrace_();
}// 转档回溯栈tracer_中的log消息
SPDLOG_INLINE void logger::dump_backtrace_()
{using details::log_msg;if (tracer_.enabled()){sink_it_(log_msg{name(), level::info, "****************** Backtrace Start  ******************"});// 弹出回溯栈所有元素并回调lambda, 将log消息写入每个sink对应的目标文件tracer_.foreach_pop([this](const log_msg &msg) { this->sink_it_(msg); });sink_it_(log_msg{name(), level::info, "****************** Backtrace End  ********************"});}
}

可以看到dump_backtrace_中的关键,是logger::sink_it_以及details::backtracer::foreach_pop。
sink_it_是logger一个私有virtual函数,logger提供了公共实现,将log消息写到所有sink目标上,属于同步操作;子类可以继承,也可以重写,例如子类async_logger也实现了一个版本,不过是将log消息添加到环形队列,然后交给线程池处理。

// logger类对sink_it_的实现, 提供公共实现方式
SPDLOG_INLINE void logger::sink_it_(const details::log_msg &msg)
{// 将log消息msg写到每个目标sink上for (auto &sink : sinks_){if (sink->should_log(msg.level)){SPDLOG_TRY{sink->log(msg);}SPDLOG_LOGGER_CATCH(msg.source)}}// 如果msg的flush level满足条件, 就冲刷所有sinkif (should_flush_(msg)){flush_();}
}// 子类async_logger的sink_it, 为异步写log消息提供实现
// send the log message to the thread pool
SPDLOG_INLINE void spdlog::async_logger::sink_it_(const details::log_msg &msg)
{if (auto pool_ptr = thread_pool_.lock()){// post log消息到环形队列, 交给线程池处理pool_ptr->post_log(shared_from_this(), msg, overflow_policy_);}else // 线程池thread_pool_ 已经释放{throw_spdlog_ex("async log: thread pool doesn't exist anymore");}
}

foreach_pop是通过不断从环形队列头弹出元素(log消息),然后作为fun的参数进行回调的。该操作会清空回溯栈内容。

// pop all items in the q and apply the given fun on each of them.
SPDLOG_INLINE void backtracer::foreach_pop(std::function<void(const  details::log_msg &)> fun)
{std::lock_guard<std::mutex> lock{mutex_};// 从环形队列头循环取出元素并回调funwhile (!messages_.empty()){auto &front_msg = messages_.front();fun(front_msg);messages_.pop_front();}
}

3.6.全局level操作

个人认为spdlog关于日志等级的操作,并不是对称的,不知出于何种原因,get_level()取的是default logger的level,而set_level()却设置的是global_level,并且影响所有已注册的logger。

// get_level返回的是default logger的log level
// Get global logging level
SPDLOG_INLINE level::level_enum get_level()
{return default_logger_raw()->level();
}SPDLOG_INLINE spdlog::logger *default_logger_raw()
{return details::registry::instance().get_default_raw();
}// get_default_raw 返回default_logger_的raw pointer
SPDLOG_INLINE logger *registry::get_default_raw()
{return default_logger_.get();
}

set_level()实现:

SPDLOG_INLINE void set_level(level::level_enum log_level)
{details::registry::instance().set_level(log_level);
}// registry::set_level 设置全局log level, 影响所有已注册logger
SPDLOG_INLINE void registry::set_level(level::level_enum log_level)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);for (auto &l : loggers_){l.second->set_level(log_level);}global_log_level_ = log_level;
}

flush_on()类似于set_level,设置的是flush_level_,并且影响所有已注册logger的flush level。

SPDLOG_INLINE void flush_on(level::level_enum log_level)
{details::registry::instance().flush_on(log_level);
}// registry::flush_on 设置全局flush level, 影响所有已注册logger
SPDLOG_INLINE void registry::flush_on(level::level_enum log_level)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);for (auto &l : loggers_){l.second->flush_on(log_level);}flush_level_ = log_level;
}// Get global logging level
SPDLOG_API level::level_enum get_level();
// Set global logging level
SPDLOG_API void set_level(level::level_enum log_level);
// Determine whether the default logger should log messages with a certain level
SPDLOG_API bool should_log(level::level_enum lvl);
// Set global flush level
SPDLOG_API void flush_on(level::level_enum log_level);

3.7.flush_every定时冲刷

利用registry::flush_every 启动一个全局定时器,定时冲刷log消息到目标。

// 全局接口, 定时flush所有已注册logger, 时间间隔interval由调用者决定
template<typename Rep, typename Period>
inline void flush_every(std::chrono::duration<Rep, Period> interval)
{details::registry::instance().flush_every(interval);
}template<typename Rep, typename Period>
void registry::flush_every(std::chrono::duration<Rep, Period> interval)
{std::lock_guard<std::mutex> lock(flusher_mutex_);auto clbk = [this]() { this->flush_all(); }; // 子线程循环定时回调的匿名函数periodic_flusher_ = details::make_unique<periodic_worker>(clbk, interval);
}

3.8.drop清除logger

  • 清除指定logger name的logger对象

要清除的logger对象,分两种情况:1)属于已注册的logger对象;2)默认的logger对象(default logger)。
如果是第1)种情况,就从map中删除;如果是第2)种情况,就释放所指对象。

// 根据logger name 清除logger对象
SPDLOG_INLINE void registry::drop(const std::string &logger_name)
{std::lock_guard<std::mutex> lock(logger_map_mutex_);loggers_.erase(logger_name); // 从map中擦除loggerif (default_logger_ && default_logger_->name() == logger_name){default_logger_.reset();}
}
  • 清除所有logger对象

清除所有logger对象:已经注册的logger对象 + 默认的logger对象。

// 清除所有logger对象
SPDLOG_INLINE void registry::drop_all()
{std::lock_guard<std::mutex> lock(logger_map_mutex_);loggers_.clear();default_logger_.reset();
}

3.9.关闭log功能

如果用户想手动关闭spdlog的日志记录功能(log功能),释放相关资源,该怎么办?
可以调用shutdown()。registry关闭log功能,有三类资源:
1)periodic_flusher_开启了一个子线程,因此需要手动释放;
2)已注册logger对象+默认的logger对象,需要调用drop_all()释放;
3)用于异步写log的线程池资源,需要手动释放;

// 清除所有资源以及关闭registry启动的线程
SPDLOG_INLINE void shutdown()
{details::registry::instance().shutdown();
}// clean all resources and threads started by the registry
SPDLOG_INLINE void registry::shutdown()
{{std::lock_guard<std::mutex> lock(flusher_mutex_);periodic_flusher_.reset();}drop_all();{std::lock_guard<std::recursive_mutex> lock(tp_mutex_);tp_.reset();}
}

3.10.设置自动注册

如果用户想控制新建的logger对象,调用initialize_logger()后,是否自动添加进全局注册表,可以通过set_automatic_registration()来设置。

// Automatic registration of loggers when using spdlog::create() or  spdlog::create_async
SPDLOG_INLINE void set_automatic_registration(bool automatic_registration)
{details::registry::instance().set_automatic_registration(automatic_registration);
}

3.11.默认logger

  • 获取共享指针管理的logger

用户可以直接通过全局default_logger()接口,获取单例registry的default_logger对象。其实现也是转发给registry::default_logger()

// get shared_ptr of default logger
SPDLOG_INLINE std::shared_ptr<spdlog::logger> default_logger()
{return details::registry::instance().default_logger();
}
  • 获取logger对象原生指针

转发给registry::get_default_raw()。

// get raw pointer of default logger
SPDLOG_INLINE spdlog::logger *default_logger_raw()
{return details::registry::instance().get_default_raw();
}
  • 设置默认logger

转发给registry::set_default_logger()。

SPDLOG_INLINE void set_default_logger(std::shared_ptr<spdlog::logger>  default_logger)
{details::registry::instance().set_default_logger(std::move(default_logger));
}

3.12.写log

既然能通过default_logger(), default_logger_raw()获取default logger,那么用户如何方便地写日志内容呢?
可以通过spdlog.h中定义的一组便捷接口,使用默认的logger写log。

  • log()写日志版本

spdlog实现了2个重载函数模板log:第一个支持调用者指定源文件;第二个源文件为空。

// 写log便捷接口, 重载函数模板// 支持源文件的log版本
// 要写的正文内容是格式串形式
template<typename... Args>
inline void log(source_loc source, level::level_enum lvl, format_string_t<Args...>  fmt, Args &&... args)
{default_logger_raw()->log(source, lvl, fmt, std::forward<Args>(args)...);
}// 源文件为空的log版本
// 要写的正文内容是格式串形式
template<typename... Args>
inline void log(level::level_enum lvl, format_string_t<Args...> fmt, Args &&...  args)
{default_logger_raw()->log(source_loc{}, lvl, fmt,  std::forward<Args>(args)...);
}// 源文件由调用者指定
// 要写正文msg的类型T是模板参数, 如果T不能转换为format string(包括string_view/wstring_view), 就直接将msg当做string
template<typename T>
inline void log(source_loc source, level::level_enum lvl, const T &msg)
{default_logger_raw()->log(source, lvl, msg);
}// 源文件为空
// 要写正文msg的类型T是模板参数, 通常要求T可转换为format string(string_view/wstring_view)
template<typename T>
inline void log(level::level_enum lvl, const T &msg)
{default_logger_raw()->log(lvl, msg);
}
  • 使用固定log level写日志

如果用户想要写log的level固定,还有使用更简便的一组接口,实现也是转发给logger类对应的接口。

// 一组全局的固定log level的写log接口template<typename... Args>
inline void trace(format_string_t<Args...> fmt, Args &&... args)
{default_logger_raw()->trace(fmt, std::forward<Args>(args)...);
}template<typename... Args>
inline void debug(format_string_t<Args...> fmt, Args &&... args)
{default_logger_raw()->debug(fmt, std::forward<Args>(args)...);
}template<typename... Args>
inline void info(format_string_t<Args...> fmt, Args &&... args)
{default_logger_raw()->info(fmt, std::forward<Args>(args)...);
}template<typename... Args>
inline void warn(format_string_t<Args...> fmt, Args &&... args)
{default_logger_raw()->warn(fmt, std::forward<Args>(args)...);
}template<typename... Args>
inline void error(format_string_t<Args...> fmt, Args &&... args)
{default_logger_raw()->error(fmt, std::forward<Args>(args)...);
}template<typename... Args>
inline void critical(format_string_t<Args...> fmt, Args &&... args)
{default_logger_raw()->critical(fmt, std::forward<Args>(args)...);
}// 要写的正文内容msg类型T, 通常要求T可转换为format string(string_view/wstring_view)template<typename T>
inline void trace(const T &msg)
{default_logger_raw()->trace(msg);
}template<typename T>
inline void debug(const T &msg)
{default_logger_raw()->debug(msg);
}template<typename T>
inline void info(const T &msg)
{default_logger_raw()->info(msg);
}template<typename T>
inline void warn(const T &msg)
{default_logger_raw()->warn(msg);
}template<typename T>
inline void error(const T &msg)
{default_logger_raw()->error(msg);
}template<typename T>
inline void critical(const T &msg)
{default_logger_raw()->critical(msg);
}

4.知识点

4.1.std::enable_if, is_convertible_to_any_format_string

C++之std::enable_if_std enable if-CSDN博客

两个重要元函数std::enable_if, is_convertible_to_any_format_string

logger有一个非常重要的实现logger::log(),用到了2个元函数:std::enable_if和自定义is_convertible_to_any_format_string。

// T cannot be statically converted to format string (including  string_view/wstring_view)
template<class T, typename  std::enable_if<!is_convertible_to_any_format_string<const T &>::value, int>::type  = 0>
void logger::log(source_loc loc, level::level_enum lvl, const T &msg)
{log(loc, lvl, "{}", msg);
}

std::enable_if作用是满足条件(第一个模板参数)时类型(第二个参数)有效。可能实现如下,当模板参数B为true时,可以通过::type得到T的类型;否则,就没有定义::type,无法通过::type得到T的类型。如果B为false,程序通过::type获取T的类型时,就会编译报错。

// 通用版本
template<bool B, class T = void>
struct enable_if {};// 偏特化版本
template<class T>
struct enable_if<true, T> { using type = T; };// enable_if_t定义
template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;

is_convertible_to_any_format_string 是通过std::is_convertible判断T是否能转化成string_view_t或者wstring_view_t,::value取回结果(true/false),实现如下:

// 如果T类型能转化成format string, 那么::value/::value_type()/::operator()就会得到true; 
// 否则, 得到false
// format string是指: 
// 1) fmt::basic_string_view<char>, 即std::string_view_t的实现
// 2) fmt::basic_string_view<wchar_t>, 即std::wstring_view_t的实现
template<class T>
struct is_convertible_to_any_format_string : std::integral_constant<bool,  is_convertible_to_basic_format_string<T, char>::value ||is_convertible_to_basic_format_string<T, wchar_t>::value>
{};// std::integral_constant
// ::value_type 返回_Ty类型, ::type 返回
template <class _Ty, _Ty _Val>
struct integral_constant {// 值元函数拥有内嵌变量::valuestatic constexpr _Ty value = _Val;// 内嵌类型using value_type = _Ty;using type       = integral_constant;// 内嵌函数constexpr operator value_type() const noexcept {return value;}// _NODISCARD是 [[nodiscard]] 别名, 调用函数处没有获取返回值, 编译器会警告_NODISCARD constexpr value_type operator()() const noexcept {return value;}
};template <bool _Val>
using bool_constant = integral_constant<bool, _Val>;using true_type  = bool_constant<true>;
using false_type = bool_constant<false>;// is_convertible_to_basic_format_string 判断类型T是否能转化成类型Char
// 当T可以转换成fmt::basic_string_view<Char>(即std::string_view_t) 或者 将T去掉cv属性(const/volatile)后与fmt::basic_runtime<Char>同类型时, 则能通过::value得到true; 否则, 得到false
// fmt::basic_runtime<Char>的本质是fmt::basic_string_view<Char>
template<class T, class Char = char>
struct is_convertible_to_basic_format_string: std::integral_constant<bool,std::is_convertible<T, fmt::basic_string_view<Char>>::value ||  std::is_same<remove_cvref_t<T>, fmt::basic_runtime<Char>>::value>
{};// std::is_convertible 判断_From类型是否能转化成_To类型
// 如果能, 那么调用者通过::value返回true; 否则, 返回false
template <class _From, class _To>
struct is_convertible : bool_constant<__is_convertible_to(_From, _To)> {// determine whether _From is convertible to _To
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/370222.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

E4.【C语言】练习:while和getchar的理解

#include <stdio.h> int main() {int ch 0;while ((ch getchar()) ! EOF){if (ch < 0 || ch>9)continue;putchar(ch);}return 0; } 理解上述代码 0-->48 9-->57 if行判断是否为数字&#xff0c;打印数字&#xff0c;不打印非数字

Selenium 切换 frame/iframe

环境&#xff1a; Python 3.8 selenium3.141.0 urllib31.26.19说明&#xff1a; driver.switch_to.frame() # 将当前定位的主体切换为frame/iframe表单的内嵌页面中 driver.switch_to.default_content() # 跳回最外层的页面# 判断元素是否在 frame/ifame 中 # 126 邮箱为例 # …

k8s公网集群安装(1.23.0)

网上搜到的公网搭建k8s都不太一致, 要么说的太复杂, 要么镜像无法下载, 所以写了一个简洁版,小白也能一次搭建成功 使用的都是centos7,k8s版本为1.23.0 使用二台机器搭建的, 三台也是一样的思路1.所有节点分别设置对应主机名 hostnamectl set-hostname master hostnamectl set…

javaSwing图书管理系统

一、 引言 图书管理系统是一个用于图书馆或书店管理图书信息、借阅记录和读者信息的应用程序。本系统使用Java Swing框架进行开发&#xff0c;提供直观的用户界面&#xff0c;方便图书馆管理员或书店工作人员对图书信息进行管理。以下是系统的设计、功能和实现的详细报告。 二…

Matplotlib Artist 1 概览

Matplotlib API中有三层 matplotlib.backend_bases.FigureCanvas&#xff1a;绘制区域matplotlib.backend_bases.Renderer&#xff1a;控制如何在FigureCanvas上绘制matplotlib.artist.Artist&#xff1a;控制render如何进行绘制 开发者95%的时间都是在使用Artist。Artist有两…

【C语言】自定义类型:联合和枚举

前言 前面我们学习了一种自定义类型&#xff0c;结构体&#xff0c;现在我们学习另外两种自定义类型&#xff0c;联合 和 枚举。 目录 一、联合体 1. 联合体类型的声明 2. 联合体的特点 3. 相同成员联合体和结构体对比 4. 联合体大小的计算 5. 用联合体判断当前机…

常见算法和Lambda

常见算法和Lambda 文章目录 常见算法和Lambda常见算法查找算法基本查找&#xff08;顺序查找&#xff09;二分查找/折半查找插值查找斐波那契查找分块查找扩展的分块查找&#xff08;无规律的数据&#xff09; 常见排序算法冒泡排序选择排序插入排序快速排序递归快速排序 Array…

hdu物联网硬件实验2 GPIO亮灯

学院 班级 学号 姓名 日期 成绩 实验题目 GPIO亮灯 实验目的 点亮三个灯闪烁频率为一秒 硬件原理 无 关键代码及注释 const int ledPin1 GREEN_LED; // the number of the LED pin const int ledPin2 YELLOW_LED; const int ledPin3 RED…

githup开了代理push不上去

你们好&#xff0c;我是金金金。 场景 git push出错 解决 cmd查看 git config --global http.proxy git config --global https.proxy 如果什么都没有&#xff0c;代表没设置全局代理&#xff0c;此时如果你开了代理&#xff0c;则执行如下&#xff0c;设置代理 git con…

【深度学习】图形模型基础(5):线性回归模型第四部分:预测与贝叶斯推断

1.引言 贝叶斯推断超越了传统估计方法&#xff0c;它包含三个关键步骤&#xff1a;结合数据和模型形成后验分布&#xff0c;通过模拟传播不确定性&#xff0c;以及利用先验分布整合额外信息。本文将通过实际案例阐释这些步骤&#xff0c;展示它们在预测和推断中的挑战和应用。…

CSS中 实现四角边框效果

效果图 关键代码 border-radius:10rpx ;background: linear-gradient(#fff, #fff) left top,linear-gradient(#fff, #fff) left top,linear-gradient(#fff, #fff) right top,linear-gradient(#fff, #fff) right top,linear-gradient(#fff, #fff) left bottom,linear-gradient(…

12 Dockerfile详解

目录 1. Dockerfile 2. Dockerfile构建过程 2.1. Dockerfile编写规则&#xff1a; 2.2. Docker执行Dockerfile的大致流程 2.3. 总结 3. Dockerfile指令 3.1. FROM 3.2. MAINTAINER 3.3. RUN 3.4. EXPOSE 3.5. WORKDIR 3.6. USER 3.7. ENV 3.8. VOLUME 3.9. ADD …

电商利器——淘宝商品月销量API接口解析

在电商时代&#xff0c;数据就是金钱。对于淘宝商家而言&#xff0c;掌握商品的销量数据无异于掌握了市场的脉搏。如今&#xff0c;淘宝商品月销量API接口的出现&#xff0c;联讯数据让商家如虎添翼&#xff0c;能够更加精准地把握市场动态&#xff0c;优化商品策略。 淘宝商…

利用redis数据库管理代理库爬取cosplay网站-cnblog

爬取cos猎人 数据库管理主要分为4个模块&#xff0c;代理获取模块&#xff0c;代理储存模块&#xff0c;代理测试模块&#xff0c;爬取模块 cos猎人已经倒闭&#xff0c;所以放出爬虫源码 api.py 为爬虫评分提供接口支持 import requests import concurrent.futures import …

鸿蒙开发设备管理:【@ohos.vibrator (振动)】

振动 说明&#xff1a; 开发前请熟悉鸿蒙开发指导文档&#xff1a;gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 本模块首批接口从API version 8开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 imp…

查询某个县区数据,没有的数据用0补充。

加油&#xff0c;新时代打工人&#xff01; 思路&#xff1a; 先查出有数据的县区&#xff0c;用县区编码判断&#xff0c;不存在县区里的数据。然后&#xff0c;用union all进行两个SQL拼接起来。 SELECTt.regionCode,t.regionName,t.testNum,t.sampleNum,t.squareNum,t.crop…

linux主机(A)通过私钥登录linux主机(B)

1.登录B主机&#xff0c;先在B主机执行 ssh-keygen 2.设置id_rsa的权限 chmod 600 id_rsa 3.将生成的id_rsa.pub导入到authorized_keys ssh-copy-id -i ./id_rsa.pub root127.0.0.1 4.将id_rsa复制到A主机 scp id_rsa_123 root1.1.1.A:/home/ 5.登录到A主机使用私钥登录 因…

dotnet ef工具使用

设置工具安装目录 dotnet tool install dotnetsay --tool-path G:\dotnet-tools安装 dotnet tool install --global dotnet-ef更新 dotnet tool update --global dotnet-ef查看版本 dotnet ef --version创建迁移文件 # 只有一个dbcontext dotnet ef migrations add init #…

微机原理与单片机 知识体系梳理

单片机笔记分享 我个人感觉单片机要记的东西很多&#xff0c;也很琐碎&#xff0c;特别是一些位、寄存器以及相关作用等&#xff0c;非常难以记忆。因此复习时将知识点整理在了一起做成思维导图&#xff0c;希望对大家有所帮助。内容不是很多&#xff0c;可能有些没覆盖全&…

PySide6 实现资源的加载:深入解析与实战案例

目录 1. 引言 2. 加载内置资源 3. 使用自定义资源文件&#xff08;.qrc&#xff09; 创建.qrc文件 编译.qrc文件 加载资源 4. 动态加载UI文件 使用Qt Designer设计UI 加载UI文件 5. 注意事项与最佳实践 6. 结论 在开发基于PySide6的桌面应用程序时&…