没看过前面内容的,先看了再来,从这里去跳转:

registry

我们先回顾一下之前的内容。spdlog主要由logger(也包括async_logger)、sink、formatter、registry这四个部分组成,我们在前两篇介绍了前面三个(logger、sink、formatter)。实际上这三个已经足够将一条日志输出(记录)下来了,还剩下没介绍的registry则是负责管理前面那三个部件的。当然用户也可以不通过registry来自行管理。所以registry不是必须的,它本身的实现并不涉及spdlog的核心功能,只是为了更好的管理资源。例如通过registry,用户对所有logger设置日志等级、可以创建带有默认设置的logger之类的……

默认logger和默认sink

registry的代码主要在registry.h、registry-inl.h。还记得这一句最简单的使用spdlog的代码吗?

1
spdlog::info("Welcome to spdlog!");

在这里我们既没有创建logger,也没有设置sink,直接就可用了。实际上是registry帮我们创建了默认的logger和默认的sink,方便我们直接使用。这样的程序设计(产品设计),让使用者易于上手,不必先了解logger、sink等概念!用起来爽!我们来看看在spdlog::info中,registry做了什么。

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
template <typename T> void info(const T &msg) {  // 即spdlog::info
default_logger_raw()->info(msg);
}

spdlog::logger *default_logger_raw() {
return registry::instance().get_default_raw();
}

registry &registry::instance() {
static registry s_instance;
return s_instance;
}

// 直接用logger的裸指针的原因,spdlog是这么解释的:
// To be used directly by the spdlog default api (e.g. spdlog::info)
// This make the default API faster
logger *registry::get_default_raw() {
return default_logger_.get();
}

registry::registry() {
auto color_sink = std::make_shared<sinks::ansicolor_stdout_sink_mt>();
const char *default_logger_name = "";
default_logger_ = std::make_shared<spdlog::logger>(default_logger_name,
std::move(color_sink));
}

可以看到spdlog::info中default_logger_raw()得到了默认logger的指针,然后顺理成章就调用info输出日志。默认logger的指针则来自registry对象中的default_logger_成员变量。registry是单例,所以获取registry对象使用的是静态方法registry::instance()。最后我们看到registry::registry()中创建了默认logger,选择的sink是ansicolor_stdout_sink_mt,也就是彩色输出到控制台,最后的“_mt”表示是线程安全的sink。也就是说,当用户什么都没设置时调用spdlog::info时,结果是像控制台输出彩色日志。这也是用户刚上手spdlog最可能希望得到的结果。就这点设计,不得不说spdlog确实简单易用懂用户。

logger工厂

registry主要作用就是管理logger(例如将所有logger日志等级、格式等统一为相同的),那么logger创建的时候就要将其共享指针存在registry中,这样registry才能管理到。在考虑到简单易用的原则,用户可以不事先了解logger和registry概念,也不必时刻记得要把logger的共享指针存到registry中。因此spdlog提供了一系列获取logger的函数,这些函数除了构造logger对象之外,还将这个logger的共享指针存到registry中。以下是这类函数的示例:

1
2
3
4
5
6
7
8
9
10
11
// stdout_logger_mt返回使用stdout_sink的logger,且多线程版本(线程安全的)
template <typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> stdout_logger_mt(....);

// basic_logger_st返回使用basic_file_sink的logger,且单线程版本(非线程安全的)
template <typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> basic_logger_st(...);

// rotating_logger_mt返回使用rotating_file_sink的logger,且多线程版本(线程安全的)
template <typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> rotating_logger_mt(...);

spdlog几乎为所有类型的sink都提供了如上类似的logger创建函数。从函数名可以看出这类函数把sink的概念给隐藏了,普通用户只需要知道创建出来的logger能够把日志写到指定地方就行了,根本不需要知道sink这类东西的存在。我们以stdout_logger创建函数为例,看一下具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using stdout_sink_mt = stdout_sink<details::console_mutex>;  // 有锁对应多线程版本
using stdout_sink_st = stdout_sink<details::console_nullmutex>; // 无锁对应单线程版本

// 模板参数Factory都默认为spdlog::synchronous_factory
template <typename Factory>
std::shared_ptr<logger> stdout_logger_mt(const std::string &logger_name) {
return Factory::create<sinks::stdout_sink_mt>(logger_name);
}

template <typename Factory>
std::shared_ptr<logger> stdout_logger_st(const std::string &logger_name) {
return Factory::create<sinks::stdout_sink_st>(logger_name);
}

struct synchronous_factory {
template <typename Sink, typename... SinkArgs>
static std::shared_ptr<spdlog::logger> create(std::string logger_name, SinkArgs &&...args) {
auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...);
auto new_logger = std::make_shared<spdlog::logger>(std::move(logger_name), std::move(sink));
details::registry::instance().initialize_logger(new_logger);
return new_logger;
}
};

不论是stdout_logger_mt还是stdout_logger_st里面都直接调用了Factory::create。模板参数Factory都默认为spdlog::synchronous_factory。除此之外还可以是async_factory,后面会讲,我们先看synchronous_factory::create的实现,它是一个模板函数,接受两个模板参数Sink和SinkArgs。因为sink可以有很多种,而且其构造函数的参数也各不相同,所以此处只能用模板来让synchronous_factory::create支持所有sink。这个函数里做的事情就是先把logger构造出来后,再传进registry的initialize_logger方法中。看方法名就知道,initialize_logger(new_logger)有做了一些初始化,例如将该logger的formatter(存在logger中的)设置为默认formatter(存在registry中的)。因为用户大体上会希望新创建的logger能够日志格式已有的或者全局的保持统一。同时initialize_logger(new_logger)也将该logger的shared_ptr存到registry中,这样用户就可以通过registry管理该logger。

