spdlog
是一个高性能C++日志库,可以支持仅头文件(header-only)模式使用。
官网地址:
spdlog是线程安全的,但不是进程安全的,可以多个线程同时使用一个spdlog对象输出日志,但不能多个进程同时往一个文件写入日志。
在spdlog内部使用fmt进行字符串的格式化,因此spdlog字符串格式的方式与fmt库相同,如:
1 | spdlog::critical("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42); |
一、日志级别
大多数日志库都有日志级别的概念,通过设定日志级别可以动态控制我们需要打印输出的日志,spdlog支持如下日志级别:
1 | trace = SPDLOG_LEVEL_TRACE // 最低级 |
如下代码设置日志级别为info
,此时只会打印输出info及比info级别高的日志:
1 | spdlog::set_level(spdlog::level::info); |
二、Logger和Sink
spdlog主要由Logger(记录器)和Sink(输出位置)两部分组成,spdlog的高可拓展性体现在Logger和Sink的可以由用户自定义方面。
每个程序可以创建多个Logger对象,而每个Logger对象又可以包含多个Sink(也就是可以同时输出到多个位置)。
2.1 创建Sink
在介绍Logger的创建方法前,我们先看看如何创建Sink对象。
每个Sink都是一个std::shared_ptr<spdlog::sink>
对象,创建Sink方法如下:
1 | auto sink = std::make_shared<spdlog::sinks::stdout_sink_mt>(); |
spdlog有_mt
(multi threaded)和_st
(single threaded)两类后缀的sink对象,用于区分是否线程安全。单线程(_st
后缀)的sink是非线程安全的,不能被多个线程使用。
spdlog内置了多种不同的sink类型,如可以输出到文件、控制台、tcp/udp端口、Windows事件日志、mongo数据库等。完整的sink可以查看源代码的sinks目录,通常一个文件对应一个sink。
虽然内置的Sink可以满足我们的大多数需求,但spdlog依然支持自定义Sink,具体方法参考官方文档:implementing-your-own-sink
下面介绍几种常用内置sink的创建方法。
simple_file_sink
一个简单的文件接收器,将日志写入到给定的日志文件,没有任何的限制。
1 |
|
关于日志的输出目录,如上面的data/logs
目录,从splog 1.5.0版本开始,spdlog将自动创建包含日志文件的目录。但在此之前,必须手动创建目录。
rotating_file_sink
当达到最大文件大小时,关闭文件,并重命名,然后创建一个新的文件。最大文件大小和最大文件数都可以在构造函数中配置。
1 |
|
daily_file_sink
每天在指定的时间创建一个新的日志文件,并在文件名后附加一个时间戳。
1 |
|
上面代码将创建一个线程安全的sink,该sink将在每天14:55创建一个新的日志文件。
stdout_sink
输出到控制台。
1 |
|
stdout_sink with colors
输出到控制台,并带颜色标记。
1 |
|
msvc_sink
输出到Windows调试接收器(如DbgView),在spdlog内部实际使用OutputDebugStringA
进行日志输出。
1 |
|
msvc_sink
的构造函数支持check_debugger_present
参数,如果该参数为true,则仅在调试环境输出日志到Windows调试接收器。
dup_filter_sink
在日志输出时移除重复的日志。如果日志与前一条日志相同,并且间隔时间小于max_skip_duration
,则跳过输出该日志。
1 |
|
上面示例输出的日志如下:
1 | [2019-06-25 17:50:56.511] [logger] [info] Hello |
2.2 创建Logger
在创建完Sink对象后,就可以使用这些Sink来创建Logger对象。
下面示例创建了名为mylogger
的同步Logger对象,为该对象配置了2个sink。
1 | auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("logs/app.log", 1048576, 7); |
spdlog内部为每个进程都维护一张全局的Logger记录表,记载了每个进程中通过工厂方法创建的Logger实例(某些情况需要用户手动注册)。因而,在使用时只需要知道创建时指定的名称即可获取Logger对象:
1 | auto logger = spdlog::get("mylogger"); |
三、自定义日志格式
日志格式作用在Sink对象上,每个Sink都有一个格式化程序,spdlog的默认日志记录格式为:
1 | [2014-10-31 23:46:59.678] [my_loggername] [info] Some message |
我们可以通过下面方式为当前进程的所有Logger对象的所有Sink都统一设置日志格式:
1 | spdlog::set_pattern("*** [%H:%M:%S %z] [thread %t] %v ***"); |
也可以通过下面方式为指定的Logger对象的所有Sink设置日志格式:
1 | some_logger->set_pattern(">>>>>>>>> %H:%M:%S %z %v <<<<<<<<<"); |
当然可以针对具体的Sink来设置日志格式:
1 | some_logger->sinks()[0]->set_pattern(">>>>>>>>> %H:%M:%S %z %v <<<<<<<<<"); |
在设置日志格式时,spdlog都会对格式进程预编译,避免每次输出日志都进程格式解析,提升日志性能。
3.1 格式标记
下面列出了spdlog的pattern字符串支持的格式标记(类似%flag
)。
标记 | 含义 | 示例 |
---|---|---|
%v | 实际的日志文本 | some user text |
%t | 线程ID | 1232 |
%P | 进程ID | 3456 |
%n | 日志名称 | some logger name |
%l | 日志等级全称 | debug、info等 |
%L | 日志等级简写 | D、I等 |
%a | 简写星期名称 | Thu |
%A | 星期名称全程 | Thursday |
%b | 简写月份名称 | Aug |
%B | 月份名称全程 | August |
%c | 日期和时间 | Thu Aug 23 15:35:46 2014 |
%C | 两位数表示年份 | 2014输出14 |
%Y | 四位数表示年份 | 2014 |
%D | MM/DD/YY格式的日期 | 08/23/14 |
%m | 月份(01-12) | 11 |
%d | 天(01-31) | 29 |
%H | 24小时制的小时(00-23) | 23 |
%I | 12小时制的小时(01-12) | 11 |
%M | 分钟(00-59) | 59 |
%S | 秒(00-59) | 58 |
%e | 毫秒 | 678 |
%f | 微妙 | 056789 |
%F | 纳秒 | 256789123 |
%p | AM/PM | AM |
%r | 12小时制时间 | 02:55:02 PM |
%R | 24小时制时间,等同于 %H:%M | 23:55 |
%T或%X | ISO 8601时间格式,等同于%H:%M:%S | 23:55:59 |
%z | ISO 8601时间格式,时区偏移 ([+/-]HH:MM) | 如中国是东8区,+08:00 |
%E | 时间戳 | 1528834770 |
%% | 输出% | % |
%+ | spdlog默认格式 | [2014-10-31 23:46:59.678] [mylogger] [info] Some message |
%^ | 开始颜色标记(只能使用一次) | [mylogger] [info(green)] Some message |
%$ | 结束颜色标记(如%^[+++]%$ %v) (只能使用一次) | [+++] Some message |
%@ | 源文件路径和所在行数,等同于%g:%# | /some/dir/my_file.cpp:123 |
%s | 源文件名 | my_file.cpp |
%g | 源文件的完整路径或相对路径,等同于__FILE__宏 | /some/dir/my_file.cpp |
%# | 源码所在行数 | 123 |
%! | 源码所在函数名 | my_func |
%o | 与上条日志的间隔时间(毫秒) | 456 |
%i | 与上条日志的间隔时间(微秒) | 456 |
%u | 与上条日志的间隔时间(纳秒) | 11456 |
%O | 与上条日志的间隔时间(秒) | 4 |
使用示例:
1 | auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("logs/app.log", 1048576, 7); |
输出日志格式如下:
1 | 2023-12-21 09:34:45.691 +08:00 Dock1703122417075610 info [15460 6948] this is log text |
源码位置标记
如果需要使用源码位置标记,如%s
, %g
, %#
, %!
,需要在包含spdlog头文件之前,定义如下宏:
1 |
3.2 对齐
每个格式标记可以通过预先添加宽度标记来实现对齐。
使用-
(左对齐)或=
(中间对齐)控制对齐方向:
对齐 | 含义 | 示例 | 结果 |
---|---|---|---|
% |
右对齐 | %8l | “ info” |
%- |
左对齐 | %-8l | “info “ |
%= |
中间对齐 | %=8l | “ info “ |
3.3 截断
还可以通过添加!
标记来进行数据截断,如果宽度超过指定的宽度,则截断结果:
对齐 | 含义 | 示例 | 结果 |
---|---|---|---|
% |
右对齐并长度超过3个后截断 | %3!l | “inf” |
%- |
左对齐并长度超过2个后截断 | %-2!l | “in” |
%= |
中间对齐并长度超过1个后截断 | %=1!l | “i” |
四、输出策略(Flush policy)
基于性能考虑,spdlog不会立即输出日志,而是在内部通过日志队列的方式缓存日志,在合适的时候进行批量输出。
如果需要让spdlog立即输出队列中的日志,可以通过下面的几种方式实现。
4.1 手动操作
我们可以单独调用Sink的flush
函数立即输出队列中的日志,也可以调用Logger对象的flush
函数让该对象下的所有sink立即输出。
如果是异步日志,调用flush函数只会给日志队列发送一个flush消息,不会立即刷新。
在程序退出前,spdlog会确保队列中的所有日志都输出完成。如果想手动确保所有日志输出完成后退出程序,可以调用spdlog::shutdown()
函数。
4.2 基于日志等级的输出
您可以设置将触发自动输出的最低日志级别。例如,每当记录错误或更严重的消息时,这将触发立即输出:
1 | my_logger->flush_on(spdlog::level::err); |
4.3 定时输出
所有已注册的Logger每5秒的定期输出:
1 | spdlog::flush_every(std::chrono::seconds(5)); |
这种方式只能使用在线程安全的sink上(即_mt
后缀),因为定时输出任务执行在不同的线程上。
五、异常
spdlog在其文档中关于异常的说明,spdlog只会在构造Logger和Sink错误时主动抛出异常,因为spdlog认为这个错误是致命的,但这个并不可信,最多只能认为spdlog在其他情况下不会主动抛出异常,但不能保证其依赖的库(如fmt)不会抛出异常。
而且如果多个进程向同一个文件写入日志,当文件达到最大大小时,spdlog会自动重命名当前文件,并新建一个新的日志文件。如果多个进程向同一个文件写入日志,会导致重命名日志文件失败,splog会将该失败认为是致命错误,并抛出异常。
如果在打印日志时出现了spdlog能够预料的错误(非预料的及三方库抛出的异常仍然会传递到外部),spdlog默认将向stderr
打印错误消息,为了避免错误消息充斥屏幕,每个Logger的错误消息输出速率限制为1条消息/分钟。
spdlog提供了错误处理方法,来让用户自定义如何处理错误信息,我们可以通过set_error_handler
函数来自定义错误处理方法:
1 | // 全局为每个Logger注册错误处理方法 |
spdlog不是异常安全的,在使用spdlog时一定要注意异常捕获,否则可能因为一个格式化标记写错导致程序崩溃,本来是想通过日志来排除问题的,结果却因为日志导致程序崩溃,如:
1 | // 导致程序崩溃 |
六、宽窄字符
spdlog内部将字符作为char
类型处理,如果需要输出wchar_t
类型的字符串,需要将其转换为char类型,spdlog仅将char类型字符串原样输出,不做字符编码判断和处理,需要调用者来觉得字符串的编码,建议统一日志字符串的编码格式为UTF-8或ANSI。
七、日志格式
spdlog日志字符串的格式化处理使用的是开源fmt库,其详细语法见:Format String Syntax。
下面列举了常用的日志格式化方法。
1 | logger->info("输出char字符串:{}", "hello"); |