本文主要讲的是在第三方依赖库源码中如何优雅地打印日志的问题。打个比方,代码工程A依赖libclient库,对于工程A来说libclient库就是一个第三方依赖库。libclient库也需要打印日志,所以本文要讨论的问题是:“如何在libclient库中优雅地打印日志,既能不跟任何其他日志库耦合在一起,也让libclient库的使用者能够自主选择使用哪个日志库来实现日志打印功能。”通过阅读本文,读者可以了解到第三方库会遇到什么日志打印问题,以及如何更好的解决这些问题。阅读本文大概需要五分钟左右。
问题背景
小明和大卫是同一个部门不同项目组的同事。小明开发了一个RPC库libclient,性能好、功能又齐全,在自己的项目中使用得很好。
这天大卫也想将小明开发的libclient库集成到自己的工程,然而令他烦恼的是,自己的工程使用的是spdlog日志库实现日志打印功能,而libclient使用的是glog日志库实现日志打印功能,大卫认为不应该同时引入两个日志库到自己的工程,所以想在libclient也支持使用spdlog日志库实现日志打印功能。于是大卫给小明提了这个需求。
小明认为不是很必要,其实使用两个日志库也没有关系,并极力劝说大卫保留现状。
问题痛点
作为第三方依赖库,libclient耦合较重。libclient可能也会依赖其他第三方依赖库,但是原则上libclient应该尽量较少依赖其他第三方依赖库,也就是尽量轻量化,不要背负太多东西,除非万不得已。大卫作为自己工程的owner,对在libclient使用哪种日志库实现日志打印功能,应该拥有选择权,而不应该强行绑定。
解决方案
使用依赖注入方法能很好解决这个问题。
依赖注入(Dependency Injection,简称DI)是一种设计模式。当某个角色(调用者) 需要另一个角色(被调用者)的协助时,在 传统的程序设计过程中,通常由调用者来创建被调用者的实例。但是使用依赖注入设计模式,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者实例的工作通常由第三方来完成,然后注入调用者,因此也称为依赖注入。
依赖注入有两种:设值注入、构造注入。设置注入是指通过Set成员函数来设置被调用者的实例;而构造注入是指在调用者实例构造函数时就通过参数将被调用者的实例传入调用者。
方案实现
方案的实现,有两个关键点:
- 依赖注入,如何注入;
- 注入会后,如何使用。
对于关键点1,上一节也讲过,依赖注入有两种:设值注入、构造注入。这里我们选择设值注入,比如:SetLogger(logger_instance)。只提供logger的接口,具体如何实现日志打印,交给使用方来实现。
对于关键点2,如何使用?可以通过获取接口使用,比如:GetLogger(),只负责获取set_logger的结果值。所以在使用的时候其实不关心logger如何实现日志日志打印,这块和logger的实现是解耦的。
日志打印接口
先定义一个日志打印的接口:
class Logger {
public:
struct Location {
Location(const char* file, int line)
: file_(file), line_(line) {}
const char* file_ = nullptr;
int line_ = 0;
};
Logger(SEVERITY severity): severity_(severity) {}
virtual ~Logger() {}
//日志级别Set & Get
virtual SEVERITY GetSeverity() { return severity_; }
virtual void SetSeveriy(SEVERITY severity) { severity_ = severity; }
//各个严重级别的日志打印接口
virtual void Trace(Location& loc, const char* message) = 0;
virtual void Debug(Location& loc, const char* message) = 0;
virtual void Info(Location& loc, const char* message) = 0;
virtual void Warning(Location& loc, const char* message) = 0;
virtual void Error(Location& loc, const char* message) = 0;
virtual void Fatal(Location& loc, const char* message) = 0;
virtual void Off(Location& loc, const char* message) = 0;
private:
SEVERITY severity_;
};
日志打印实例
声明一个全局的日志打印实例:
std::shared_ptr<Logger> logger_;
可以通过 Set 对它更新:
void SetLogger(std::shared_ptr<Logger>& logger) {
logger_ = logger;
}
也可以通过 Get 获取它:
std::shared_ptr<Logger> GetLogger() {
return logger_;
}
在第三方依赖库中打印日志
GetLogger()->Info(Logger::Location(__FILE__, __LINE__), "hello world!");
更优雅一点的方式
#define COMMON_LOG_INFO(message) \
GetLogger()->Info(Logger::Location(__FILE__, __LINE__), message);
#define COMMON_LOG(severity, message) COMMON_LOG_ ## severity(message);
COMMON_LOG(INFO, "hello world");
那这里读者可能也发现一个问题,这个打印接口对于格式化日志不太友好,能不能像std::cout这种打印呢?当然可以,如下所示:
COMMON_LOG(INFO) << "hello" << " " << "world";
源码比较多,这里就不贴出,感兴趣的读者,可以去github下载源码,地址:github.com/sullivan120…
。
那使用方如何实现日志打印呢?
日志打印实现
这是一个把日志打印到终端的实现例子:
class MyLoggerImpl: public Logger {
public:
MyLogger(cmlg::SEVERITY severity): cmlg::Logger(severity) {}
virtual void Trace(Location& loc, const char* message) override {
std::cout << "[TRACE][" << loc.file_ << ":" << loc.line_ << "] " << message << std::endl;
}
virtual void Debug(Location& loc, const char* message) override {
std::cout << "[DEBUG][" << loc.file_ << ":" << loc.line_ << "] " << message << std::endl;
}
virtual void Info(Location& loc, const char* message) override {
std::cout << "[INFO][" << loc.file_ << ":" << loc.line_ << "] " << message << std::endl;
}
virtual void Warning(Location& loc, const char* message) override {
std::cout << "[WARNING][" << loc.file_ << ":" << loc.line_ << "] " << message << std::endl;
}
virtual void Error(Location& loc, const char* message) override {
std::cout << "[ERROR][" << loc.file_ << ":" << loc.line_ << "] " << message << std::endl;
}
virtual void Fatal(Location& loc, const char* message) override {
std::cout << "[FATAL][" << loc.file_ << ":" << loc.line_ << "] " << message << std::endl;
}
virtual void Off(Location& loc, const char* message) override {
std::cout << "[OFF][" << loc.file_ << ":" << loc.line_ << "] " << message << std::endl;
}
};
如果使用方像把日志打印到glog,只需把std::cout改成LOG(severity)即可,severity可能是INFO、WARNING、ERROR等。
class MyLogger: public Logger {
public:
MyLogger(cmlg::SEVERITY severity): cmlg::Logger(severity) {}
...
virtual void Info(Location& loc, const char* message) override {
LOG(INFO) << "[INFO][" << loc.file_ << ":" << loc.line_ << "] " << message << std::endl;
}
...
};
最后
完整的源码实现,可是去github下载,地址为:github.com/sullivan120… 。