sylar-from-scratch----日志模块

255 阅读13分钟

日志模块概述

主要包含的类有:

class LogLevel---日志级别类
class LogEvent---日志事件类 
class LogFormatter---日志格式器类  
class LogAppender---日志输出器类  
class Logger---日志类 
class LogEventWrap---日志事件包装器类  
class LogManager日志管理类

1、LogLevel类:

枚举级别种类:

参考log44cpp,级别越低表明情况越为严重。

 enum Level { 
    /// 致命情况,系统不可用
    FATAL  = 0,
    /// 高优先级情况,例如数据库系统崩溃
    ALERT  = 100,
    /// 严重错误,例如硬盘错误
    CRIT   = 200,
    /// 错误
    ERROR  = 300,
    /// 警告
    WARN   = 400,
    /// 正常但值得注意
    NOTICE = 500,
    /// 一般信息
    INFO   = 600,
    /// 调试信息
    DEBUG  = 700,
    /// 未设置
    NOTSET = 800,
};

日志级别转字符串(ToString)

使用宏定义与switch-case实现各级别的字符串转化。define宏定义各级别,undef取消宏定义用默认值返回对于switch语句中不匹配的情况。

XX(name)等价于:
case LogLevel::name:
     return #name;  

字符串转日志级别(FromString)

