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.mzph.cn/bicheng/41790.shtml

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

相关文章

合并区间(python3)

合并区间 题目描述解题思路代码实现复杂度 题目描述 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中的所有区间 。 示…

认识流式处理框架Apache Flink

目录 一、Apache Flink 的基础概念 1.1 Apache Flink是什么&#xff1f; 1.2 Flink的定义 二、Apache Flink 的发展史 2.1 Flink前身Stratosphere 2.2 Flink发展时间线及重大变更 三、Flink核心特性 3.1 批流一体化 3.2 同时支持高吞吐、低延迟、高性能 3.3 支持事件时…

Git 运用小知识

1.Git添加未完善代码的解决方法 1.1 Git只是提交未推送 把未完善的代码提交到本地仓库 只需点击撤销提交&#xff0c;提交的未完善代码会被撤回 代码显示未提交状态 1.2 Git提交并推送 把未完善的代码提交并推送到远程仓库 点击【未完善提交并推送】的结点选择还原提交&#x…

指定版本ceph-common安装

如&#xff0c;安装15.2.13的ceph-common PACKAGE_NAMEceph-common CEPH_VERSION15.2.13 wget -q -O- https://download.ceph.com/keys/release.asc | sudo apt-key add - echo deb http://download.ceph.com/debian-${CEPH_VERSION}/ $(lsb_release -sc) main | sudo tee …

关于学习方法的优化

这是一种新的学习方法&#xff0c;一种新的学习形式&#xff0c;可以通过歌唱的方式&#xff0c;运用&#xff0c;把自己每天要进行的内容进行一个复习&#xff0c;进行一个重复&#xff0c;这样可以实现随时随地进行一个学习&#xff0c;这样可以帮助快速走出来&#xff01; 您…

面试记录总结

es6新特性 --------------------------------------------------------------------------------------- let 1.变量不能重复声明 2.块儿级作用域 3.不存在变量提升 4.不影响作用域链 const 1.一定要赋初始值 2.一般常量使用大写(潜规则) 3.常量的值不能修改 4.块儿级作用域 5…

MinIO - 从 环境搭建 -> SpringBoot实战 -> 演示,掌握 Bucket 和 Object 操作

目录 开始 Docker 部署 MinIO 中的基本概念 SpringBoot 集成 MinIO 依赖 配置 MinIO 时间差问题报错 The difference between the request time and the servers time is too large MinIO 中对 Bucket&#xff08;文件夹&#xff09; 的操作 是否存在 / 创建 查询所有…

如何产生一个有价值的观点

今天看了黄执中的一个课叫如何产生有价值的观点收获挺大的。 有价值指的是能让人听完有“哦”的提壶灌顶的感觉。而观点是指具有浅负担&#xff0c;深触动&#xff0c;可迁移属性的一个想法。 黄执中说这样的观点不是灵机一现&#xff0c;而是可以量产的。他给我举了三个计算…

Apache Seata 源码分析Seata-XID传递 Dubbo篇

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 源码分析 Seata-XID 传递 Dubbo 篇 本文作者&#xff1a;FUNKYE(陈健斌),杭州某互联网公司主…

TQ15EG开发板教程:MPSOC创建fmcomms8工程

链接&#xff1a;https://pan.baidu.com/s/1jbuYs9alP2SaqnV5fpNgyg 提取码&#xff1a;r00c 本例程需要实现在hdl加no-OS系统中&#xff0c;通过修改fmcomms8/zcu102项目&#xff0c;实现在MPSOC两个fmc口上运行fmcomms8项目。 目录 1 下载文件与切换版本 2 编译fmcomms8项…

超越YOLO! RT-DETR 实时目标检测技术介绍

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

基于STM32F103C8T6的同步电机驱动-电流环PI与力矩模式

基于STM32F103C8T6的同步电机驱动-电流环PI与力矩模式 本系列文章: 基于STM32F103C8T6的同步电机驱动-CubeMX配置与IQmath调用基于STM32F103C8T6的同步电机驱动-PWM驱动代码以及SVPWM的实现基于STM32F103C8T6的同步电机驱动-ADC采样与基于MT6701的角度获取基于STM32F103C8T6的…

JavaScript中的解构赋值:用途与优势

文章目录 JavaScript中的解构赋值&#xff1a;用途与优势一、简化数组和对象数据的访问数组解构对象解构 二、函数返回多个值三、与扩展运算符结合使用数组扩展对象扩展 四、默认值五、嵌套解构总结 JavaScript中的解构赋值&#xff1a;用途与优势 在JavaScript中&#xff0c;…

linux cpuspeed工具

cpuspeed 是一个用于管理和监控 CPU 频率的工具&#xff0c;主要用于基于 Linux 的系统。它可以帮助用户根据系统的负载自动调整 CPU 频率&#xff0c;以节省电能或提高性能。 以下是一些常见的 cpuspeed 用法及其配置示例&#xff1a; 安装 cpuspeed 在某些 Linux 发行版上…

免杀笔记 ----> DLL注入

这段时间我们暂时没什么事情干的话我们就继续更新我们的免杀笔记力&#xff01;&#xff01;&#xff01; &#xff1a;今天我们讲DLL注入 目录 1.DLL注入 2.直接加载DLL&#xff1f; 3.远程线程注入 获取Handle 远程申请内存空间 将我们的CS的DLL加载入内存 创建远程线…

02:C语言数据类型

C语言数据类型 1、整型变量2、浮点型变量3、字符型变量4、有符号数和无符号数 1、整型变量 #include <stdio.h>int main(void) { /* int a; //定义一个整型变量aint b; //定义一个整型变量bint c; //定义一个整型变量c */ /* int a,b,c; //也可以这样定义a 25;b 6; *…

Linux 服务器环境搭建

一、安装 JDK 官网下载地址&#xff1a;https://www.oracle.com/java/technologies/downloads # 创建目录 mkdir /usr/local/java/# 解压 tar -zxvf jdk-8u333-linux-x64.tar.gz -C /usr/local/java/# 配置环境变量 vim /etc/profileexport export JAVA_HOME/usr/local/java/…

【Linux】进程信号_4

文章目录 八、进程信号3. 信号的处理4. 可重入函数5. volatile 未完待续 八、进程信号 3. 信号的处理 当某个信号的处理函数被调用时&#xff0c;操作系统会自动将当前信号假如进程的信号屏蔽字当中。如果处理完该信号&#xff0c;该信号同样也会自动从信号屏蔽字中 移除。该…

马尔科夫假设

马尔科夫假设 马尔科夫假设&#xff08;Markov Assumption&#xff09;&#xff0c;也称为无记忆假设&#xff0c;它假设在给定当前状态的情况下&#xff0c;未来的状态只依赖于当前状态&#xff0c;而与过去的状态序列无关。在强化学习中&#xff0c;如果这个假设不成立&…

Apache Seata core 模块源码分析

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 一 . 导读 core 模块定义了事务的类型、状态&#xff0c;通用的行为&#xff0c;client 和 s…