Cpp 日志库 spdlog 配置与使用
整理一下关于C++项目中日志库的学习,在之前曾经陆续实现过几次较为简单的日志工具, 但是在编程逐步正式化之后,自己造这种基础的轮子还是没必要的,采用成熟的spdlog日志库比较合适。
概述
spdlog是一个快速的、跨平台的C++日志库,它提供了简单易用的API,支持多种日志级别、多线程安全、异步日志写入等功能。 它具有灵活的配置选项,允许开发者根据需要自定义日志记录行为,支持日志文件的轮换和分割等常见功能。 因为spdlog的使用简单直观,文档齐全,社区活跃,它已经成为当前最常用的C++日志记录工具之一。
spdlog对使用者非常友好:
- 可以作为纯头文件库使用,直接复制到项目中
- 可以在编译安装后通过CMake导入
- 可以将源码直接放在项目文件夹下,作为CMake子项目添加并使用(作为头文件库或二进制库均可)
spdlog更推荐通过编译使用,因为可以节约整体的编译时间,否则每次编译时spdlog也要占用相当的时间,可能是用了大量模板的原因。在导入spdlog的头文件时,采用按需取用的策略,而不是某些库采用的只需要include一个万能头文件即可。
spdlog依赖fmtlib进行格式化输出,fmtlib已经被c++20收编了,变成了std::format
),为了避免版本冲突,spdlog可以选择使用内置或外部提供的fmtlib。
下面是最简单的Helloworld 1
2
3
4
5
6
int main()
{
// Use the default logger (stdout, multi-threaded, colored)
spdlog::info("Hello, {}!", "World");
}
编译与测试
将spdlog源码直接下载到本地进行编译,但是查看spdlog查看的ci.yml,发现它只支持如下几个环境:
- linux:gcc,clang
- macOS:clang
在Windows上官方并不支持,但测试发现msvc其实可以成功编译。而某些MinGW-W64发行版无法通过编译,例如 LLVM-MinGW。 编译产物是spdlog.lib,将其和整个spdlog头文件夹手动复制到项目中就可以使用了。
创建如下的main.cpp文件,
1 |
|
使用如下命令手动编译即可 1
clang++ main.cpp -Iinclude -Llib -lspdlog -o test
缺少库文件也是可以的,作为纯头文件库使用 1
clang++ main.cpp -Iinclude -o test
可以正常运行即可,程序输出内容如下 1
2
3
4
5
6
7
8[2024-04-15 19:41:38.245] [info] Welcome to spdlog!
[2024-04-15 19:41:38.245] [error] Some error message with arg: 1
[2024-04-15 19:41:38.246] [warning] Easy padding in numbers like 00000012
[2024-04-15 19:41:38.246] [critical] Support for int: 42; hex: 2a; oct: 52; bin: 101010
[2024-04-15 19:41:38.246] [info] Support for floats 1.23
[2024-04-15 19:41:38.247] [info] Positional args are supported too..
[2024-04-15 19:41:38.247] [info] left aligned
[2024-04-15 19:41:38.247] [debug] This message should be displayed..
CMake导入spdlog
基于URL添加源码
我们可以使用FetchContent
模块:基于FetchContent
模块的FetchContent_Declare
命令,它会在生成项目时,进行源码的拉取。(需要保证网络的畅通)
在项目根目录的CMakeLists.txt中添加如下片段 1
2
3
4
5
6
7include(FetchContent)
FetchContent_Declare(
spdlog
URL https://github.com/gabime/spdlog/archive/refs/tags/v1.13.0.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(spdlog)
本地下载添加源码
我们同样可以手动下载spdlog的完整源码,并复制为当前项目的根目录下的子文件夹spdlog/
,将其作为当前CMake项目的子项目,
这免去了每次从网络下载的过程。
在根目录下的CMakeLists.txt直接添加子目录作为子项目即可(注意导入的顺序要在其他使用spdlog项目之前)
1 | add_subdirectory(spdlog) |
导入已安装的spdlog
如果spdlog已经被安装了,CMake可以成功找到,那么直接导入也是可以的
1 | find_package(spdlog REQUIRED) |
注意此时使用导入的目标似乎都要带上spdlog::
名称前缀。
项目源码分析
支持头文件和编译使用
spdlog源码的项目结构大致如下
1 | |-cmake/ |
有几个比较重要的文件:
- spdlog/spdlog.h 为日志库接口,提供日志宏的属性控制函数。
- spdlog/logger.h 为日志管理器,为前后端连接的枢纽。
- spdlog/async.h 为异步模式接口。
- spdlog/sinks/base_sink.h 为日志文件格式父类,后面所有的日志文件格式都是继承该类来实现不同功能。
- spdlog/sinks/registry.h 用于登记所有的logger,及一些默认的属性,如日志格式、日志写入等级。
首先探究一下spdlog是如何实现同时支持编译使用和纯头文件使用的。
spdlog的实现文件全部以头文件或-inl.h
文件的形式存放在include/spdlog/
目录中,有的头文件和-inl.h
文件是成对出现的,有的则只含有头文件。
对于成对出现的xxx.h
和xxx-inl.h
,其中的内容为
1 |
|
1 |
|
在src/
目录下只有几个非必要的cpp文件,只是为了成功编译而加上的,这几个文件中的主要内容为
1 |
|
很明显,我们要关注下面的宏:
SPDLOG_HEADER_ONLY
,代表作为纯头文件库使用SPDLOG_COMPILED_LIB
,代表正在编译,如果没有定义SPDLOG_COMPILED_LIB
,在尝试编译那些*.cpp
文件时直接报错。
项目结构
spdlog项目的逻辑结构大致如图
主要由logger
(也包括async_logger)、sink
、formatter
、registry
这四个部分组成,它们之间的基本逻辑结构如下图所示
spdlog log API
:建立在logger
之上的用户友好型接口,只是对logger
使用的封装,目的只是为了能够像官网给的示例代码spdlog::info("Welcome to spdlog!");那样,让用户能够以最简单的方式使用spdlog打印出log。logger
:spdlog开始处理日志的入口。对于同步的sync-logger
,主要负责日志信息的整理,将格式化(通过fmt)后的日志内容、日志等级、日志时间等信息打包整理为一个名为log_msg
结构体的对象,然后再交给下游的sink进行处理。对于异步的async-logger
,则是在将整理后的log_msg
对象交给线程池,由线程池接管并处理后续的工作。sink
:接收log_msg
对象,将对象中所含有的信息通过formatter
转换成字符串,最后将字符串输出到指定的地方,例如控制台、文件等,甚至支持通过tcp/udp
将字符串进行网络传输。sink
可以理解为“下沉”或“落笔”,即负责把日志真正记录下来。formatter
:负责将log_msg
对象中的信息转换成字符串,例如等级、时间、实际内容等。时间的格式和精度、等级输出显示的颜色等都是由formatter
决定的。支持用户自定义格式。registry
:负责管理所有的logger
,包括创建、销毁、获取等。用户还可以通过registry
对所有的logger
进行一些全局设置,例如设置日志等级。
使用方法与配置
日志等级
首先需要建立的概念是日志等级,spdlog支持如下日志等级
- trace,最低等级,显示代码的执行过程信息
- debug,调试信息
- info,默认等级,输出一些提示信息
- warn,警告
- error,错误
- critical,致命错误
- off,最高等级,用于关闭所有日志记录。
等级从低到高,默认等级是info,可以设置全局的日志等级(在设置之后生效)
1
spdlog::set_level(spdlog::level::debug);
低于全局日志等级的输出语句不会输出信息,例如下面的输出语句中只有第二个才会输出
1
2
3
4// default level is info
spdlog::debug("debug no show!");
spdlog::set_level(spdlog::level::debug);
spdlog::debug("debug show!");
如果设置为最高等级off,就会关闭所有的日志输出。
与此同时还有一组宏也可以控制日志等级 1
2
3
4
5
6
7
logger与sinker
spdlog的核心概念是logger,
默认logger
为了使用方便,spdlog提供了一个默认的全局logger(输出到stdout,彩色输出,线程安全),
可以直接使用spdlog::info(...)
等语句调用。
1
spdlog::info("Hello,world!");
默认logger也可以被替换为其它logger(以shared_ptr形式)
1
2spdlog::set_default_logger(some_other_logger);
spdlog::info("Use the new default logger");
补充
默认情况下的日志输出是同步模式的,可通过以下方法开启异步模式:
1 | size_t q_size = 4096; // queue size must be power of 2 |
在异步模式下,日志先存入队列(队列占用的内存 = 设置的队列大小 * slot的大小, 64位系统下slot大小为104字节),再由工作者线程从队列中取出并输出。当队列满时,会根据设定策略处理:
- 阻塞新来的日志,直到队列中有剩余空间(默认处理方式);
- 丢弃新来的日志,需要如下设定策略:
1
spdlog::set_async_mode(q_size, spdlog::async_overflow_policy::discard_log_msg);
spdlog为了保证线程安全和效率,将很多对象都分别提供了多线程与单线程版本:
*_st
:单线程版本,不用加锁,效率更高。*_mt
:多线程版本,加锁保证程序是线程安全的。
在具体实现时,spdlog使用了下面的技巧来同时提供有锁和无锁版本,即用一个空锁类型代表不加锁的那一类处理
1
2
3
4
5
6
7using basic_file_sink_mt = basic_file_sink<std::mutex>;
using basic_file_sink_st = basic_file_sink<details::null_mutex>;
struct null_mutex {
void lock() const {}
void unlock() const {}
};