XX(level,v)等价于:
if(str==v){
return LogLevel::level;

2、LogEvent类:

构造函数(LogEvent)

一系列记录日志现场的日志内容,包括日志器名称、日志级别、文件名、行号、累计时间、线程id、协程id、UTC时间、线程名称;通过构造函数的初始化列表实现各属性的初始化。主要的日志内容有:

 logger_name 日志器名称
 level 日志级别
 file 文件名
 line 行号
 elapse 从日志器创建开始到当前的累计运行毫秒
 thead_id 线程id
 fiber_id 协程id
 time UTC时间
 thread_name 线程名称   

不同风格写入日志(printf、vprint)

printf风格:接受可变参数,将其按照fmt格式进行转化。

void LogEvent::printf(const char *fmt, ...){
    va_list ap;
    va_start(ap, fmt);
    vprintf(fmt, ap);
    va_end(ap);
}

vprintf风格:接受参数列表与fmt格式,将其转化为string类型,使用stringstream类型的字符串接收。

void LogEvent::vprintf(const char *fmt, va_list ap) {
char *buf = nullptr;
int len = vasprintf(&buf, fmt, ap);
if(len != -1) {
    m_ss << std::string(buf, len);
    free(buf);
} 

3、LogFormatter类

根据日志模板器解析模板字符串,提取出其中的转义字符与常规字符;判断解析过程是否出错;其难点在于日志模板器的初始化。

初始化(init)

遍历日志格式模板字符串中的字符,判断其为普通字符还是转义字符,将其保存在pair<int,string>数据对patterns中,int为0代表常规字符,为1代表转义字符;由“%d"开头的日期格式单独存放在dataform中。 转义字符以%开头,"%d"开头的{}内的为日期格式,如模板字符串可以表示为:

"%d{%Y-%m-%d %H:%M:%S} [%rms]%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"
参数对应为:
 * - %%m 消息
 * - %%p 日志级别
 * - %%c 日志器名称
 * - %%d 日期时间,后面可跟一对括号指定时间格式,比如%%d{%%Y-%%m-%%d %%H:%%M:%%S},这里的格式字符与C语言strftime一致
 * - %%r 该日志器创建后的累计运行毫秒数
 * - %%f 文件名
 * - %%l 行号
 * - %%t 线程id
 * - %%F 协程id
 * - %%N 线程名称
 * - %%% 百分号
 * - %%T 制表符
 * - %%n 换行

将转义字符和普通字符解析出之后,在格式化日志事件时,将转义字符替换为日志事件的具体内容,普通字符保持不变。其流程为:for循环遍历m_pattern中的字符串--->若为“%”,有可能是转移字符也有可能是常规字符;若不为“%”则可能是常规字符,将其持续读入,否则也可能是日期,“d"为日期格式的开始标志,若再找到“{”开始连续读取字符到dataform中直到“}”,若没有读取到末尾的“}”,直接报错。

日志内容格式化项类(FormatItem)

上述对应参数通过该抽象类重写纯虚函数,调用LogEvent中的获取项函数进行获取并将解析后的格式模板组通过格式化流或字符串流进行输出;如获取日志器名称为:

class LoggerNameFormatItem : public LogFormatter::FormatItem {
public:
   LoggerNameFormatItem(const std::string &str) {}
   void format(std::ostream &os, LogEvent::ptr event) override {
      os << event->getLoggerName();
 }
};

格式化日志事件(format)

对解析后的格式化模板数组逐个遍历分别格式化为日志文本会日志流;

std::string LogFormatter::format(LogEvent::ptr event) {
    std::stringstream ss;
    for(auto &i:m_items) {//遍历解析后的每一个模板项
        i->format(ss, event);//i为FormateItem指针类型,这里的format为FormatItem类中的format函数
    }
    return ss.str();//将日志事件输出为字符串流
}

std::ostream &LogFormatter::format(std::ostream &os, LogEvent::ptr event) {
    for(auto &i:m_items) {//将日志事件格式化流  直接输出  不进行转换
        i->format(os, event);
    }
    return os;
}

4、LogAppender类

日志输出器,用于输出一个日志事件。是一个虚类,可以派生出不同的具体实现,比如输出到终端的StdoutLogAppender,以及输出到文件的FileLogAppender,同时含有设置日志模板器、获取日志模板器接口。

本地输出器(StdoutLogAppender)

根据使用的日志模板器选择输出的模板方式,调用LogFormatter的format函数将其输出为格式化日志流到本地;或者以YAML节点方式的字符串输出到本地。

void StdoutLogAppender::log(LogEvent::ptr event) {
   if(m_formatter) {
      m_formatter->format(std::cout, event);
   } else {
        m_defaultFormatter->format(std::cout, event);
      }
  }

std::string StdoutLogAppender::toYamlString() {
    MutexType::Lock lock(m_mutex);//互斥锁
    YAML::Node node;//定义YAML节点
    node["type"] = "StdoutLogAppender";//设置节点类型
    node["pattern"] = m_formatter->getPattern();//日志格式器
    std::stringstream ss;
    ss << node;
    return ss.str();//输出节点
}

文件输出器(FileLogAppender)

将日志事件以格式化流或YAML格式输出到文件,若一个日志事件距离上次写日志超过3秒,则需要重新打开一次日志。

5、Logger类

日志器,用于输出日志。这个类是直接与用户进行交互的类,提供log方法用于输出日志事件。其实现包含了日志级别,日志器名称,创建时间,以及一个LogAppender数组,可以写日志,可以增加、删除、清空日志输出器;

写日志(log)

日志事件由log方法输出,log方法首先判断日志级别是否达到本Logger的级别要求,是则将日志传给各个LogAppender进行输出,否则抛弃这条日志。

void Logger::log(LogEvent::ptr event) {
   if(event->getLevel() <= m_level) {
      for(auto &i : m_appenders) {
          i->log(event);
      }
  }
}

也可以将日志通过LogAppender以YAML的形式输出;

 std::string Logger::toYamlString() {
   MutexType::Lock lock(m_mutex);
   YAML::Node node;
   node["name"] = m_name;
   node["level"] = LogLevel::ToString(m_level);
   for(auto &i : m_appenders) {
       node["appenders"].push_back(YAML::Load(i->toYamlString()));
   }
   std::stringstream ss;
   ss << node;
   return ss.str();
 }

6、LogEventWrap类

日志事件包装类,在日志现场构造,包装了日志器和日志事件两个对象,在日志记录结束后,LogEventWrap析构时,调用日志器的log方法输出日志事件。

LogEventWrap::LogEventWrap(Logger::ptr logger, LogEvent::ptr   event): m_logger(logger), m_event(event) {}
LogEventWrap::~LogEventWrap() {
   m_logger->log(m_event);
}

7、LoggerManager类

日志器管理类,单例模式,用于统一管理全部的日志器,提供getLogger()方法用于创建/获取日志器。内部维护一个名称到日志器的map,当获取的日志器存在时,直接返回对应的日志器指针,否则创建对应的日志器并返回。

Logger::ptr LoggerManager::getLogger(const std::string &name) {
   MutexType::Lock lock(m_mutex);
   auto it = m_loggers.find(name);
   if(it != m_loggers.end()) {
      return it->second;
   }

   Logger::ptr logger(new Logger(name));
   m_loggers[name] = logger;
   return logger;
}

宏定义

  • 获取root日志器及指定名称的日志器
#define SYLAR_LOG_ROOT() sylar::LoggerMgr::GetInstance()->getRoot()
#define SYLAR_LOG_NAME(name) sylar::LoggerMgr::GetInstance()->getLogger(name)
  • 使用流式方式将日志级别level的日志写入到日志器中
    构造一个LogEventWrap对象,打包日志器和日志事件,在对象析构时调用日志器写日志事件。
#define SYLAR_LOG_LEVEL(logger , level) \
    if(level <= logger->getLevel()) \
        sylar::LogEventWrap(logger, sylar::LogEvent::ptr(new sylar::LogEvent(logger->getName(), \
            level, __FILE__, __LINE__, sylar::GetElapsedMS() - logger->getCreateTime(), \
            sylar::GetThreadId(), sylar::GetFiberId(), time(0), sylar::GetThreadName()))).getLogEvent()->getSS()
#define SYLAR_LOG_FATAL(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::FATAL)
#define SYLAR_LOG_ALERT(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::ALERT)
#define SYLAR_LOG_CRIT(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::CRIT)
#define SYLAR_LOG_ERROR(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::ERROR)
#define SYLAR_LOG_WARN(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::WARN)
#define SYLAR_LOG_NOTICE(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::NOTICE)
#define SYLAR_LOG_INFO(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::INFO)
#define SYLAR_LOG_DEBUG(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::DEBUG)
  • 使用c print的方式将日志级别为level的日志写入到logger中
#define SYLAR_LOG_FMT_LEVEL(logger, level, fmt, ...) \
    if(level <= logger->getLevel()) \
        sylar::LogEventWrap(logger, sylar::LogEvent::ptr(new sylar::LogEvent(logger->getName(), \
            level, __FILE__, __LINE__, sylar::GetElapsedMS() - logger->getCreateTime(), \
            sylar::GetThreadId(), sylar::GetFiberId(), time(0), sylar::GetThreadName()))).getLogEvent()->printf(fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_FATAL(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::FATAL, fmt, __VA_ARGS__)

#define SYLAR_LOG_FMT_ALERT(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::ALERT, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_CRIT(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::CRIT, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_ERROR(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::ERROR, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_WARN(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::WARN, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_NOTICE(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::NOTICE, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_INFO(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::INFO, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_DEBUG(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::DEBUG, fmt, __VA_ARGS__)

日志模块工作流程

日志管理类单例
typedef sylar::Singleton<LoggerManager> LoggerMgr;
//定义一个日志器(这里使用的是root)
static sylar::Logger::ptr g_logger = SYLAR_LOG_ROOT();

int main(int argc, char** argv) {
    // 使用流式风格写日志
    SYLAR_LOG_INFO(g_logger) << "hello logger stream";
    // 使用格式化写日志
    SYLAR_LOG_FMT_INFO(g_logger, "%s", "hello logger format");
    return 0;
}

1、获得日志器logger:通过日志管理器获得日志管理器,使用宏定义获取root日志器,此时由于是新定义的日志器,管理器会使用getLogger函数创建一个新的日志器,新创建的logger不带logappender,其它为默认参数,其名称默认为root。
2、初始化日志格式器和输出器:在初始化logformatter时会解析对应的formatitem存放在m_items中,将默认的StdoutLogAppender加到m_appenders中。
3、打印日志:使用宏定义SYLAR_LOG_INFO()创建相应级别的logevent,logevent与logeventwrap打包起来的,在析构时会调用log函数写日志。如日志器为m_logger->log(m_event);log函数会根据日志事件的级别选择是否遍历日志器中的输出器来调用对应输出器的log函数做最后的打印工作。因此打印工作的完成是由logappender中的log函数完成的。

void Logger::log(LogEvent::ptr event) {
    if(event->getLevel() <= m_level) {
        for(auto &i : m_appenders) {
            i->log(event);
        }
    }
} 

基础知识

1. C++11的智能指针类型与关键字override

std::unique_ptr:独占资源所有权的指针

独占资源的所有权,离开unique_ptr对象的作用域时,会自动释放资源

  • 使用裸指针时要使用delete释放内存
  • unique_ptr自动管理内存,如: std::unique_ptr<int> uptr=std::make_unique<int>(200);
  • unique_ptr是move-only的,不能直接将已有的指针赋给新的指针,如: std::unique_ptr<int> uptr2=uptr;//编译错误 std::unique_ptr<int> uptr2 = std::move(uptr); assert(uptr == nullptr);
  • unique_ptr可以指向一个数组
{
    std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);
    for (int i = 0; i < 10; i++) {
        uptr[i] = i * i;
    }   
    for (int i = 0; i < 10; i++) {
        std::cout << uptr[i] << std::endl;
    }   
}
  • 自定义deleter
{
    struct FileCloser {
        void operator()(FILE* fp) const {
            if (fp != nullptr) {
                fclose(fp);
            }
        }   
    };  
    std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));
}
  • 使用lamba的deleter
{
    std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(
        fopen("test_file.txt", "w"), [](FILE* fp) {
            fclose(fp);
        });
}

