当前位置 博文首页 > wanggao的专栏:muduo学习笔记:base部分之高性能日志库-Logger

    wanggao的专栏:muduo学习笔记:base部分之高性能日志库-Logger

    作者:[db:作者] 时间:2021-09-09 09:49

    前面介绍了【muduo学习笔记:base部分之高性能日志库-LogStream】前端中的LogStream类,本文接受日志前端的管理对象Logger,代码位于 muduo/base/Logging.{h, cc}。

    整体功能如下图:Logger负责全局的日志级别、输出目的地设置(静态成员函数),实际的数据流处理由Impl内部类实现。Imp的成员变量LogSteam对象是实际的缓存处理对象,包含了日志信息的加工,通过Logger的stream()函数取得实现各种日志宏功能。当Logger对象析构时,将LogStream的日志数据flush到输出目的地,默认是stdout。
    在这里插入图片描述
    Logging部分实现由多个部分,先给头文件主体代码(略作修改),再依次介绍不同部分。

    class TimeZone;
    
    class Logger
    {
     public:
      enum LogLevel{ TRACE, DEBUG, INFO, WARN, ERROR, FATAL, NUM_LOG_LEVELS };
    
      // 编译器计算源文件名
      class SourceFile
      {
       public:
        template<int N>
        SourceFile(const char (&arr)[N]) : data_(arr), size_(N-1){
          const char* slash = strrchr(data_, '/'); // builtin function
          if (slash){
            data_ = slash + 1;
            size_ -= static_cast<int>(data_ - arr);
          }
        }
    
        explicit SourceFile(const char* filename) : data_(filename)
        {
          const char* slash = strrchr(filename, '/');
          if (slash){
            data_ = slash + 1;
          }
          size_ = static_cast<int>(strlen(data_));
        }
    
        const char* data_;
        int size_;
      };
    
      // 构造函数,实际是用于初始化 Impl 类
      Logger(SourceFile file, int line) 
      	: impl_(INFO, 0, file, line) {}
      Logger(SourceFile file, int line, LogLevel level) 
      	: impl_(level, 0, file, line) {  impl_.stream_ << func << ' '; }
      Logger(SourceFile file, int line, LogLevel level, const char* func) 
      	: impl_(level, 0, file, line) {}
      Logger(SourceFile file, int line, bool toAbort) 
      	: impl_(toAbort?FATAL:ERROR, errno, file, line) {}
      	
      // 析构函数, flush输出
      ~Logger();
    
      // 用于日志宏
      LogStream& stream() { return impl_.stream_; }
    
      // 全局方法,设置日志级别、flush输出目的地、日志时区等
      static LogLevel logLevel();
      static void setLogLevel(LogLevel level);
    
      typedef void (*OutputFunc)(const char* msg, int len);
      typedef void (*FlushFunc)();
      static void setOutput(OutputFunc);   // 默认 fwrite 到 stdout
      static void setFlush(FlushFunc);     // 默认 fflush 到 stdout
      static void setTimeZone(const TimeZone& tz); // 默认 GMT
    
     private:
    
    //  私有类,实际的日志消息缓冲处理
    class Impl
    {
     public:
      typedef Logger::LogLevel LogLevel;
      Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
      void formatTime();   // 格式化时间
      void finish();
    
      Timestamp time_;		// 日志时间戳
      LogStream stream_;	// 日志缓存流
      LogLevel level_;      // 日志级别
      int line_;            // 当前记录日式宏的 源代码行数
      SourceFile basename_; // 当前记录日式宏的 源代码名称
    };
    
      Impl impl_;
    };
    
    // 全局的日志级别,静态成员函数定义,静态成员函数实现
    extern Logger::LogLevel g_logLevel;
    inline Logger::LogLevel Logger::logLevel(){
      return g_logLevel;
    }
    

    1、Logger类

    Logger类主要有三个部分:

    (1)静态成员函数:

    全局方法,设置日志级别、flush输出目的地、日志时区等。

    (2)构造函数

    Logger构造函数传递参数,主要用于构造Impl类。

    第一个参数类型是SourceFile,传递一个字符串数组、或者一个字符串的文件路径,使用函数strrchr查找分隔符获取文件的basename;在Release模式下编译,将在编译期调用__builtin_strrchr计算分隔符位置而得到文件名,避免了运行时的计算,提高效率。

    (3)成员变量 Impl

    内部类,实际用于日志信息的处理类。在Logger构造函数的初始化列表中进行初始化。

    理论上,使用Impl的设计手法,应该使用指针或指针隐藏实现,如

    //************************** 头文件
    class Logger
    {
    public:
      ...
    
    private:
      class Impl;
      std::unique_ptr<Impl> impl_;
    };
    
    //************************** 源文件
    class Logger::Impl
    {
     public:
      typedef Logger::LogLevel LogLevel;
      Impl(LogLevel level, int old_errno, const Logger::SourceFile& file, int line);
      void formatTime();
      void finish();
    
      Timestamp time_;
      LogStream stream_;
      LogLevel level_;
      int line_;
      Logger::SourceFile basename_;
    };
    
    Logger::Logger(SourceFile file, int line)
      : //impl_(INFO, 0, file, line)
      impl_(new Impl(INFO, 0, file, line))
    {
    }
    
    ...
    

    目前muduo是开源库,Impl设计没有多大意思。另外,使用指针管理,在堆中分配,相比在栈中分配相比些微效率上的影响。直接只用类对象方式,是为了模块化编程,使得编程结构层次清晰。

    (4)成员函数 LogStream& LogStream::stream()

    返回Impl内部的LogStream成员变量引用。用于日志宏。

    2、日式宏

    根据日志级别,定义了一堆日志宏。

    // 日志宏
    #define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
      muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
    #define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
      muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
    #define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
      muduo::Logger(__FILE__, __LINE__).stream()
    #define LOG_WARN muduo::Logger(__FILE__, __LINE__, muduo::Logger::WARN).stream()
    #define LOG_ERROR muduo::Logger(__FILE__, __LINE__, muduo::Logger::ERROR).stream()
    #define LOG_FATAL muduo::Logger(__FILE__, __LINE__, muduo::Logger::FATAL).stream()
    #define LOG_SYSERR muduo::Logger(__FILE__, __LINE__, false).stream()
    #define LOG_SYSFATAL muduo::Logger(__FILE__, __LINE__, true).stream()
    

    3、实现细节

    Logger类和LogStream类如何配合?

    使用LOG_*之类的宏会创建一个临时匿名Logger对象,这个对象有一个Impl对象,而Impl对象有一个LogStream对象。LOG_*宏会返回一个LogStream对象的引用。用于将内容输入到LogStream中的一个buffer中。

    在Logger的析构函数中,调用 g_output,即 g_output(buf.data(), buf.length()),将存于LogStream的buffer的日志内容输出。如果是FATAL错误,还要调用g_flush,最后abort()程序。如果没有调用g_flush,会一直输出到缓冲区(标准输出缓冲区,文件FILE缓冲区)满才会真的输出在标准输出,或者写入到文件中去。

    3.1、Impl细节

    一次日志写入,Impl主要干三件事:

    (1)构造函数初始化各成员变量,往LogStream中写入格式化时间字符串、线程id,日志级别
    (2)往LogStream中写入日志消息(实际是Logger外部处理的)
    (3)往LogStream中写入记录日志所在源文件名、行数(Logger析构调用)

    着重说明格式化时间字符串的处理。有两个线程前后两次日志操作,都是在同一秒钟内,仅格式化微秒部分。为了实现多线程中日志时间格式化的效率,增加了两个__thread变量,用于缓存当前线程存日期时间字符串、上一次日志记录的秒数。线程id使用了同样的方式缓存对应的字符串。

    __thread char t_errnobuf[512];
    __thread char t_time[64];		// 当前线程的时间字符串 “年:月:日 时:分:秒”
    __thread time_t t_lastSecond;	// 当前线程上一次日志记录时的秒数
    
    //Impl类的构造函数
    //级别,错误(没有错误则传0),文件,行
    //Impl类主要是负责日志的格式化
    Logger::Impl::Impl(