日志的作用
在程序运行过程中打印日志,可以帮助我们跟踪调试程序的运行状态,获取需要的调试信息。通过日志打印的时间,我们可以了解程序在特定时间的行为,分析性能瓶颈,甚至有大佬可以预测潜在的系统故障。
什么是spdlog
spdlog是一款开源的C++日志库,他有着极高的性能和几乎零成本的抽象。spdlog支持异步和同步日志记录,提供了多种日志级别,可以将日志打印在终端,文件或者自定义接收器(如数据库)。
git clone https://github.com/gabime/spdlog.git cd spdlog mkdir build & cd build cmake .. make sudo make install
spdlog为什么高效
- 零成本的抽象:spdlog通过模版和内联函数,确保只有在真正需要的时候才进行日志记录。(内联函数在需要的时候会直接编译在程序中,模版只有在我们需要的时候才会展开)
- 异步日志记录:spdlog支持异步日志记录,这意味着他可以将消息发送到线程池进行处理,不会阻塞当前线程,减少了主线程的开销。
- 高效的格式化:spdlog使用fmt库进行高效的字符串格式化,减少格式化的时间。
spdlog的特征
- 高速的日志记录:spdlog可以每秒处理百万条日志消息。
- 低内存占用:spdlog的设计确保在高负载下依然保持低内存占用。
- 灵活的配置:用户可以根据需要配置spdlog,选择异步(mt)或者同步(st),选择不同的日志级别和输出目标。
spdlog的输出控制
- 多种日志级别:spdlog的日志级别包括:trace,debug,info,warn,error和critical。
- 多目标:可以将日志输出到终端,文件和接收器(数据库、远程服务器等)。
- 格式化输出:fmt支持高调的格式化输出
spdlog处理流程
spdlog的不同组件负责不同的部分。registry负责管理所有的组件,logger负责记录日志,sink负责将日志输出到指定位置,formatter将日志转化为特定格式,Async Logger可以异步将日志消息写入sink。
spdlog使用方法
使用默认logger打印输出
#include <chrono> #include <iostream> #include <memory> #include <spdlog/async.h> #include <spdlog/async_logger.h> #include <spdlog/common.h> #include <spdlog/logger.h> #include <spdlog/spdlog.h> #include <spdlog/sinks/stdout_color_sinks.h> #include <spdlog/sinks/basic_file_sink.h> int main() { spdlog::trace("hello trace"); spdlog::debug("hello debug"); spdlog::info("hello info"); spdlog::warn("hello warn"); spdlog::error("hello error"); spdlog::critical("hello critical"); return 0; }
我们查看这些接口的源代码实现如下:
template <typename T> inline void trace(const T &msg) { default_logger_raw()->trace(msg); } template <typename T> inline void debug(const T &msg) { default_logger_raw()->debug(msg); } template <typename T> inline void info(const T &msg) { default_logger_raw()->info(msg); } template <typename T> inline void warn(const T &msg) { default_logger_raw()->warn(msg); } template <typename T> inline void error(const T &msg) { default_logger_raw()->error(msg); } template <typename T> inline void critical(const T &msg) { default_logger_raw()->critical(msg); }
我们上面说过,logger是负责记录日志消息的,在spdlog中存在一个默认logger可以直接使用,不需要我们进行配置。通过上图的输出可以看出来:默认的logger只会输出info级别及其以上级别的日志。
通过工厂模式接口配置logger
int main() { auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("console"); logger->set_pattern("***[%H:%M:%S %z][thread %t] %v***"); logger->info("logger info"); spdlog::get("console"); spdlog::get("console")->error("logger error"); return 0; }
这边我们自己定义了一个logger,对他进行了输出的格式化,并且将已经打印的日志get出来,重新打印输出。这里之所以可以get出来是因为:从上面的结构图可以看出,logger是归registry管理的,我们通过单例registry将logger找到,换句话说,每一个logger在创建时,都是向registry注册。
源码接口:
namespace spdlog { template <typename Factory> SPDLOG_INLINE std::shared_ptr<logger> stdout_color_mt(const std::string &logger_name, color_mode mode) { return Factory::template create<sinks::stdout_color_sink_mt>(logger_name, mode); } template <typename Factory> SPDLOG_INLINE std::shared_ptr<logger> stdout_color_st(const std::string &logger_name, color_mode mode) { return Factory::template create<sinks::stdout_color_sink_st>(logger_name, mode); } template <typename Factory> SPDLOG_INLINE std::shared_ptr<logger> stderr_color_mt(const std::string &logger_name, color_mode mode) { return Factory::template create<sinks::stderr_color_sink_mt>(logger_name, mode); } template <typename Factory> SPDLOG_INLINE std::shared_ptr<logger> stderr_color_st(const std::string &logger_name, color_mode mode) { return Factory::template create<sinks::stderr_color_sink_st>(logger_name, mode); } } //这里是通过工厂模式的接口,有mt(异步)和st(同步)的接口。 SPDLOG_INLINE std::shared_ptr<logger> get(const std::string &name) { return details::registry::instance().get(name); } //从这里可以看出,单例registry可以get到logger
配置格式化打印(logger层面)
logger->set_pattern("***[%H:%M:%S %z][thread %t] %v***"); • 1
这一行就是配置格式化打印,配置方法可以参考文档中的表格:
配置输出位置sink & 配置格式化打印(sink层面)
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_text = "lenn-flag"; dest.append(some_text.data(), some_text.data() + some_text.size()); } std::unique_ptr<spdlog::custom_flag_formatter> clone() const override { return spdlog::details::make_unique<my_formatter_flag>(); } }; int main() { auto logger1 = std::make_shared<spdlog::logger>(std::string("console1")); auto sink1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); auto sink2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("lenn.log"); auto formatter = std::make_unique<spdlog::pattern_formatter>(); formatter->add_flag<my_formatter_flag>('*').set_pattern("[%n] [%*] [%^%l%$] %v"); sink1->set_formatter(std::move(formatter)); sink2->set_pattern("[%^%l%$] %v"); logger1->sinks().push_back(sink1); logger1->sinks().push_back(sink2); spdlog::register_logger(logger1); spdlog::get("console1")->info("sink demoe"); return 0; }
这里我们配置了两个sink,一个是输出到终端(stdout)还有一个输出到文件(basic_file)。上面一种通过工厂接口配置logger只能有一个sink,但是我们这里手动配置logger就可以有多个sink。
我们还可以定制标志,通过继承spdlog::custom_flag_formatter来定义自己的flag,比如我这里定义了一个叫[lenn-flag]的flag。
这里我们对两个sink分别设置输出格式,而不是对logger设置格式让logger管理下的所有sink的格式都一样;当人你也可以对logger直接set_pattern来使这个logger的所有输出格式都是一样的。
sink的选取
关于sink的选取,我们可以在源代码中看到:
这些sinks都是可以使用的,有android,qt,console等等,输出到不同地方的sinks。大家可以参考文档自行选取使用。
显示日志所在行号
#include <chrono> #include <iostream> #include <memory> #include <spdlog/async.h> #include <spdlog/async_logger.h> #include <spdlog/common.h> #include <spdlog/logger.h> #include <spdlog/spdlog.h> #include <spdlog/sinks/stdout_color_sinks.h> #include <spdlog/sinks/basic_file_sink.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_text = "lenn-flag"; dest.append(some_text.data(), some_text.data() + some_text.size()); } std::unique_ptr<spdlog::custom_flag_formatter> clone() const override { return spdlog::details::make_unique<my_formatter_flag>(); } }; int main() { auto logger1 = std::make_shared<spdlog::logger>(std::string("console1")); auto sink1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); auto sink2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("lenn.log"); auto formatter = std::make_unique<spdlog::pattern_formatter>(); formatter->add_flag<my_formatter_flag>('*').set_pattern("[%n] [%*] [%^%l%$] %v"); sink1->set_formatter(std::move(formatter)); sink2->set_pattern("[%^%l%$] %v"); logger1->sinks().push_back(sink1); logger1->sinks().push_back(sink2); spdlog::register_logger(logger1); spdlog::get("console1")->info("sink demoe"); //这里可以显示行号 SPDLOG_INFO("hello line"); SPDLOG_LOGGER_INFO(logger1, "hello logger line"); return 0; }
SPDLOG_INFO宏在spdlog库中的实现使用了预定义的宏__FILE__、__LINE__和__func__(编译器的行为),这些预定义宏可以在编译时获取当前源文件名、行号和函数名。当你使用SPDLOG_INFO宏输出日志消息时,内部会自动将当前的文件名、行号和函数名信息加入到输出的日志消息中。这样就能够方便地追踪日志发生的位置。
线程池处理
int main() { auto sink1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); auto sink2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("lenn.log"); //队列中最多有8292条日志消息,有8个子线程打印输出 spdlog::init_thread_pool(8292, 8); std::vector<spdlog::sink_ptr> sinks{sink1, sink2}; auto logger_tp = std::make_shared<spdlog::async_logger>("tp", sinks.begin(), sinks.end(),//传入vector的迭代器即可 spdlog::thread_pool(), spdlog::async_overflow_policy::overrun_oldest); logger_tp->info("hello thread pool"); logger_tp->flush();//强制刷星所有注册的sink,确保日志消息立刻写入目标输出 logger_tp->flush_on(spdlog::level::err);//遇到error级别的日志立刻刷新,缺号尽快得到error级别的日志 spdlog::flush_every(std::chrono::seconds(5));//每5s刷新一次,将缓存的日志写入目标 return 0; }