使用
日志级别
在logger.cpp/logger.cpp定义了以下 五个日志级别:
typedef enum {
LTrace = 0, LDebug, LInfo, LWarn, LError
} LogLevel;
日志的调用方式
在logger.cpp/logger.h中提供了五个日志级别的宏定义
//用法: DebugL << 1 << "+" << 2 << '=' << 3;
#define WriteL(level) ::toolkit::LogContextCapture(::toolkit::getLogger(), level, __FILE__, __FUNCTION__, __LINE__)
#define TraceL WriteL(::toolkit::LTrace)
#define DebugL WriteL(::toolkit::LDebug)
#define InfoL WriteL(::toolkit::LInfo)
#define WarnL WriteL(::toolkit::LWarn)
#define ErrorL WriteL(::toolkit::LError)
在test_logger.cpp中有使用示例
源码分析
路径
- logger.cpp/logger.cpp
类图
先整体看下日志模块的类图,主要分为五部分,LogChannel、LogWriter、LogContext、logger、logContextCapturer。下面将会对这五个模块分别分析。
模块分析
LogContextCapture
日志上下文捕获器,调用者使用日志功能时,直接接触到的就是该类
每次打印日志时,都实例化一个该类的对象。
类的内部,持有LogContext和Logger两个类的对象。
- LogContext对象存储用户输出的日志信息
- Logger对象最终把LogContext中字符串信息输出。
using LogContextPtr = std::shared_ptr<LogContext>;
class LogContextCapture {
......
private:
LogContextPtr _ctx;
Logger &_logger;
};
该类重载了<<运算符,有以下两个版本:
class LogContextCapture {
....
template<typename T>
LogContextCapture &operator<<(T &&data) ;
LogContextCapture &operator<<(std::ostream &(*f)(std::ostream &));
...
};
(1)第一个版本,模板函数
主要是将用户输入的log信息存储到LogContext对象中。 实现如下:
template<typename T>
LogContextCapture &operator<<(T &&data) {
if (!_ctx) {
return *this;
}
(*_ctx) << std::forward<T>(data);
return *this;
}
- 如果传入的data是c++内置数据类型或者是标准库中实现了ostream重载的类型,可以直接使用
- 如果是用户自定义类,则需要自己在自定义类中重载以下运算符(为什么需要将在下面说明)
friend ostream& operator<<(ostream& out, const userClass& userObj );
(2)第二个版本
参数f是一个函数指针,说明这个重载版本需要的是一个函数,那么什么时候会用到这个版本的重载呢?
/**
* 输入std::endl(回车符)立即输出日志
* @param f std::endl(回车符)
* @return 自身引用
*/
LogContextCapture &operator<<(std::ostream &(*f)(std::ostream &));
- 重载这个版本的目的是为了立即将当前的日志信息进行输出,作者在这里直接复用了STL中的std::endl。在<<std::endl时会执行这个重载函数。如下为std::endl的定义
template<typename _CharT, typename _Traits>
inline basic_ostream<_CharT, _Traits>&
endl(basic_ostream<_CharT, _Traits>& __os)
{ return flush(__os.put(__os.widen('\n'))); }
- 可以看到,endl就是一个函数,且函数原型实际上就是ostream& endl(ostream&);
用户在使用该类输出日志时,首先实例化一个类对象,在构造函数中,实例化了LogContext对象。
当执行logContextCapturer<<str时,会把日志先存储到logContext中,最后在调用logContextCapturer<<endl或者对象析构时,通过Logger的对象将日志内容输出。
LogContextCapture &LogContextCapture::operator<<(ostream &(*f)(ostream &)) {
if (!_ctx) {
return *this;
}
_logger.write(_ctx);
_ctx.reset();
return *this;
}
LogContext
日志上下文,用于存储日志信息,包括日志级别、日志所在文件名、函数名、行号、用户待输出的日志信息。
class LogContext {
...
LogLevel _level;
int _line;
int _repeat = 0;
std::string _file;
std::string _function;
std::string _thread_name;
std::string _module_name;
struct timeval _tv;
const std::string &str();
private:
bool _got_content = false;
std::string _content; // 用户待输出的日志信息。
};
该类继承了ostringstream类,用户待输出的日志信息会先存储在ostringstream中。前边说的用户自定义类型在使用日志功能时,需要重载以下函数的原因就是基于此。ostringstream的基类是ostream。
friend ostream& operator<<(ostream& out, const userClass& userObj );
class LogContext : public std::ostringstream {
}
Logger
该类是一个单例类,用于日志模块的配置管理。
Logger *g_defaultLogger = nullptr;
Logger &getLogger() {
if (!g_defaultLogger) {
g_defaultLogger = &Logger::Instance();
}
return *g_defaultLogger;
}
主要有以下功能:
- 配置日志输出通道。是终端还是文件、或者系统日志中,对应函数add、del、get;
- 设置写日志器。实际上就是控制日志是否要异步输出,对应函数setWriter;
- 设置各类型通道的日志级别。对应函数setLevel;
- 控制是同步还是异步写日志。如果没有设置写日志器,就是同步写,否则就是异步写。实际的写日志操作最终是在各类型日志通道中完成。
成员
_logger_name
private:
std::string _logger_name;
构造函数中初始化
Logger::Logger(const string &loggerName) {
_logger_name = loggerName;
}
const string &Logger::getName() const {
return _logger_name;
}
_channels:日志通道,实现了将日志写到不同的目的地
private:
std::map<std::string, std::shared_ptr<LogChannel> > _channels;
key为通道名字,value为不同的LogChannel
1、怎么管理_channels:
1.1、可以通过如下函数添加、删除、查询一个LogChannel
void Logger::add(const std::shared_ptr<LogChannel> &channel) {
_channels[channel->name()] = channel;
}
void Logger::del(const string &name) {
_channels.erase(name);
}
std::shared_ptr<LogChannel> Logger::get(const string &name) {
auto it = _channels.find(name);
if (it == _channels.end()) {
return nullptr;
}
return it->second;
}
1.2、Logger是单例的,整个程序只有一个。当Logger析构时,所有的LogChannel都会自动关闭
Logger::~Logger() {
_channels.clear();
}
2、怎么用:
2.1、 为所有LogChannel设置通用的日志等级
void Logger::setLevel(LogLevel level) {
for (auto &chn : _channels) {
chn.second->setLevel(level);
}
}
2.2、 写日志到各channel
private:
void writeChannels(const LogContextPtr &ctx);
void writeChannels_l(const LogContextPtr &ctx);
writeChannels_l
将上下文写入到所有目的地
void Logger::writeChannels_l(const LogContextPtr &ctx) {
for (auto &chn : _channels) {
chn.second->write(*this, ctx);
}
}
chn.second->write
是LogChannel的纯虚函数,由具体的通道自己实现,是真正的写日志函数
class LogChannel {
public:
virtual void write(const Logger &logger, const LogContextPtr &ctx) = 0;
writeChannels_l
仅由writeChannels
调用
void Logger::writeChannels(const LogContextPtr &ctx) {
if (ctx->_line == _last_log->_line && ctx->_file == _last_log->_file && ctx->str() == _last_log->str()) {
//重复的日志每隔500ms打印一次,过滤频繁的重复日志
++_last_log->_repeat;
if (timevalDiff(_last_log->_tv, ctx->_tv) > 500) {
ctx->_repeat = _last_log->_repeat;
writeChannels_l(ctx);
}
return;
}
if (_last_log->_repeat) { // 等于0时
writeChannels_l(_last_log);
}
writeChannels_l(ctx);
}
_writer,负责写日志
private:
std::shared_ptr<LogWriter> _writer;
1、怎么管理
void Logger::setWriter(const std::shared_ptr<LogWriter> &writer) {
_writer = writer;
}
Logger::~Logger() {
_writer.reset();
}
2、我们先来看戏
可以看到,如果设置了writer,那么就使用用户提供的writer来写日志。
- 注意:如果这个writer是用户自己实现的,它必须LogWriter
(1)_last_log:为最后写入的上下文
private:
LogContextPtr _last_log;
- 在构造函数中初始化
Logger::Logger(const string &loggerName) {
_last_log = std::make_shared<LogContext>(); // 初始时
}
LogWriter
写日志器类,该类是一个抽象类,定义了一个write纯虚函数。
实际上,该类目前只有一个派生类,即AsyncLogWriter类。
AsyncLogWriter
作用是开启一个线程,先将日志存储在队列中,然后异步的将日志信息输出。
怎么实现
首先,它有一个成员_thread,表示内部开启了一个线程
private:
std::shared_ptr<std::thread> _thread;
它将在构造函数中初始化这个线程并启动这个线程
AsyncLogWriter::AsyncLogWriter() {
_thread = std::make_shared<thread>([this]() { this->run(); });
}
在析构函数中等待线程退出:
AsyncLogWriter::~AsyncLogWriter() {
....
_thread->join();
...
}
上面的 this->run();
如下:
void AsyncLogWriter::run() {
while (......) {
_sem.wait();
flushAll();
}
}
可以看到它主要做了两件事(不断循环):
- 阻塞等待信号到来
- 刷新
那_sem是个什么东西呢?
private:
semaphore _sem;
它是一个信号量,用于线程间通信的。因为AsyncLogWriter是一个异步线程嘛。如果需要写日志时,需要被通知到。我们来看看它会在哪里调用
- 当前AsyncLogWriter析构时,需要先通知AsyncLogWriter去刷新日志
AsyncLogWriter::~AsyncLogWriter() {
...
_sem.post();
_thread->join();
...
}
- 当有日志需要写时,也会发送一个信号
void AsyncLogWriter::write(const LogContextPtr &ctx, Logger &logger) {
.....
_sem.post();
}
那 flushAll()又做了什么呢?在研究这个之前,我们需要先知道AsyncLogWriter类的下面两个成员
private:
......
std::mutex _mutex;
List<std::pair<LogContextPtr, Logger *> > _pending;
_mutex是用来辅助_pending,以线程安全的。而_pending它是一个list,成员是一个个{日志内容、Logger单例日志类}
如下,当有日志需要写时,就将要写的日志和logger压入_pending
void AsyncLogWriter::write(const LogContextPtr &ctx, Logger &logger) {
{
lock_guard<mutex> lock(_mutex);
_pending.emplace_back(std::make_pair(ctx, &logger));
}
....
}
然后在flushAll时从list中取出所有的要写的日志
void AsyncLogWriter::flushAll() {
decltype(_pending) tmp;
{
lock_guard<mutex> lock(_mutex);
tmp.swap(_pending);
}
tmp.for_each([&](std::pair<LogContextPtr, Logger *> &pr) {
pr.second->writeChannels(pr.first);
});
}
而writeChannels又会去调用writeChannels_l,具体参见Logger 类
void Logger::writeChannels(const LogContextPtr &ctx) {
writeChannels_l(ctx);
}
void Logger::writeChannels_l(const LogContextPtr &ctx) {
for (auto &chn : _channels) {
chn.second->write(*this, ctx);
}
}
class LogChannel : public noncopyable {
virtual void write(const Logger &logger, const LogContextPtr &ctx) = 0;
}
/**
* 输出日至到广播
*/
class EventChannel : public LogChannel {
}
/**
* 输出日志至终端,支持输出日志至android logcat
*/
class ConsoleChannel : public LogChannel {
}
LogChannel
日志通道类,该类是一个抽象类,且是所有特定类型通道的基类。。该类包含一个纯虚函数write,以及一个静态函数printTime,两个成员函数name和setLevel,以及一个虚函数format,实际的日志输出最终在format中,如果是终端,还会对不同级别的日志信息分颜色输出。
两个成员变量:
protected:
std::string _name;
LogLevel _level;
- 通道名字
LogChannel::LogChannel(const string &name, LogLevel level) : _name(name), _level(level) {}
const string &LogChannel::name() const { return _name; }
- 日式level
void LogChannel::setLevel(LogLevel level) { _level = level; }
三个重要函数:
1、write:每个继承它的应该实现这个纯虚函数
class LogChannel{
...
virtual void write(const Logger &logger, const LogContextPtr &ctx) = 0;
}
2、默认日期格式
class LogChannel {
public:
static std::string printTime(const timeval &tv);
std::string LogChannel::printTime(const timeval &tv) {
auto tm = getLocalTime(tv.tv_sec);
char buf[128];
snprintf(buf, sizeof(buf), "%d-%02d-%02d %02d:%02d:%02d.%03d",
1900 + tm.tm_year,
1 + tm.tm_mon,
tm.tm_mday,
tm.tm_hour,
tm.tm_min,
tm.tm_sec,
(int) (tv.tv_usec / 1000));
return buf;
}
3、打印日志至输出流
protected:
/**
* 打印日志至输出流
* @param ost 输出流
* @param enable_color 是否启用颜色
* @param enable_detail 是否打印细节(函数名、源码文件名、源码行)
*/
virtual void format(const Logger &logger, std::ostream &ost, const LogContextPtr &ctx, bool enable_color = true, bool enable_detail = true);
void LogChannel::format(const Logger &logger, ostream &ost, const LogContextPtr &ctx, bool enable_color,
bool enable_detail) {
if (!enable_detail && ctx->str().empty()) {
//没有任何信息打印
return;
}
if (enable_color) {
#ifdef _WIN32
SetConsoleColor(LOG_CONST_TABLE[ctx->_level][1]);
#else
ost << LOG_CONST_TABLE[ctx->_level][1];
#endif
}
#ifdef _WIN32
ost << printTime(ctx->_tv) << " " << (char)LOG_CONST_TABLE[ctx->_level][2] << " ";
#else
ost << printTime(ctx->_tv) << " " << LOG_CONST_TABLE[ctx->_level][2] << " ";
#endif
if (enable_detail) {
#if defined(_WIN32)
ost << ctx->_module_name <<"[" << GetCurrentProcessId() << "-" << ctx->_thread_name;
#else
ost << logger.getName() << "[" << getpid() << "-" << ctx->_thread_name;
#endif
ost << "] " << ctx->_file << ":" << ctx->_line << " " << ctx->_function << " | ";
}
ost << ctx->str();
if (enable_color) {
#ifdef _WIN32
SetConsoleColor(CLEAR_COLOR);
#else
ost << CLEAR_COLOR;
#endif
}
if (ctx->_repeat > 1) {
ost << "\r\n Last message repeated " << ctx->_repeat << " times";
}
ost << endl;
}
ConsoleChannel
ConsoleChannel,输出日志到终端,使用std::cout。
void ConsoleChannel::write(const Logger &logger, const LogContextPtr &ctx) {
if (_level > ctx->_level) {
return;
}
//linux/windows日志启用颜色并显示日志详情
format(logger, std::cout, ctx);
}
SysLogChannel
SysLogChannel,输出日志到系统日志。windows、IOS、Android不支持。
void SysLogChannel::write(const Logger &logger, const LogContextPtr &ctx) {
if (_level > ctx->_level) {
return;
}
static int s_syslog_lev[10];
static onceToken s_token([]() {
s_syslog_lev[LTrace] = LOG_DEBUG;
s_syslog_lev[LDebug] = LOG_INFO;
s_syslog_lev[LInfo] = LOG_NOTICE;
s_syslog_lev[LWarn] = LOG_WARNING;
s_syslog_lev[LError] = LOG_ERR;
}, nullptr);
syslog(s_syslog_lev[ctx->_level], "-> %s %d\r\n", ctx->_file.data(), ctx->_line);
syslog(s_syslog_lev[ctx->_level], "## %s %s | %s %s\r\n", printTime(ctx->_tv).data(),
LOG_CONST_TABLE[ctx->_level][2], ctx->_function.data(), ctx->str().data());
}
FileChannelBase
FileChannelBase,输出日志到文件,使用ofstream。因为写日志到文件还涉及文件的保存天数、单文件大小、文件数量等配置项,所以输出日志到文件最终使用的是FileChannel类。
void FileChannelBase::write(const Logger &logger, const std::shared_ptr<LogContext> &ctx) {
if (_level > ctx->_level) {
return;
}
if (!_fstream.is_open()) {
open();
}
//打印至文件,不启用颜色
format(logger, _fstream, ctx, false);
}
_level 设置的日志等级,继承父类的
_fstream是FileChannelBase的成员变量:
protected:
std::ofstream _fstream;
- 打开_fstream,FileChannelBase::write上面的open()就是这么做的,如下:
bool FileChannelBase::open() {
.......
_fstream.close();
......
_fstream.open(_path.data(), ios::out | ios::app);
if (!_fstream.is_open()) {
return false;
}
//打开文件成功
return true;
}