spdlog库学习(四):logger

系列文章目录


文章目录

  • 系列文章目录
  • 创建系统控制台日志
    • logger获取
  • 创建基本文件日志
  • 循环日志文件
    • 每日日志
  • 异步日志
    • async_logger
  • 总结


上一篇学习了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);
}

创建过程总结以下几点:

  • 创建console时,可以指定工厂,默认是spdlog::synchronous_factory
  • 创建的logger类型由sink类型决定,sink类型由调用函数决定。创建时,通过模板传递个工厂的Create函数。
  • 调用的函数 spdlog::stdout_color_mt("console")是spdlog的作者把常用的logger类型的创建封装了一层,避免显示指明sink的类型的麻烦。
  • logger的创建都要向registry进行注册,用于后续通过名字查找并获取logger。registry回给logger设置formatter,设置错误处理函数,设置日志级别。如果设置了自动注册,则会将logger放入registry中的std::unordered_map> loggers_

logger获取

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图又可以简单扩展下。实现先不管,关系先捋顺
spdlog库学习(四):logger_第1张图片

循环日志文件

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类型而已,也暂时还没有补全内容。
spdlog库学习(四):logger_第2张图片

异步日志

我认为异步日志是比较有意思的地方,里面用到了线程池、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到线程池任务队列中排队,由线程池将日志内容输出。

async_logger

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 loggers_两个对象。
spdlog库学习(四):logger_第3张图片

总结

到现在为止,大致知道了整个日志库的运行框架。知道了控制台日志、文件日志、异步日志大致的实现方式。接下来会看几个额外的小功能,包括:

  1. 周期刷新
  2. trace功能
  3. stopwatch功能
  4. 系统日志是怎么回事

之后就会深入细节,学习下c++的编码和实现、项目组织上的技巧

你可能感兴趣的:(开源库学习,学习,java,c++)