ZLToolKit源码阅读:日志功能分析

1059 篇文章 277 订阅

使用

日志级别

在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;
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值