跟Envoy学logging
潘忠显 / 2020-11-24
Envoy的日志打印非常灵活,其中启动命令说明中,有很大一部分参数是关于日志的说明,包括日志级别、日志格式、日志路径等参数。其中有一个参数比较特别:
--component-log-level <string>
参数的解释:(optional) The comma separated list of logging level per component. Non developers should generally never set this option. For example, if you want upstream component to run at debug level and connection component to run at trace level, you should pass upstream:debug,connection:trace
to this flag. See ALL_LOGGER_IDS
in /source/common/common/logger.h for a list of components.
也就是说,Envoy可以针对的不同的组件来设置不同的日志级别。
本文中会简介Envoy日志模块依赖的spdlog外部项目,并介绍了Envoy对日志模块的设计。
spdlog
C++库,可以只引用头文件即可。Envoy的日志实现就是在这个库基础上做了一些封装。
仓库地址:https://github.com/gabime/spdlog
wiki地址:https://github.com/gabime/spdlog/wiki
项目中的术语
logger:用于记录日志的对象
rotate:日志文件的切换
registry:注册处,从统一一个位置来获得logger
sink : 实际执行落日志到文件或DB动作的类
mt:multiple-thread 的缩写,带_mt
后缀的是多线程安全的
st:single-thread的缩写,带_st
的函数非线程安全的
ex:exception的缩写,spdlog::spdlog_ex
slot:插槽,在spdlog线程池构造时,预分配queue slot
tweaking:(稍稍改进),自己可以指定一些参数。
flush:刷日志
bundled:捆绑的,spdlog/include/spdlog/fmt/bundled/
使用的外部库的代码
只有头文件
spdlog is a header only library.
include what you need – your code should include the features that actually needed.
引起的问题:
Because spdlog is header-only, building shared libraries and using it in a main program will not share the registry between them. This means that calls to functions like
spdlog::set_level(spdlog::level::level_enum::info)
will not change loggers in DLLs.
Registry
使用unordered_map
存储名称和logger的映射关系,可以比较方便的在各个位置使用对应的logger.
自定义Sink
Sinks are the objects that actually write the log to their target. Each sink should be responsible for only single target (e.g file, console, db), and each sink has its own private instance of formatter object.
自定义的sink需要继承并实现spdlog::sinks::sink
中的纯虚函数。
推荐使用spdlog::sinks::sink
,可以处理线程锁,创建多线程安全的sink类。
class DelegatingLogSink : public spdlog::sinks::sink
格式化输入
使用了fmt库,打印日志的时候,会使用跟Python中str.format相似语法。
spdlog::warn("Easy padding in numbers like {:08d}", 12);
spdlog::critical(
"Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
spdlog::info("Support for floats {:03.2f}", 1.23456);
spdlog::info("Positional args are {1} {0}..", "too", "supported");
spdlog::info("{:<30}", "left aligned");
自定义行格式
默认格式
[2014-10-31 23:46:59.678] [my_loggername] [info] Some message
全局格式
spdlog::set_pattern("*** [%H:%M:%S %z] [thread %t] %v ***");
单logger格式
some_logger->set_pattern(">>>>>>>>> %H:%M:%S %z %v <<<<<<<<<");
线程池
by default, spdlog create a global thread pool with queue size of 8192 and 1 worker thread which to serve all async loggers.
This means that creating and destructing async loggers is cheap, since they do not own nor create any backing threads or queues- these are created and managed by the shared thread pool object.
All the queue slots are pre-allocated on thread-pool construction (each slot occupies ~256 bytes on 64bit systems)
Envoy使用spdlog
Logger – 对spdlog::logger
封装
class Envoy::Logger::Logger
对 spdlog::logger
进行了封装。
class Logger {
...
private:
std::shared_ptr<spdlog::logger> logger_; // Use shared_ptr here to allow static construction
// of vector in Registry::allLoggers().
// TODO(junr03): expand Logger's public API to delete this friendship.
friend class Registry;
};
class Envoy::Logger::StandardLogger
继承自Logger,使用的是自定义的DelegatingLogSink
.
Registry
与spdlog相同,Envoy 也有一个注册处 Registry,维护了其使用的所有命名 logger,可以根据不同模块获得不同的 logger。
spdlog 本身使用的是 unordered_map
存储名称和logger的映射关系,而 Envoy 使用一个vector<Logger>
存储对应类型的logger。
同时,Envoy为每个需要logger的类型定义一个Enumerate的值(Envoy::logger::Id),因为Id值恰好是从0开始,可以与vector<Logger>
中的元素一一对应,所以可以很方便的根据模块ID找到从Registry中找到对应的logger。
std::vector<Logger>& Registry::allLoggers() {
static std::vector<Logger>* all_loggers =
new std::vector<Logger>({ALL_LOGGER_IDS(GENERATE_LOGGER)});
return *all_loggers;
}
spdlog::logger& Registry::getLog(Id id) { return *allLoggers()[static_cast<int>(id)].logger_; }
Envoy::logger::Id 定义过程涉及到一个C++技巧,点击查看详细解释。使用枚举+向量的方式带来一个缺点(TODO),因为枚举无法动态的添加,所以也无法允许扩展的增加 logger 打印日志。
Loggable 类模板
上边说的命名 logger 主要按照功能模块进行区分,举例如:admin
、client
、config
、filter
、health_checker
、main
、router
。Envoy的代码都是面向对象编程,而这些功能模块恰好与会与一些class对应,比如涉及到main
相关的有main函数中调用到server_.run()的Envoy::Server::InstanceImpl
。
为了方便,Envoy定义了一个模板类:
template <Id id> class Loggable {
protected:
static spdlog::logger& __log_do_not_use_read_comment() {
static spdlog::logger& instance = Registry::getLog(id);
return instance;
}
};
这样的话,在Envoy::Server::InstanceImpl
中就可以调用上边的成员函数去获得 logger 的实例。
为了使用方便, Envoy在logger.h头文件中定义了大量的宏,用于方便的上报日志。比如,使用宏定义ENVOY_LOGGER()
去获得logger实例:
#define ENVOY_LOGGER() __log_do_not_use_read_comment()
使用ENVOY_LOG
去上报日志(对获得 logger 又进行了封装)
#define ENVOY_LOG(LEVEL, ...) ENVOY_LOG_TO_LOGGER(ENVOY_LOGGER(), LEVEL, ##__VA_ARGS__)
可以使用命令行中--enable-fine-grain-logging
参数指定不使用Loggable
,而是替换用各个文件中的logger。
DelegatingLogSink
Registry中有一个静态成员函数getSink()
,会返回一个DelegatingLogSink
的共享指针。
static DelegatingLogSinkSharedPtr getSink() {
static DelegatingLogSinkSharedPtr sink = DelegatingLogSink::init();
return sink;
}
DelegatingLogSink
继承自spdlog::sinks::sink
,声明了自己的友类SinkDelegate
,这就是spdlog中自定义的sink的方式,实现了以下sink基类中的纯虚函数:
virtual void log(const details::log_msg &msg) = 0;
virtual void flush() = 0;
virtual void set_pattern(const std::string &pattern) = 0;
virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) = 0;
除此之外,DelegatingLogSink
还有两个函数,调用了初始化init
之后,才能构造一个sink的对象并用共享指针管理,然后方能使用。
bool hasLock() const { return stderr_sink_->hasLock(); }
static DelegatingLogSinkSharedPtr init();
SinkDelegate
DelegatingLogSink中还维护着sink的栈,是通过SinkDelegate
这个类来实现的。
void setDelegate(SinkDelegate* sink) {
absl::WriterMutexLock lock(&sink_mutex_);
sink_ = sink;
}
SinkDelegate* delegate() {
absl::ReaderMutexLock lock(&sink_mutex_);
return sink_;
}
SinkDelegate* sink_ ABSL_GUARDED_BY(sink_mutex_){nullptr};
Stacks logging sinks, so you can temporarily override the logging mechanism, restoring the previous state when the DelegatingSink is destructed.
SinkDelegate
是一个接口类(需要其子类实现具体功能),也一个Stack的节点类型,维护着SinkDelegate* previous_delegate_
指针,其定义如下:
class SinkDelegate : NonCopyable {
public:
explicit SinkDelegate(DelegatingLogSinkSharedPtr log_sink);
private:
SinkDelegate* previous_delegate_{nullptr};
DelegatingLogSinkSharedPtr log_sink_;
}
void SinkDelegate::setDelegate() {
// There should be no previous delegate before this call.
assert(previous_delegate_ == nullptr);
previous_delegate_ = log_sink_->delegate();
log_sink_->setDelegate(this);
}
void SinkDelegate::restoreDelegate() {
// Ensures stacked allocation of delegates.
assert(log_sink_->delegate() == this);
log_sink_->setDelegate(previous_delegate_);
previous_delegate_ = nullptr;
}
只有在setDelegate
中有对previous_delegate_
赋非空指针的值,只有在restoreDelegate
中有对previous_delegate_
赋空(除了构造函数)。
因此,上边的两个断言等价于,对于一个SinkDelegate对象,setDelegate()
只能依次交替调用set和restore两个函数。
SinkDelegate
实际是个接口类,就是不会有直接使用这个类来创建对象。而其一个实现FileSinkDelegate
在构造由函数中调用一次setDeltegate(),在析构中调用了一次restoreDelegate()
DelegatingLogSink与SinkDelegate的关系
TODO 后续继续整理中
两个实现了的SinkDelegate
class StderrSinkDelegate
将日志打印到stderr
class FileSinkDelegate
将日志落到文件中