std::shared_ptr共享资源所有权指针

std::shared_ptr 其实就是对资源做引用计数——当引用计数为 0 的时候,自动释放资源。头文件为#include; std::shared_ptr采用引用计数,每个shared_ptr的拷贝指向相同的内容,当最后一个指针析构时,该指针所指向内存才会被释放。

{
    std::shared_ptr<int> sptr = std::make_shared<int>(200);
    assert(sptr.use_count() == 1);  // 此时引用计数为 1
    {   
        std::shared_ptr<int> sptr1 = sptr;
        assert(sptr.get() == sptr1.get());
        assert(sptr.use_count() == 2);   // sptr 和 sptr1 共享资源,引用计数为 2
    }   
    assert(sptr.use_count() == 1);   // sptr1 已经释放
}
// use_count 为 0 时自动释放内存
  • shared_ptr 也可以指向数组和自定义 deleter
{
    // C++20 才支持 std::make_shared<int[]>
    // std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);
    std::shared_ptr<int[]> sptr(new int[10]);
    for (int i = 0; i < 10; i++) {
        sptr[i] = i * i;
    }   
    for (int i = 0; i < 10; i++) {
        std::cout << sptr[i] << std::endl;
    }   
}

{
    std::shared_ptr<FILE> sptr(
        fopen("test_file.txt", "w"), [](FILE* fp) {
            std::cout << "close " << fp << std::endl;
            fclose(fp);
        });
}
  • std::shared_ptr的实现原理
    一个 shared_ptr 对象的内存开销要比裸指针和无自定义 deleter 的 unique_ptr 对象略大。
    无自定义 deleter 的 unique_ptr 只需要将裸指针用 RAII 的手法封装好就行,无需保存其它信息,所以它的开销和裸指针是一样的。如果有自定义 deleter,还需要保存 deleter 的信息。
    shared_ptr 需要维护的信息有两部分:
    指向共享资源的指针。
    引用计数等共享资源的控制信息——实现上是维护一个指向控制信息的指针。

  • std::shared_ptr 支持 aliasing constructor

std::weak_ptr

共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。

override

C++的多态行为的基础为基类声明虚函数,派生类实现该函数的类外实现与覆盖,对此常出的问题有:
无意的重写:在派生类中声明了一个与基类中具有相同签名的成员函数,不小心重写了虚函数;
虚函数签名不匹配:虚函数签名不匹配即因为函数名、参数列表 或 const 属性不同而创建了一个新函数,而不是重写了虚函数。
针对此问题C++11新增关键字override和final:
override:保证在派生类中声明的重载函数,与基类的虚函数有相同的签名;若不同编译器报错,即显示地告诉编译器,该函数为虚函数的类外实现;
final:阻止类的进一步派生和虚函数的进一步重写。

2. 自旋锁typedef Spinlock MutexType

spinlock与mutex
spinlock为自旋锁,是实现保护共享资源提供的一种锁机制,自旋锁与互斥锁比较类似,都是为了解决某项资源的互斥使用的。二者在调度机制上存在区别,对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名
spin lock 和mutex - 知乎 (zhihu.com)

3. 数据类型stringstream、ostream

  • stringstream
    C++专门处理字符串的输入输出流类,将数据从一个对象到另一个对象的流动抽象为“流”,使用前被创建,使用后被删除,数据的输入输出使用I/O流实现,cin、cout是C++预定义的流类对象,有头文件:
    iostream:从流读取数据、向流写入数据、读写流
    fstream:从文件中读取数据、向文件写入数据、读写文件
    sstream:从string中读取数据、向string写入数据、读写string
    stringstream位于头文件下 构造函数、输出字符串、清空和修改内容、用途 【C++】stringstream类 最全超详细解析(什么是stringstream? stringstrem有哪些作用? 如何在算法中应用?)-CSDN博客
  • ostream为输出流类型,常见的就是cout("<<")

4. C printf风格与C vprintf风格写入日志的区别

printf为变长参数,就是常用的输出函数;vprintf只有当自定义一个输出函数时才会用到,其参数为一个单独的val_list。 image.png

int error(char *fmt, ...)
{
    int result;
    va_list args;
    va_start(args, fmt);
    fputs("Error: ", stderr);
    result = vfprintf(stderr, fmt, args);
    va_end(args);
    return result;
}