【c++中间件】spdlog日志介绍 二次封装
文章目录
- I. spdlog 的介绍
- spdlog 与 glog 组件的区别
- Ⅱ. Spdlog 的使用
- 1. 头文件 && 链接库
- 2. 日志输出等级枚举
- 3. 日志输出格式自定义
- 4. 日志记录器类
- 5. 异步日志记录类
- 6. 日志记录器工厂类
- 7. 日志落地类
- 8. 全局接口
- 9. 记录日志
- Ⅲ. 使用样例
- Ⅳ. spdlog的二次封装 -- logger.hpp
I. spdlog 的介绍
github链接
spdlog 是一个高性能、超快速、零配置的 C++ 日志库,它旨在提供简洁的 API 和丰富的功能,同时保持高性能的日志记录。它支持多种输出目标、格式化选项、线程安全以及异步日志记录。特点如下所示:
- 高性能:
spdlog专为速度而设计,即使在高负载情况下也能保持良好的性能。 - 零配置:无需复杂的配置,只需包含头文件即可在项目中使用。
- 异步日志:支持异步日志记录,减少对主线程的影响。
- 格式化:支持自定义日志消息的格式化,包括时间戳、线程ID、日志级别等。
- 多平台:跨平台兼容,支持
Windows、Linux、macOS等操作系统。 - 丰富的
API:提供丰富的日志级别和操作符重载,方便记录各种类型的日志。
spdlog 与 glog 组件的区别
glog 和 spdlog 都是流行的 C++ 日志库,它们各自具有不同的特点和优势。以下是对这两个库的对比分析,包括性能测试的结果和使用场景的考量。
glog 是由 Google 开发的一个开源 C++ 日志库,它提供了丰富的日志功能,包括多种日志级别、条件日志记录、日志文件管理、信号处理、自定义日志格式等。glog 默认情况下是同步记录日志的,这意味着每次写日志操作都会阻塞直到日志数据被写入磁盘。根据张生荣的性能对比测试分析,glog 在同步调用的场景下的性能较 spdlog 慢。在一台低配的服务器上,glog 耗时 1.027 秒处理十万笔日志数据,而在固态硬盘上的耗时为 0.475 秒。
spdlog 是一个开源的、高性能的 C++ 日志库,它 支持异步日志记录,允许在不影响主线程的情况下进行日志写入。spdlog 旨在提供零配置的用户体验,只需包含头文件即可使用。它还支持多种输出目标、格式化选项和线程安全。在同样的性能测试中,spdlog 在同步调用的场景下比 glog 快。在低配服务器上的耗时为 0.135 秒,而在固态硬盘上的耗时为 0.057 秒。此外,spdlog 还提供了异步日志记录的功能,其简单异步模式的耗时为 0.158 秒。
对比总结如下:
- 性能:从性能测试结果来看,
spdlog在同步调用场景下的性能优于glog。当涉及到大量日志数据时,spdlog显示出更快的处理速度。 - 异步日志:
spdlog支持异步日志记录,这在处理高负载应用程序时非常有用,可以减少日志操作对主线程的影响。 - 易用性:
spdlog提供了更简单的集成和配置方式,只需包含头文件即可使用,而glog可能需要额外的编译和配置步骤。 - 功能:
glog提供了一些特定的功能,如条件日志记录和信号处理,这些在某些场景下可能非常有用。 - 使用场景:
glog可能更适合那些对日志性能要求不是特别高,但需要一些特定功能的场景。而spdlog则适合需要高性能日志记录和异步日志能力的应用程序。
在选择日志库时,开发者应根据项目的具体需求和性能要求来决定使用哪个库。如果项目对日志性能有较高要求,或者需要异步日志记录来避免阻塞主线程,spdlog 可能是更好的选择。如果项目需要一些特定的日志功能,或者已经在使用 glog 且没有显著的性能问题,那么继续使用 glog 也是合理的。
安装命令:
sudo apt-get install libspdlog-dev
Ⅱ. Spdlog 的使用
1. 头文件 && 链接库
注意在编译的时候要链接库!
// 头文件:include <spdlog/spdlog.h>// makefile文件:g++ -std=c++17 -o $@ $^ -lspdlog
注意事项:
由于
spdlog.h头文件可能没有包含一些落地类的头文件,比如spdlog/sinks/stdout_color_sinks.h,所以报错的时候需要我们用grep去/usr/include/spdlog文件夹中查找一下需要的头文件,然后包含进来!
还需要在编译时候带上
-lfmt的选项进行链接!
2. 日志输出等级枚举
namespace level {enum level_enum : int {trace = SPDLOG_LEVEL_TRACE,debug = SPDLOG_LEVEL_DEBUG,info = SPDLOG_LEVEL_INFO,warn = SPDLOG_LEVEL_WARN,err = SPDLOG_LEVEL_ERROR,critical = SPDLOG_LEVEL_CRITICAL,off = SPDLOG_LEVEL_OFF,n_levels};
}
3. 日志输出格式自定义
logger->set_pattern("%Y-%m-%d %H:%M:%S [%t] [%-8l] %v");
%t:线程 ID(Thread ID)%n:日志器名称(Logger name)%l:日志级别名称(Level name),如INFO、DEBUG、ERROR等%v:日志内容(message)%Y:年(Year)%m:月(Month)%d:日(Day)%H:小时(24-hour format)%M:分钟(Minute)%S:秒(Second)
4. 日志记录器类
所有的日志信息都是通过该日志记录器类也就是 logger 类来进行输出的!
创建一个基本的日志记录器,并设置日志级别和输出模式:
namespace spdlog {class logger {// 构造函数logger(std::string name);logger(std::string name, sink_ptr single_sink);logger(std::string name, sinks_init_list sinks);// 设置日志级别和输出模式void set_level(level::level_enum log_level);void set_formatter(std::unique_ptr<formatter> f);// 设置不同级别的日志输出模式template<typename... Args>void trace(fmt::format_string<Args...> fmt, Args &&...args);template<typename... Args>void debug(fmt::format_string<Args...> fmt, Args &&...args);template<typename... Args>void info(fmt::format_string<Args...> fmt, Args &&...args);template<typename... Args>void warn(fmt::format_string<Args...> fmt, Args &&...args);template<typename... Args>void error(fmt::format_string<Args...> fmt, Args &&...args);template<typename... Args>void critical(fmt::format_string<Args...> fmt, Args &&...args);// 刷新日志void flush(); // 策略刷新--触发指定等级日志的时候立即刷新日志的输出void flush_on(level::level_enum log_level);
};
注意事项:
set_level()用于设置日志级别的函数。日志级别决定了哪些日志消息会被实际输出,哪些会被忽略。- 例如,当你调用
logger->set_level(spdlog::level::warn);时,只有warn、error和critical级别的日志消息会被记录,而trace、debug和info级别的消息将被忽略。
- 例如,当你调用
flush_on()用于设置日志在何种级别的消息输出时进行刷新操作。刷新操作是将日志信息从缓冲区写入到实际的输出目标(如文件、控制台等)。- 例如,当你调用
logger->flush_on(spdlog::level::error);,表示在 输出error级别及以上的日志时,会立即刷新日志缓冲区。在一些需要确保重要日志信息及时输出的场景下很重要,避免信息滞留在缓冲区而没有及时写入文件或显示在控制台,比如在处理错误时,确保错误信息能尽快输出,避免程序崩溃后丢失错误日志。
- 例如,当你调用
flush_every()用于设置日志的定期刷新间隔。
5. 异步日志记录类
异步日志记录类的作用是 将日志的写入操作与主线程分离,通过线程池来异步完成日志记录工作,从而提高性能,可以使用 spdlog::async_logger 来创建:
// 继承于logger,可以使用设置日志级别以及输出模式的接口
class async_logger final : public logger {async_logger(std::string logger_name, // 日志器的名称sinks_init_list sinks_list, // 日志的输出目标(落地类,下面会介绍)std::weak_ptr<details::thread_pool> tp, // 指向异步任务处理线程池的弱指针async_overflow_policy overflow_policy = async_overflow_policy::block // 队满时的策略);async_logger(std::string logger_name,sink_ptr single_sink, // 单一日志输出目标的指针,与上面的区别是这里只支持一个Sinkstd::weak_ptr<details::thread_pool> tp,async_overflow_policy overflow_policy = async_overflow_policy::block);// async_logger的工作需要异步工作线程的支持,下面是线程池类class SPDLOG_API thread_pool {thread_pool(size_t q_max_items, // 队列的最大容量,用于存放待处理的日志任务size_t threads_n, // 线程池中的线程数量std::function<void()> on_thread_start, // 一个可选的回调函数,用于在每个线程启动时执行std::function<void()> on_thread_stop); // 一个可选的回调函数,用于在每个线程结束时执行thread_pool(size_t q_max_items, size_t threads_n, std::function<void()> on_thread_start);thread_pool(size_t q_max_items, size_t threads_n);};
};// 获取默认线程池的实例的接口,默认线程池由spdlog内部维护,多个async_logger可以共享同一个线程池
// 应用场景:当不需要单独创建线程池时,可以直接使用默认线程池。
std::shared_ptr<spdlog::details::thread_pool> thread_pool() {return details::registry::instance().get_tp();
}// 默认线程池的初始化接口
inline void init_thread_pool(size_t q_size, size_t thread_count);// 使用例子:
auto async_logger = spdlog::async_logger_mt("async_logger", "logs/async_log.txt"); // 工厂类(下面会讲)
async_logger->info("This is an asynchronous info message");
6. 日志记录器工厂类
using async_factory = async_factory_impl<async_overflow_policy::block>;template<typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async(std::string logger_name, SinkArgs &&...sink_args);// 创建一个彩色输出到标准输出的日志记录器,默认工厂创建同步日志记录器
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> stdout_color_mt(const std::string &logger_name, color_mode mode = color_mode::automatic);// 标准错误
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> stderr_color_mt(const std::string &logger_name, color_mode mode = color_mode::automatic);// 指定文件
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> basic_logger_mt(const std::string &logger_name, const filename_t &filename,bool truncate = false, const file_event_handlers &event_handlers = {});// 循环文件(即用多个文件记录,保证单个文件大小不会太大)
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> 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);
...
7. 日志落地类
所谓日志落地类,就是 指定日志要往哪里输出,是往文件输出,还是往终端设备输出,都是用该类指定的!
namespace spdlog {namespace sinks {// 可以继承下面的sink类来重写接口class SPDLOG_API sink{ public:virtual ~sink() = default;virtual void log(const details::log_msg &msg) = 0;virtual void flush() = 0;virtual void set_pattern(const std::string &pattern) = 0;virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) = 0;void set_level(level::level_enum log_level);};// 不同类型的日志输出目标#ifdef _WIN32using stdout_color_sink_mt = wincolor_stdout_sink_mt;using stdout_color_sink_st = wincolor_stdout_sink_st;using stderr_color_sink_mt = wincolor_stderr_sink_mt;using stderr_color_sink_st = wincolor_stderr_sink_st;#elseusing stdout_color_sink_mt = ansicolor_stdout_sink_mt;using stdout_color_sink_st = ansicolor_stdout_sink_st;using stderr_color_sink_mt = ansicolor_stderr_sink_mt;using stderr_color_sink_st = ansicolor_stderr_sink_st;#endif// 滚动日志文件-超过一定大小则自动重新创建新的日志文件sink_ptr rotating_file_sink(filename_t base_filename, std::size_t max_size, std::size_t max_files, bool rotate_on_open = false, const file_event_handlers &event_handlers = {});using rotating_file_sink_mt = rotating_file_sink<std::mutex>;// 普通的文件落地类sink_ptr basic_file_sink(const filename_t &filename,bool truncate = false,const file_event_handlers &event_handlers = {});using basic_file_sink_mt = basic_file_sink<std::mutex>;using kafka_sink_mt = kafka_sink<std::mutex>;using mongo_sink_mt = mongo_sink<std::mutex>;using tcp_sink_mt = tcp_sink<std::mutex>;using udp_sink_mt = udp_sink<std::mutex>;.....// *_st:单线程版本,不用加锁,效率更高。// *_mt:多线程版本,用于多线程程序是线程安全的。}
}// 使用例子:
auto console_logger = spdlog::stdout_logger_mt("console_logger");
console_logger->info("This is an info message to stdout.");
8. 全局接口
spdlog 中的 set_level 接口既存在于 logger 类中,又有全局的 set_level 接口,这种设计是 为了提供灵活的日志级别控制。
void set_level(level::level_enum log_level); // 输出等级设置接口void flush_every(std::chrono::seconds interval); // 日志刷新策略:每隔 N 秒刷新一次
void flush_on(level::level_enum log_level); // 日志刷新策略:触发指定等级以上立即刷新
9. 记录日志
使用日志记录器记录不同级别的日志:
logger->trace("This is a trace message");
logger->debug("This is a debug message");
logger->info("This is an info message");
logger->warn("This is a warning message");
logger->error("This is an error message");
logger->critical("This is a critical message");
注意事项:
-
若要打印占位元素信息,不再像
printf等函数一样用%s等来表示,而是 直接用{}来表示占位符,如下所示:logger->debug("你好啊!{}", "小明");
Ⅲ. 使用样例
下面我们分别创建同步日志记录器和异步日志记录器来看看区别,无非就是创建工厂类使用的参数不同而已,其他基本都是一样的!
同步日志记录器:
#include <iostream>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
using namespace std;int main()
{// 设置全局刷新策略spdlog::flush_every(std::chrono::seconds(1)); // 设置每秒刷新spdlog::flush_on(spdlog::level::level_enum::debug); // 设置为debug级别以上立刻刷新// 设置全局日志输出等级(可忽略,因为一般下面都会对特定的日志级别进行输出)spdlog::set_level(spdlog::level::level_enum::debug);// 创建同步日志器auto logger = spdlog::stdout_color_mt("default-logger");// 设置日志同步器的刷新策略、输出级别(这也是为什么上面不需要设置全局输出等级的原因)logger->flush_on(spdlog::level::level_enum::debug);logger->set_level(spdlog::level::level_enum::info);// 设置输出格式logger->set_pattern("[%H:%M:%S][%t][%-8l] %v");// 进行简单的日志输出logger->trace("你好!{}", "liren");logger->debug("你好!{}", "liren");logger->info("你好!{}", "liren");logger->warn("你好!{}", "liren");logger->error("你好!{}", "liren");logger->critical("你好!{}", "liren");std::cout << "同步日志输出演示完毕!" << std::endl;return 0;
}
异步日志记录器:
#include <iostream>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/async.h>
using namespace std;int main()
{spdlog::flush_every(std::chrono::seconds(1)); spdlog::flush_on(spdlog::level::level_enum::debug); // 唯一不同之处:创建异步日志器,传入模板参数即可auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("default-logger");logger->flush_on(spdlog::level::level_enum::debug);logger->set_level(spdlog::level::level_enum::info);logger->set_pattern("[%H:%M:%S][%t][%-8l] %v");logger->trace("你好!{}", "liren");logger->debug("你好!{}", "liren");logger->info("你好!{}", "liren");logger->warn("你好!{}", "liren");logger->error("你好!{}", "liren");logger->critical("你好!{}", "liren");std::cout << "异步日志输出演示完毕!" << std::endl;return 0;
}
makefile 文件:
all : sync async
sync : sync.ccg++ -std=c++17 -o $@ $^ -lspdlog -lfmt
async : async.ccg++ -std=c++17 -o $@ $^ -lspdlog -lfmt
执行结果如下图所示:

Ⅳ. spdlog的二次封装 – logger.hpp
对 spdlog 二次封装的原因如下所示:
- 为了避免单例对象的锁冲突,所以需要直接创建一个全局的线程安全的日志器进行使用!因为对于日志操作,通常是频繁调用的操作,如果每次调用都需要加锁检查单例对象是否存在,会带来性能损耗,而在程序初始化时创建一个全局的日志器,可以避免这种开销。
- 因为
spdlog中的日志输出 没有包含【行号】与【文件名】,所以需要用宏来二次封装输出 - 封装出一个初始化接口,便于使用!如根据不同模式采取不同的输出落地目标,调试模式输出到标准输出,而发行模式则输出到文件中!
// logger.hpp头文件
#include <iostream>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <string>
#include <memory>std::shared_ptr<spdlog::logger> logger;// mode:true表示发布模式,false表示调试模式
// file:文件名
// level:输出等级
void init_logger(bool mode, const std::string& file, int level)
{if(mode == false) // 如果是调试模式,则建立标准输出日志器,输出等级为最低{logger = spdlog::stdout_color_mt("default-logger");logger->set_level(spdlog::level::level_enum::trace);logger->flush_on(spdlog::level::level_enum::trace);}else // 如果是发布模式,则建立文件输出日志器,输出等级根据传入的参数决定{logger = spdlog::basic_logger_mt("default-logger", file);logger->set_level((spdlog::level::level_enum)level);logger->flush_on(spdlog::level::level_enum::trace);}logger->set_pattern("[%n][%H:%M:%S][%t][%-8l]%v");
}#define LOG_TRACE(format, ...) logger->trace(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__);
#define LOG_DEBUG(format, ...) logger->debug(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__);
#define LOG_INFO(format, ...) logger->info(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__);
#define LOG_WARN(format, ...) logger->warn(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__);
#define LOG_ERROR(format, ...) logger->error(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__);
#define LOG_CRITICAL(format, ...) logger->critical(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__);
下面是测试样例,结合上前面学到的 gflags 一起使用:
// main.cc文件:
#include "logger.hpp"
#include <gflags/gflags.h>DEFINE_bool(run_mode, false, "程序运行模式:true为发布模式,false为调试模式");
DEFINE_string(log_file, "", "发布模式下用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下用于指定日志输出等级");int main(int argc, char* argv[])
{google::ParseCommandLineFlags(&argc, &argv, true);init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);LOG_TRACE("你好啊,{}!", "liren");LOG_DEBUG("你好啊,{}!", "liren");LOG_INFO("你好啊,{}!", "liren");LOG_WARN("你好啊,{}!", "liren");LOG_ERROR("你好啊,{}!", "liren");LOG_CRITICAL("你好啊,{}!", "liren");return 0;
}// makefile文件:
main : main.ccg++ -std=c++17 -o $@ $^ -lspdlog -lfmt -lgflags
执行结果如下所示:





