上一篇学习了spdlog库的默认日志输出流程。默认的日志输出使用的logger并无特殊,只是其sink使用了ansicolor_stdout_sink_mt
。给出的示例中还给出了另外的用法。包括:手动创建控制台日志、文件日志包括很多种:基本文件日志、循环文件日志、每日日志文件。接下来主要学习这几种日志。示例中其他列出的功能会在后续的文章中学习。
手动创建控制台日志的方法如下:
auto console = spdlog::stdout_color_mt("console");//标准输出
auto err_logger = spdlog::stderr_color_mt("stderr"); // 标准错误输出
该方法一个模板,定义在stdout_color_sinks.h
,接口定义如下:
以标准输出stdout_color_mt为例,标准错误输出同理
template
std::shared_ptr stdout_color_mt(const std::string &logger_name, color_mode mode = color_mode::automatic);
实现在stdout_color_sinks-inl.h
template
SPDLOG_INLINE std::shared_ptr stdout_color_mt(const std::string &logger_name, color_mode mode)
{
return Factory::template create(logger_name, mode);
}
这里这个Factory::template
是什么用法暂时不知道。但我们知道Factory默认类型是spdlog::synchronous_factory
,从名字知道是同步工厂。通过该工厂创建的logger都是即时输出的。其实现在synchronous_factory.h
template
static std::shared_ptr create(std::string logger_name, SinkArgs &&... args)
{
auto sink = std::make_shared(std::forward(args)...);
auto new_logger = std::make_shared(std::move(logger_name), std::move(sink));
**details::registry::instance().initialize_logger(new_logger);**
return new_logger;
}
if (automatic_registration_)
{
register_logger_(std::move(new_logger));
}
SPDLOG_INLINE void registry::register_logger_(std::shared_ptr new_logger)
{
auto logger_name = new_logger->name();
throw_if_exists_(logger_name);
loggers_[logger_name] = std::move(new_logger);
}
创建过程总结以下几点:
spdlog::synchronous_factory
spdlog::stdout_color_mt("console")
是spdlog的作者把常用的logger类型的创建封装了一层,避免显示指明sink的类型的麻烦。std::unordered_map> loggers_
logger的获取就比较简单,只是通过registry获取而已。
spdlog::get("console")
SPDLOG_INLINE std::shared_ptr get(const std::string &name)
{
return details::registry::instance().get(name);
}
SPDLOG_INLINE std::shared_ptr registry::get(const std::string &logger_name)
{
std::lock_guard lock(logger_map_mutex_);
auto found = loggers_.find(logger_name);
return found == loggers_.end() ? nullptr : found->second;
}
logger是从registry的loggers_中获取的。如果创建logger的时候,没有开启自动注册,则会拿不到
创建基本文件日志的示例如下
try
{
auto logger = spdlog::basic_logger_mt("basic_logger", "logs/basic-log.txt");
}
catch (const spdlog::spdlog_ex &ex)
{
std::cout << "Log init failed: " << ex.what() << std::endl;
}
由于打开文件可能会出现打开失败等异常,因此放在try catch块中
与控制台日志类似,spdlog::basic_logger_mt("basic_logger", "logs/basic-log.txt");
也是一个模板,实现在basic_file.sink.h
template
inline std::shared_ptr basic_logger_mt(
const std::string &logger_name, const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {})
{
return Factory::template create(logger_name, filename, truncate, event_handlers);
}
看到这里,我想就清楚多了,这里Factory和上面的console一样都是同步工厂,只是sink不一样。我们简单追踪下basic_file_sink_mt
的实现位置。
多说一句,这里的mt代表多线程,还有一种st代表单线程。其区别在于加不加锁。他们都是用的同步工厂。同步与异步在于日志输出时机的问题,而多线程与单线程是线程安全的问题,不要搞混。异步输出后面会看
template< typename Factory = spdlog::synchronous_factory >
inline std::shared_ptr basic_logger_st(
const std::string &logger_name, const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {})
basic_file_sink_mt
也是实现在basic_file.sink.h
using basic_file_sink_mt = basic_file_sink;
而basic_file_sink是继承于 base_sink_< Mutex>的。而base_sink_< Mutex>又继承于sink。我们的uml图又可以简单扩展下。实现先不管,关系先捋顺
auto max_size = 1048576 * 5;
auto max_files = 3;
//最多三个文件循环,每个文件最多5M,
auto logger = spdlog::rotating_logger_mt("some_logger_name", "logs/rotating.txt", max_size, max_files);
看到这个函数,也就不陌生了,这里再贴一次,实现在rotating_fils_sink.h
template
inline std::shared_ptr rotating_logger_mt(const std::string &logger_name, const filename_t &filename, size_t max_file_size,
size_t max_files, bool rotate_on_open = false, const file_event_handlers &event_handlers = {})
{
return Factory::template create(
logger_name, filename, max_file_size, max_files, rotate_on_open, event_handlers);
}
还是看下rotating_file_sink_mt的实现
using rotating_file_sink_mt = rotating_file_sink;
class rotating_file_sink final : public base_sink {
.....
};
rotating_file_sink final
也是继承于base_sink< Mutex>。所以base_sink只是把与文件相关的内容封装起来,具体对文件的各种功能型封装再继承一层来实现。
每日日志跟前面都差不多
// Create a daily logger - a new file is created every day on 2:30am
// 每天2.30创建一个日志文件
auto logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", 2, 30);
daily_file_sink.h
template
inline std::shared_ptr daily_logger_mt(const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0,
bool truncate = false, uint16_t max_files = 0, const file_event_handlers &event_handlers = {})
{
return Factory::template create(logger_name, filename, hour, minute, truncate, max_files, event_handlers);
}
using daily_file_sink_mt = daily_file_sink;
class daily_file_sink final : public base_sink {
.....
}
此外,还包括一种系统日志,其实现结构也差不多一样,实现在syslog_sink.h,就不列出了。之后有时间会再看看。
至此,文件部分的uml图如下。因为这里只有sink不同,所以只是再原UML图上加了几个sink类型而已,也暂时还没有补全内容。
我认为异步日志是比较有意思的地方,里面用到了线程池、blocking queue等线程异步操作的同步工具。
//基本异步文件日志
auto async_file = spdlog::basic_logger_mt("async_file_logger", "logs/async_log.txt")
注意,这里函数还是那个函数,只不过,工厂变成了异步工厂async_factory
,实现在async.h。
using async_factory = async_factory_impl;
async_factory_impl只是一个简单的结构体,没有继承任何东西。其create函数定义如下
注意,这时返回的logger已经变成了async_logger。到现在为止终于看到了另一种logger。因为异步日志不再是输出目标的不同了,而是输出策略的不同。所以不能再用sink做区分,而是要用logger做区分
static std::shared_ptr create(std::string logger_name, SinkArgs &&... args)
{
auto ®istry_inst = details::registry::instance();
// create global thread pool if not already exists..
auto &mutex = registry_inst.tp_mutex();
std::lock_guard tp_lock(mutex);
// 获取线程池
auto tp = registry_inst.get_tp();
if (tp == nullptr)
{
// 初始化线程池
tp = std::make_shared(details::default_async_q_size, 1U);
registry_inst.set_tp(tp);
}
auto sink = std::make_shared(std::forward(args)...);
auto new_logger = std::make_shared(std::move(logger_name), std::move(sink), **std::move(tp)**, OverflowPolicy);
registry_inst.initialize_logger(new_logger);
return new_logger;
}
异步logger在初始化时,会先拿到registry的线程池 tp对象。输入日志时,不是立即输出到文件,而是先将输出的任务post到线程池任务队列中排队,由线程池将日志内容输出。
class SPDLOG_API async_logger final : public std::enable_shared_from_this, public logger{
...
protected:
//不直接输出到sink,而是post到线程池
void sink_it_(const details::log_msg &msg) override;
void flush_() override;
// 输出到sink目标,由线程池调用
void backend_sink_it_(const details::log_msg &incoming_log_msg);
void backend_flush_();
private:
std::weak_ptr thread_pool_;
async_overflow_policy overflow_policy_;
...
};
async_logger除了继承于logger外,还继承自enable_shared_from_this。这是一个c++标准库的模板工具,用于将this指针变成一个智能指针。
这是多线程下,对象生命周期管理办法。
SPDLOG_INLINE void spdlog::async_logger::sink_it_(const details::log_msg &msg)
{
if (auto pool_ptr = thread_pool_.lock())
{
**pool_ptr->post_log(shared_from_this(), msg, overflow_policy_);**
}
else
{
throw_spdlog_ex("async log: thread pool doesn't exist anymore");
}
}
async_logger::sink_it_将自己(this)指针post到任务队列中执行。该任务是延后执行的。如果该任务执行时,logger已经被释放了,那么直接使用this指针就会core_dump。使用指针就不会有这种麻烦。看到代码中使用了shared_from_this()
,用于确保,logger对象永远不会析构。因为此时shared_ptr引用计数至少为1。
还有一种用法是使用weak_ptr。在调用时先lock一下,确保对象没有被释放,再进行指针调用。async_logger对thread_pool的使用就是用了这个技巧。在这里的逻辑是:线程池对象只能由registry持有和管理,所有的异步logger只有使用权,而不能决定thread_pool的声明周期。所以用weak_ptr来保存。
到这里,registry对象又添加了thread_pool、std::unordered_map
到现在为止,大致知道了整个日志库的运行框架。知道了控制台日志、文件日志、异步日志大致的实现方式。接下来会看几个额外的小功能,包括:
之后就会深入细节,学习下c++的编码和实现、项目组织上的技巧