刚才讲到template 中的Factory模板参数还可以是async_factory。其实async_factory::create与synchronous_factory::create做的事情基本相同,不过因为创建的是async_logger对象(本系列上篇讲过该类),所以需要额外做些事情,这部分事情主要就是async_logger中所使用的线程池的创建。具体不再展开,感兴趣的朋友可以自己去看,代码在async.h中。

感悟

写到这里想插入一段自己看registry实现时候的感悟。registry作用是管理logger,那么势必要将logger存入其中。所以提供了Factory(包括synchronous_factory和async_factory)的create方法用于在创建logger之后返回logger给用户之前,将其注册进registry。学过设计模式的人就能想起这种通过某个统一接口创建对象的方法是工厂方法或者抽象工厂。这里不纠结具体是哪个。我之前也学过设计模式,当时看得也云里雾里,不知其真正作用,以为工厂方法重点在于重建对象或者更好的组织代码。看完registry和Factory的实现才知道,这里的重点不在于创建logger,而是在创建logger之后将其注册进registry。registry需要注册logger,所以才提供Factory来在创建logger后完成注册。

宏定义使用

结合(上)(中)两篇和这篇的上半部分,spdlog的所有核心部件都介绍完了。接下来还有一些可以分享的稀碎内容,都是跟宏定义相关的。

compiled version利用header-only version代码

在(上)中我们刚开始介绍spdlog的时候提到其有header-only version和compiled version两个版本。我们一直以来介绍用的代码都是header-only version的。其实compiled version代码跟header-only version没差,只不过是把声明和实现分别写到两种不同文件中,.h和.cpp(大废话!)。那有了header-only version之后,怎么写compiled version呢?答案是把header-only version的代码对应好“抄”过来就行了。对抄!是个好主意!

compiled version的.cpp代码都在src文件夹下,我们来看看async.cpp中的代码,注意是全部代码!

1
2
3
4
5
6
7
8
#ifndef SPDLOG_COMPILED_LIB
#error Please define SPDLOG_COMPILED_LIB to compile this file.
#endif

#include <spdlog/async.h>
#include <spdlog/async_logger-inl.h>
#include <spdlog/details/periodic_worker-inl.h>
#include <spdlog/details/thread_pool-inl.h>

不要诧异,确实这个文件里的代码就这么几行,其他.cpp文件里的代码也类似,最多50行!我们先来看看这里的前三句代码是要求SPDLOG_COMPILED_LIB必须先被定义,不然会报错。这是符号被定义就意味着我们用spdlog的代码是按compiled version的方式进行编译,反之则是header-only version。接着include进了四个文件,观察这四个文件名,有async.h和xxx-inl.h。async.h就是声明,把它include进当前async.cpp文件十分正常。而后面那是三个xxx-inl.h就是async.h对应的实现。我们通过代码来理解这是什么意思。

在header-only version,跟async(async_logger及其它的工厂方法)相关的代码主要在async.h、async_logger.h、async_logger-inl.h三个文件中。async.h在文件开头部分就include了async_logger.h,而async_logger.h在文件结尾include的async_logger-inl.h。它是这么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifdef SPDLOG_HEADER_ONLY
#include "async_logger-inl.h"
#endif
```C++
这段代码意思是如果是header-only version方式,才会把async_logger-inl.h给include进来,否则不会。我们来看看SPDLOG_COMPILED_LIB和SPDLOG_HEADER_ONLY两个宏定义是什么关系。
```C++
#ifdef SPDLOG_COMPILED_LIB
#undef SPDLOG_HEADER_ONLY
#define SPDLOG_INLINE
#else
#define SPDLOG_HEADER_ONLY
#define SPDLOG_INLINE inline
#endif
```C++
它们是互斥关系。同时还注意到这里定义了SPDLOG_INLINE,在SPDLOG_HEADER_ONLY是它表示inline,否则为空。SPDLOG_INLINE用在xxx-inl.h文件中,像下面这样。用来控制函数是内联或者非内联的。
```C++
SPDLOG_INLINE void logger::set_level(level::level_enum log_level) {
level_.store(log_level);
}

看到这里可能你还是有点云里雾里,不知道怎么回事,那让我们把上面这些信息串起来。对于header-only version,代码会被分在xxx.h和xxx-inl.h文件中,基本上xxx.h只有函数和类的声明,而实现都已inline的方式写在了xxx-inl.h中(此处inl就是inline的意思)。这样调节SPDLOG_HEADER_ONLY宏定义,可以调节.h文件中是否包含了其实现的代码,如果包含了那就是header-only version。如果不包含,那它就是compiled version中普通的头文件。并且由于实现的代码在xxx-inl.h,而compiled version时候需要在.cpp中也要有一份实现代码,所以上述async.cpp文件中就直接通过#include<xxx-inl.h>的方式“抄”过来了。这样的设计真是巧妙啊!画出示意图类似下面这样:

header-only version和compiled version的代码关系

多平台支持

spdlog是支持多平台的,不同平台的实现大体相同,但是又有部分差异。处理这部分差异的相关代码基本都在os.h和os-inl.h中了。也是通过宏定义实现,示例代码如下。经过这样封装使得更上层的业务实现对这部分差异是无感知的,只要调用对应接口就行了。

1
2
3
4
5
6
7
void sleep_for_millis(unsigned int milliseconds) {
#if defined(_WIN32)
::Sleep(milliseconds);
#else
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
#endif
}

至此,spdlog源码解析系列结束!

参考链接:https://zhuanlan.zhihu.com/p/675918624