spdlog源码学习

spdlog是一个用c++11实现的高性能日志库。 接入方便,功能丰富,代码可读性较高。


Features

  • Very fast - performance is the primary goal
  • Headers only, just copy and use.
  • Feature rich using the excellent fmt library.
  • Extremely fast asynchronous mode (optional) - using lockfree queues and other tricks to reach millions of calls/sec.
  • Custom formatting.
  • Multi/Single threaded loggers.
  • Various log targets:
    • Rotating log files.
    • Daily log files.
    • Console logging (colors supported).
    • syslog.
    • Windows debugger (OutputDebugString(..))
    • Easily extendable with custom log targets (just implement a single function in the sink interface).
  • Severity based filtering - threshold levels can be modified in runtime as well as in compile time.

核心设计

spdlog中定义了一系列Sink类,作为实际上把log输出到指定目标的对象,每一个Sink唯一对应一个log的输出目标(如console、文件、db). 每一个logger包含一个sink列表,每当上层调用logger的log方法写日志时,logger就会调用sink列表中的每一个sink的 sink(log_msg) 函数,真正往目标中写日志。

实现Rotating file写日志

当前正在写日志的文件名一直都是log.txt。 当log.txt写不下时,把它重命名为log1.txt,再重新打开一个log.txt文件。 多个文件循环也依次类推。

// Rotate files:
// log.txt -> log.1.txt
// log.1.txt -> log2.txt
// log.2.txt -> log3.txt
// log.3.txt -> delete
/*
 * Rotating file sink based on size
 */
template<class Mutex>
class rotating_file_sink : public base_sink < Mutex >
{
public:
    rotating_file_sink(const filename_t &base_filename, const filename_t &extension,
                       std::size_t max_size, std::size_t max_files                       ) :
    {
        _file_helper.open(calc_filename(_base_filename, 0, _extension));
        _current_size = _file_helper.size(); //expensive. called only once
    }

protected:
    void _sink_it(const details::log_msg& msg) override
    {
        _current_size += msg.formatted.size();
        if (_current_size > _max_size)
        {
            _rotate();
            _current_size = msg.formatted.size();
        }
        _file_helper.write(msg);
    }

private:
    static filename_t calc_filename(const filename_t& filename, std::size_t index, const filename_t& extension)
    {
    //...
        return w.str();
    }

    void _rotate()
    {
        using details::os::filename_to_str;
        _file_helper.close();
        for (auto i = _max_files; i > 0; --i)
        {
            filename_t src = calc_filename(_base_filename, i - 1, _extension);
            filename_t target = calc_filename(_base_filename, i, _extension);

            if (details::file_helper::file_exists(target))
            {
                details::os::remove(target);
            }
            if (details::file_helper::file_exists(src) && details::os::rename(src, target))
            {
                //...
            }
        }
        _file_helper.reopen(true);
    }
    filename_t _base_filename;
    filename_t _extension;
    std::size_t _max_size;
    std::size_t _max_files;
    std::size_t _current_size;
    details::file_helper _file_helper;
};

实现异步写日志

所有异步写日志的请求都会被push到一个固定大小的队列中。 有一个工作线程会不停的从队列中pop一条日志消息,并最终写到日志中。

调用方:

void async_example()
{
    size_t q_size = 4096; //queue size must be power of 2
    spdlog::set_async_mode(q_size);
    auto async_file = spd::daily_logger_st("async_file_logger", "logs/async_log.txt");

    for (int i = 0; i < 100; ++i)
        async_file->info("Async message #{}", i);
}

具体实现: 开启一个工作线程,在async_log初始时就开始工作了。并且在析构是被关闭

// Async Logger implementation
// Use an async_sink (queue per logger) to perform the logging in a worker thread

//一个工作线程,在async_log初始时就开始工作了。并且在析构是被关闭
// worker thread
std::thread _worker_thread;
_worker_thread(&async_log_helper::worker_loop, this)
inline void spdlog::details::async_log_helper::worker_loop()
{
    try
    {
        if (_worker_warmup_cb) _worker_warmup_cb();
        auto last_pop = details::os::now();
        auto last_flush = last_pop;
        while(process_next_msg(last_pop, last_flush));
        if (_worker_teardown_cb) _worker_teardown_cb();
    }
    catch (const std::exception &ex)
    {
        _err_handler(ex.what());
    }
    catch (...)
    {
        _err_handler("Unknown exception");
    }
}
// Send to the worker thread termination message(level=off)
// and wait for it to finish gracefully
inline spdlog::details::async_log_helper::~async_log_helper()
{
    try
    {
        push_msg(async_msg(async_msg_type::terminate));
        _worker_thread.join();
    }
    catch (...) // don't crash in destructor
    {}
}

从队列中取消息,如果是写日志的消息,则调用sink写日志。

//从队列中取消息,如果是写日志的消息,则调用sink写日志。
// process next message in the queue
// return true if this thread should still be active (while no terminate msg was received)
inline bool spdlog::details::async_log_helper::process_next_msg(log_clock::time_point& last_pop, log_clock::time_point& last_flush)
{
    async_msg incoming_async_msg;

    if (_q.dequeue(incoming_async_msg))
    {
        last_pop = details::os::now();
        switch (incoming_async_msg.msg_type)
        {
        case async_msg_type::flush:
            _flush_requested = true;
            break;

        case async_msg_type::terminate:
            _flush_requested = true;
            _terminate_requested = true;
            break;

        default:
            log_msg incoming_log_msg;
            incoming_async_msg.fill_log_msg(incoming_log_msg);
            _formatter->format(incoming_log_msg);
            for (auto &s : _sinks)
            {
                if(s->should_log( incoming_log_msg.level))
                {
                    s->log(incoming_log_msg);
                }
            }
        }
        return true;
    }

    // Handle empty queue..
    // This is the only place where the queue can terminate or flush to avoid losing messages already in the queue
    else
    {
        auto now = details::os::now();
        handle_flush_interval(now, last_flush);
        sleep_or_yield(now, last_pop);
        return !_terminate_requested;
    }
}

