spdlog是一个高性能C++日志库,可以支持仅头文件(header-only)模式使用。

官网地址:

https://github.com/gabime/spdlog

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
2
3
4
5
6
trace = SPDLOG_LEVEL_TRACE   // 最低级
debug = SPDLOG_LEVEL_DEBUG
info = SPDLOG_LEVEL_INFO
warn = SPDLOG_LEVEL_WARN
err = SPDLOG_LEVEL_ERROR
critical = SPDLOG_LEVEL_CRITICAL // 最高级

如下代码设置日志级别为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
2
3
#include "spdlog/sinks/basic_file_sink.h"
// ...
auto logger = spdlog::basic_logger_mt("mylogger", "log.txt");

关于日志的输出目录,如上面的data/logs目录,从splog 1.5.0版本开始,spdlog将自动创建包含日志文件的目录。但在此之前,必须手动创建目录。

rotating_file_sink

当达到最大文件大小时,关闭文件,并重命名,然后创建一个新的文件。最大文件大小和最大文件数都可以在构造函数中配置。

1
2
3
4
#include "spdlog/sinks/rotating_file_sink.h"
// ...

auto file_logger = spdlog::rotating_logger_mt("file_logger", "data/logs/mylogfile.log", 1048576 * 5, 3);

daily_file_sink

每天在指定的时间创建一个新的日志文件,并在文件名后附加一个时间戳。

1
2
3
#include "spdlog/sinks/daily_file_sink.h"
// ...
auto daily_logger = spdlog::daily_logger_mt("daily_logger", "logs/daily", 14, 55);

上面代码将创建一个线程安全的sink,该sink将在每天14:55创建一个新的日志文件。

stdout_sink

输出到控制台。

1
2
#include "spdlog/sinks/stdout_sinks.h"
auto sink = std::make_shared<spdlog::sinks::stdout_sink_mt>();

stdout_sink with colors

输出到控制台,并带颜色标记。

1
2
#include "spdlog/sinks/stdout_sinks.h"
auto sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

msvc_sink

输出到Windows调试接收器(如DbgView),在spdlog内部实际使用OutputDebugStringA进行日志输出。

1
2
3
#include "spdlog/sinks/msvc_sink.h"
// ...
auto sink = std::make_shared<spdlog::sinks::msvc_sink_mt>(false);

msvc_sink的构造函数支持check_debugger_present参数,如果该参数为true,则仅在调试环境输出日志到Windows调试接收器。

dup_filter_sink

在日志输出时移除重复的日志。如果日志与前一条日志相同,并且间隔时间小于max_skip_duration,则跳过输出该日志。

1
2
3
4
5
6
7
8
9
#include "spdlog/sinks/dup_filter_sink.h"

auto dup_filter = std::make_shared<dup_filter_sink_mt>(std::chrono::seconds(5));

spdlog::logger l("logger", dup_filter);
l.info("Hello");
l.info("Hello");
l.info("Hello");
l.info("Different Hello");

上面示例输出的日志如下:

1
2
3
[2019-06-25 17:50:56.511] [logger] [info] Hello
[2019-06-25 17:50:56.512] [logger] [info] Skipped 3 duplicate messages..
[2019-06-25 17:50:56.512] [logger] [info] Different Hello

2.2 创建Logger

在创建完Sink对象后,就可以使用这些Sink来创建Logger对象。

下面示例创建了名为mylogger的同步Logger对象,为该对象配置了2个sink。

1
2
3
4
5
6
7
8
9
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("logs/app.log", 1048576, 7);

auto msvc_sink = std::make_shared<spdlog::sinks::msvc_sink_mt>(false);

std::vector<spdlog::sink_ptr> sinks = {file_sink, msvc_sink};

auto logger = std::make_shared<spdlog::logger>("mylogger",
sinks.begin(),
sinks.end());

spdlog内部为每个进程都维护一张全局的Logger记录表,记载了每个进程中通过工厂方法创建的Logger实例(某些情况需要用户手动注册)。因而,在使用时只需要知道创建时指定的名称即可获取Logger对象:

1
2
3
auto logger = spdlog::get("mylogger");

logger->info("some things want to say.");

三、自定义日志格式

日志格式作用在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
2
3
4
5
some_logger->sinks()[0]->set_pattern(">>>>>>>>> %H:%M:%S %z %v <<<<<<<<<");
some_logger->sinks()[1]->set_pattern("..");

// 或
some_sink->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
2
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("logs/app.log", 1048576, 7);
file_sink->set_pattern("%Y-%m-%d %H:%M:%S.%e %z %-10n %-8l [%P %t] %v");

