spdlog高性能日志系统
spdlog
是一个快速、简单、功能丰富的 C++ 日志库,专为现代 C++ 开发设计。它支持多种日志后端(如控制台、文件、syslog 等),并提供灵活的格式化和线程安全的日志输出。
1. 特点
-
极高的性能:大量的编译时运算、使用fmt库提高格式化打印性能
-
零成本的抽象:通过模板和内联函数,将运算放到编译时
-
支持异步日志和同步日志
2. 问题
-
多线程使用日志库,跟同步和异步是否有关联?
没有关联。多线程指的是日志使用者同时有多个,而同步和异步指的是打印日志的方式。
在多线程情况下,如果往同一个文件中输出日志,日志库需要考虑线程安全,包括日志写入操作的线程安全和异步方式下日志消息队列的线程安全。
-
同一个线程处理,是不是就是同步?
不一定。例如在协程中,IO操作是在同一个线程中处理的,但是中间发生了协程上下文切换,等epoll发出事件通知后才继续处理,所以是异步的。
3. 输出控制
3.1 多种日志级别
trace、debug、info、warn、error和critical
不同日志级别反应日志信息的不同重要程度。
最低日志级别:低于最低日志级别的日志将不会被打印。
3.2 多种输出目标
控制台、文件、通过网络发送到远程服务器等。
3.3 格式化输出
使用fmt进行格式化输出,比C++标准库和snprintf等性能高30%。
4. spdlog处理流程
日志时间乱序问题
如果是写入文件中,可以用命令行工具排序。如果输出到数据库,可以使用索引。
4.1 registry
使用了单例模式
cpp
SPDLOG_INLINE registry ®istry::instance() {
static registry s_instance;
return s_instance;
}
registry中有一个thread_pool,负责异步写入日志,里面包含一个多生产者多消费者的阻塞队列。
4.2 logger
cpp
class SPDLOG_API logger {
public:
void log();
protected:
...
virtual void sink_it_(const details::log_msg &msg);
virtual void flush_();
...
}
logger的sink_it_会调用所有sinks的log方法,后者会调用其自身的sink_it_方法,sink_it_会调用flush_方法。
4.3 sink
cpp
template <typename Mutex>
class basic_file_sink final : public base_sink<Mutex> {
public:
explicit basic_file_sink(const filename_t &filename,
bool truncate = false,
const file_event_handlers &event_handlers = {});
const filename_t &filename() const;
void truncate();
protected:
void sink_it_(const details::log_msg &msg) override;
void flush_() override;
private:
details::file_helper file_helper_;
};
主要是重写sink_it_和flush_两个方法。
5. spdlog的使用
5.1 创建logger
5.1.2 工厂方法创建
工厂方法
工厂方法是把创建对象的接口抽象出来,让子类负责创建具体的产品对象。一般当产品类的创建流程比较复杂、产品类的依赖关系比较复杂或者客户没有必要知道创建哪个具体的产品类时可以使用此设计模式。
工厂方法:
调用这个工厂方法来创建logger对象。
异步工厂的create
工厂方法的目的是方便对象的创建,尤其是当对象具有复杂的创建流程与依赖关系时。
cpp
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/async.h>
int main()
{
spdlog::info("default setting");
// 工厂方法创建logger
auto logger1 = spdlog::basic_logger_mt("sync_logger", "basic.txt");
auto logger2 = spdlog::basic_logger_mt<spdlog::async_factory>("async_logger",
"basic.txt");
logger1->info("factory method setting");
logger2->info("async factory method setting");
spdlog::get("sync_logger")->error("there is an error");
return 0;
}
5.1.3 手动创建
好处是方便直接为logger绑定多个sink。手动创建的流程和工厂方法中调用的create中的创建流程类似。
下面是手动创建一个sync logger的代码。
cpp
// sync logger
auto sink1 = std::make_shared<spdlog::sinks::ansicolor_stdout_sink_mt>();
auto sink2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("manual.txt", true);
auto logger3 = std::make_shared<spdlog::logger>("manual_logger",
spdlog::sinks_init_list{sink1, sink2});
spdlog::register_logger(logger3);
logger3->info("good");
如果要手动创建一个async logger,就需要保证registry中的线程池已经被初始化,需要手动加锁检查:
cpp
auto &mutex = registry_inst.tp_mutex();
std::lock_guard<std::recursive_mutex> tp_lock(mutex);
auto tp = registry_inst.get_tp();
if (tp == nullptr) {
tp = std::make_shared<details::thread_pool>(details::default_async_q_size, 1U);
registry_inst.set_tp(tp);
}
之后,分别构造sink和logger即可
cpp
auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...);
auto new_logger = std::make_shared<async_logger>(std::move(logger_name), std::move(sink),std::move(tp), OverflowPolicy);
registry_inst.initialize_logger(new_logger);
5.2 创建sink
使用了模板方法设计模式,将从log函数的骨架中抽象出sink_it_和flush_两个方法供子类实现。sink_it_负责把日志写到用户缓冲区,flush_负责把日志刷到内核缓冲区。
模板方法
模板方法定义了一个算法框架,并将其中容易变化的步骤抽象出来交给子类去实现。通过这种方式,模板方法允许子类在不改变算法结构的情况下重新定义算法的某些特定步骤。
该设计模式适用于当一个过程中的部分步骤容易发生变化的场景。
5.3 自定义格式化
参考spdlog wiki。
5.3.1 set_pattern
cpp
// 自定义输出格式
spdlog::set_pattern("[%^L%$] %v"); // 全局
logger3->set_pattern("[%Y/%m/%d %H:%M:%S] [%^%L%$] %v"); // logger范围
sink1->set_pattern("[%Y/%m/%d %H:%M:%S] [%^%L%$] %v [OK]"); // sink范围
logger3->info("test");
5.3.2 自定义pattern flags
cpp
#include "spdlog/pattern_formatter.h"
class my_formatter_flag : public spdlog::custom_flag_formatter
{
public:
void format(const spdlog::details::log_msg &, const std::tm &, spdlog::memory_buf_t &dest) override
{
std::string some_txt = "custom-flag";
dest.append(some_txt.data(), some_txt.data() + some_txt.size());
}
std::unique_ptr<custom_flag_formatter> clone() const override
{
return spdlog::details::make_unique<my_formatter_flag>();
}
};
void custom_flags_example()
{
auto formatter = std::make_unique<spdlog::pattern_formatter>();
formatter->add_flag<my_formatter_flag>('*').set_pattern("[%n] [%*] [%^%l%$] %v");
spdlog::set_formatter(std::move(formatter));
}
5.4 创建异步logger
5.4.1 使用async factory工厂
5.4.2 使用create_async
只是对前一个方法的简单封装:
cpp
template <typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async(std::string logger_name, SinkArgs &&...sink_args) {
return async_factory::create<Sink>(std::move(logger_name), std::forward<SinkArgs>(sink_args)...);
}
5.4.3 使用create_async_nb
创建非阻塞的异步日志,与前者的区别在于,其设置了日志消息的淘汰策略。
cpp
using async_factory_nonblock = async_factory_impl<async_overflow_policy::overrun_oldest>;
template <typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async_nb(std::string logger_name, SinkArgs &&...sink_args) {
return async_factory_nonblock::create<Sink> (std::move(logger_name), std::forward<SinkArgs>(sink_args)...);
}
5.4.4 手动构造async_logger
参照:
这种方式过于繁杂,不推荐,即使想要自定义OverflowPolicy,也可以选择使用async_factory_impl
。
5.5 刷新策略
cpp
// 刷新策略
// 1. 手动flush
logger5->flush(); // 对于异步日志,只是将消息放进队列
// 2. 条件flush,设置最小的触发flush的日志等级
logger5->flush_on(spdlog::level::debug);
// 3. 间隔flush,会开启一个线程来每隔一段时间flush一次
spdlog::flush_every(std::chrono::seconds(5));
学习参考
学习更多相关知识请参考零声 github。