调用写日志,往队列里塞一条写日志消息。

template<typename T>
inline void spdlog::logger::log(level::level_enum lvl, const T& msg)
{
    if (!should_log(lvl)) return;
    try
    {
        details::log_msg log_msg(&_name, lvl);
        log_msg.raw << msg;
        _sink_it(log_msg);
    }
    catch (const std::exception &ex)
    {
        _err_handler(ex.what());
    }
    catch (...)
    {
        _err_handler("Unknown exception");
    }
}

inline void spdlog::async_logger::_sink_it(details::log_msg& msg)
{
    try
    {
        _async_log_helper->log(msg);
        if (_should_flush_on(msg))
            _async_log_helper->flush(false); // do async flush
    }
    catch (const std::exception &ex)
    {
        _err_handler(ex.what());
    }
    catch (...)
    {
        _err_handler("Unknown exception");
    }
}

// 往队列里塞一条日志进去,准备被写
//Try to push and block until succeeded (if the policy is not to discard when the queue is full)
inline void spdlog::details::async_log_helper::log(const details::log_msg& msg)
{
    push_msg(async_msg(msg));
}

inline void spdlog::details::async_log_helper::push_msg(details::async_log_helper::async_msg&& new_msg)
{
    if (!_q.enqueue(std::move(new_msg)) && _overflow_policy != async_overflow_policy::discard_log_msg)
    {
        auto last_op_time = details::os::now();
        auto now = last_op_time;
        do
        {
            now = details::os::now();
            sleep_or_yield(now, last_op_time);
        }
        while (!_q.enqueue(std::move(new_msg)));
    }
}

根据时间间隔让线程等待

// spin, yield or sleep. use the time passed since last message as a hint
inline void spdlog::details::async_log_helper::sleep_or_yield(const spdlog::log_clock::time_point& now, const spdlog::log_clock::time_point& last_op_time)
{
    using namespace std::this_thread;
    using std::chrono::milliseconds;
    using std::chrono::microseconds;

    auto time_since_op = now - last_op_time;

    // spin upto 50 micros
    if (time_since_op <= microseconds(50))
        return;

    // yield upto 150 micros
    if (time_since_op <= microseconds(100))
        return std::this_thread::yield();

    // sleep for 20 ms upto 200 ms
    if (time_since_op <= milliseconds(200))
        return sleep_for(milliseconds(20));

    // sleep for 200 ms
    return sleep_for(milliseconds(200));
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏信安之路

【作者投稿】MITMF安装与使用

MITMF其实就是一个基于python编写的中间人攻击的框架,就好比metaspoit一样,无比强大且但十分易用。下面笔者就给大家介绍一下它有哪些用途,本文具有...

12600
来自专栏JetpropelledSnake

ELK学习笔记之Logstash和Filebeat解析对java异常堆栈下多行日志配置支持

logstash官方最新文档。 假设有几十台服务器,每台服务器要监控系统日志syslog、tomcat日志、nginx日志、mysql日志等等,监控OOM、内存...

56810
来自专栏小狼的世界

Analog使用中的一些技巧和总结

Analog是一款用来快速处理日志的开源工具,具有很高的效率,但是生成的结果并不美观,本文就analog使用过程中的一些问题进行总结,讨论如何对analog进行...

9410
来自专栏Gaussic

OpenBR安装与编译过程 原

首先要安装VS2013,官网上说装Express版本就行,我这边装了Professional中文版,除了编译的时候经常出现字符问题,其他没什么影响。

11210
来自专栏JetpropelledSnake

RESTful源码学习笔记之RPC和Restful深入理解

RPC 即远程过程调用(Remote Procedure Call Protocol,简称RPC),像调用本地服务(方法)一样调用服务器的服务(方法)。通常的实...

11230
来自专栏大大的微笑

ZOOKEEPER集群搭建及测试

①. zk是由java编写的需要java运行环境,所以大家首先要安装JDK 具体安装步骤,不再赘述      ②. 首先进入zk的conf目录,将zoo_sam...

309100
来自专栏木木玲

Netty 那些事儿 ——— Reactor模式详解

55470
来自专栏菩提树下的杨过

gradle项目中profile的实现

gradle中并没有直接类似maven中的profile支持,只能变通的用其它方法来处理,在打包不同环境的应用时,通常会遇到二类问题: 一、不同的环境依赖的ja...

23460
来自专栏北京马哥教育

万字长文为你深入解读 Linux 用户及用户组管理

运维行业正在变革,推荐阅读:30万年薪Linux运维工程师成长魔法 无论是出于 Linux 本身的多用户多任务分时操作系统的性质,还是出于系统安全的考虑, L...

42550
来自专栏黑白安全

Windows下更改Mac地址

在桌面上的“网上邻居”图标上单击右键,选择“属性”,在弹出的“网络连接”的对话框中,在“本地连接”图标上单击右键,选择“属性”,会弹出一个“本地连接属性”的对话...

24030

扫码关注云+社区

领取腾讯云代金券