输出日志格式如下:

1
2023-12-21 09:34:45.691 +08:00 Dock1703122417075610 info     [15460 6948] this is log text

源码位置标记

如果需要使用源码位置标记,如%s, %g, %#, %!,需要在包含spdlog头文件之前,定义如下宏:

1
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_TRACE

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
2
3
4
5
6
7
8
9
// 全局为每个Logger注册错误处理方法
spdlog::set_error_handler([](const std::string& msg) {
std::cerr << "my err handler: " << msg << std::endl;
});

// 分别为特定的Logger注册错误处理方法
critical_logger->set_error_handler([](const std::string& msg) {
throw std::runtime_error(msg);
});

spdlog不是异常安全的,在使用spdlog时一定要注意异常捕获,否则可能因为一个格式化标记写错导致程序崩溃,本来是想通过日志来排除问题的,结果却因为日志导致程序崩溃,如:

1
2
// 导致程序崩溃
logger->error("create device enumerator failed, hr: {#x}", hr);

六、宽窄字符

spdlog内部将字符作为char类型处理,如果需要输出wchar_t类型的字符串,需要将其转换为char类型,spdlog仅将char类型字符串原样输出,不做字符编码判断和处理,需要调用者来觉得字符串的编码,建议统一日志字符串的编码格式为UTF-8或ANSI。

七、日志格式

spdlog日志字符串的格式化处理使用的是开源fmt库,其详细语法见:Format String Syntax

下面列举了常用的日志格式化方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
logger->info("输出char字符串:{}", "hello");
// 输出char字符串:hello

logger->info("输出std::string:{}", std::string("hello"));
// 输出std::string:hello

/*
* 输出最小长度的字符串,如果长度不足则用固定字符填充,但超出不会截断字符串
*/
logger->info("输出左对齐字符串(最小长度10,空格填充):{:<10}", "hello");
// 输出左对齐字符串(最小长度10,空格填充):hello

logger->info("输出左对齐字符串(最小长度10):{:<10}", "hello my world");
// 输出左对齐字符串(最小长度10):hello my world

logger->info("输出右对齐字符串(最小长度10,空格填充):{:>10}", "hello");
// 输出右对齐字符串(最小长度10,空格填充): hello

logger->info("输出居中对齐字符串(最小长度10,空格填充):{:^10}", "hello");
// 输出居中对齐字符串(最小长度10,空格填充): hello

logger->info("输出居中对齐字符串(最小长度10,*填充):{:*^10}", "hello");
// 输出居中对齐字符串(最小长度10,*填充):**hello***

logger->info("输出整型:{}", 1234);
// 输出整型:1234

logger->info("输出布尔类型:{}", true);
// 输出布尔类型:true

logger->info("输出float:{}", 3.1415936f);
// 输出float:3.1415937

logger->info("输出double:{}", 3.1415936);
// 输出double:3.1415936

/*
* 输出最小位数(含小数点)的浮点型
* 小数点前部的最小位数=总位数-后部固定位数-1,位数不足默认填充空格,超过不截断;
* 小数点后部的位数始终固定,位数不足则在尾部添加0,超出则四舍五入后截断;
*/
logger->info("输出浮点型(含小数点最小位数6位(左边填充空格),小数点后始终3位):{:6.3f}", 3.1415936);
// 输出浮点型(含小数点最小位数6位(左边填充空格),小数点后始终3位): 3.142

logger->info("输出浮点型(含小数点最小位数6位(左边填充空格),小数点后始终3位):{:6.2f}", 31415926.1);
// 输出浮点型(含小数点最小位数6位(左边填充空格),小数点后始终3位):31415926.10

logger->info("输出小写十六进制:{:x}", 123);
// 输出小写十六进制:7b

logger->info("输出大写十六进制:{:X}", 123);
// 输出大写十六进制:7B

logger->info("输出小写十六进制(带0x前缀):{:#x}", 123);
// 输出小写十六进制(带0x前缀):0x7b

logger->info("输出大写十六进制(带0x前缀):{:#X}", 123);
// 输出大写十六进制(带0x前缀):0X7B

logger->info("输出小写十六进制(固定8位,不足补0):{:08x}", 123);
// 输出小写十六进制(固定8位,不足补0):0000007b

logger->info("输出小写十六进制(带0x前缀,固定8位,不足补0):{:#010x}", 123);
// 输出小写十六进制(带0x前缀,固定8位,不足补0):0x0000007b

HRESULT hr = E_FAIL;
logger->info("输出HRESULT:{:#010x}", (unsigned long)hr);
// 输出HRESULT:0x80004005