sylar 源码解析之日志模块

本贴最后更新于 232 天前,其中的信息可能已经渤澥桑田

一、日志模块整体设计

设想一下,凭借我们的经验,一般日志会有哪些内容呢?等级,具体定位信息(函数名、行号、文件名,线程名),时间。

OK,这确实就是日志输出的内容,但输出的内容仅呈现在前台,那么回到后台看,日志应该具备重定向和灵活配置功能,因此日志模块的设计应具备以下功能:

  • 区分不同的日志级别,如 DEBUG/INFO/WARN/ERROR 等。 日志等级及控制输出
  • 可以指定输出级别,灵活控制日志输出。
  • 支持输出到不同的目标,如标准输出、文件、syslog、网络日志服务器等。
  • 可以对日志进行分类和命名,方便判断来源。
  • 日志格式可灵活配置,可以选择是否包含文件名/行号、时间戳、线程/协程号、日志级别等信息。
  • 支持通过配置文件进行配置。

那么我们看下 sylar 关于日志处理是怎么设计的。

1.1、总体日志框图

  • 整体的类依赖图

image

上图可以看到,以 Logger 类为中心,与之相关联的类分别有 LogAppender、LogEvent、LogFormatter...等,将类单独拎出来看注释如下:

  • 详细的类定义 ^(使用了大量的智能指针)^

  • LogFormatter:日志格式器,可用于格式化日志事件,通过 format 方法用于将日志事件格式化成字符串;
  • LogAppender:日志输出目标,用于将日志事件输出到对应的输出地,该类包含 LogFormatter 格式化方法和 log 方法,可通过继承该类实现到不同的输出地输出;
  • Logger:日志器,负责日志输出,提供 log 方法,若该日志需要输出则调用 LogAppender 将日志输出,否则抛弃;
  • LogEvent:日志时间,用于记录日志信息,比如该日志的级别,文件名 / 行号,日志消息,线程 / 协程号,所属日志器名称等。;
  • LogEventWrap:日志事件包装器,将日志事件与日志器包装在一起,通过宏定义简化日志模块的使用,当期析构时候,则调用日志器的 log 方法进行输出;
  • LogManager:日志器管理类,单例模式,用于统一管理所有的日志器,提供日志器的创建与获取方法,内部维护一个名称到日志器的 map,当获取的日志器存在时,直接返回对应的日志器指针,否则创建对应的日志器并返回。

image

class Logger : public std::enable_shared_from_this<Logger> {};
// 日志级别
class LogLevel {};
// 日志格式化器
class LogFormatter {};
// 日志输出目标
class LogAppender {};
// 输出到控制台的日志输出目标
class StdoutLogAppender : public LogAppender {};
// 输出到文件的日志输出目标
class FileLogAppender : public LogAppender {};
// 日志事件
class LogEvent {};
// 日志事件包装器
class LogEventWrap {};
// 日志器管理类
class LogManager {};

二、代码流程

2.1、从怎么用去反推着看

作者在 ReadMe 中指引可以看到:

支持流式日志风格写日志和格式化风格写日志,支持日志格式自定义,日志级别,多日志分离等等功能

流式日志使用:SYLAR_LOG_INFO(g_logger) << "this is a log";

格式化日志使用:SYLAR_LOG_FMT_INFO(g_logger, "%s", "this is a log");

支持时间,线程 id,线程名称,日志级别,日志名称,文件名,行号等内容的自由配置

从两个宏可以看出通过日志事件包装器结合日志事件,将日志按约定格式打包进数据流,最终通过 getSS()​获取字符串流。那么对 LogEvent 和 LogEventWrap 进行拆解。

/**
 * @brief 使用流式方式将日志级别level的日志写入到logger
 */
#define SYLAR_LOG_LEVEL(logger, level) \
    if(logger->getLevel() <= level) \
        sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \
                        __FILE__, __LINE__, 0, sylar::GetThreadId(),\
                sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getSS()

/**
 * @brief 使用格式化方式将日志级别level的日志写入到logger
 */
#define SYLAR_LOG_FMT_LEVEL(logger, level, fmt, ...) \
    if(logger->getLevel() <= level) \
        sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \
                        __FILE__, __LINE__, 0, sylar::GetThreadId(),\
                sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getEvent()->format(fmt, __VA_ARGS__)

2.2、LogEvent 和 LogEventWrap

2.2.1、LogEvent

UML 显示如下:

@startuml class LogEvent { - m_file: const char* - m_line: int32_t - m_elapse: uint32_t - m_threadId: uint32_t - m_fiberId: uint32_t - m_time: uint64_t - m_threadName: std::string - m_ss: std::stringstream - m_logger: std::shared_ptr<Logger> - m_level: LogLevel::Level + LogEvent(logger: std::shared_ptr<Logger>, level: LogLevel::Level, file: const char*, line: int32_t, elapse: uint32_t, thread_id: uint32_t, fiber_id: uint32_t, time: uint64_t, thread_name: const std::string&) + getFile(): const char* + getLine(): int32_t + getElapse(): uint32_t + getThreadId(): uint32_t + getFiberId(): uint32_t + getTime(): uint64_t + getThreadName(): const std::string& + getContent(): std::string + getLogger(): std::shared_ptr<Logger> + getLevel(): LogLevel::Level } @enduml

该类主要是用于记录日志中的一些描述信息,如线程 ID、行列号、文件名、日志等级等,其中具体把 content 进行格式化的函数是 format

2.2.2、LogEventWrap 日志附加信息装饰器

Wrap 主要的作用是通过构造将 m_event 进行转换(获取)并通过 getSS 来获取日志内容流

@startuml class LogEvent { // LogEvent 类的成员变量和方法 } class LogEventWrap { - m_event: LogEvent::ptr + LogEventWrap(e: LogEvent::ptr) + ~LogEventWrap() + getEvent(): LogEvent::ptr + getSS(): std::stringstream& } LogEventWrap --> LogEvent: m_event @enduml

2.2.3、LogLevel 等级有哪些?

@startuml class LogLevel { - UNKNOW: int - DEBUG: int - INFO: int - WARN: int - ERROR: int - FATAL: int + ToString(level: int): const char* + FromString(str: string): int } @enduml

其中 ToString 还使用了些技巧。把 Switch Case 干掉1

那么从以上三个类结合调用接口,仅仅可以看出是怎么获取日志内容流的,但似乎并没有看到是怎么组成内容流的?这里又有一个技巧,在 LogEventWrap 析构时,调用 Logger 中的 log 接口,把日志内容流塞回 Logger。

LogEventWrap::~LogEventWrap() {
    m_event->getLogger()->log(m_event->getLevel(), m_event);
}

2.2.4、Logger

LogEvent、LogLevel、LogAppender、LogFormatter 和 Logger 之间的 UML 类图关系如下:

@startuml class LogEvent { + getMessage(): std::string + getLevel(): LogLevel::Level + getLoggerName(): std::string + getThreadID(): uint64_t + getTimestamp(): uint64_t + getFile(): std::string + getLine(): uint32_t + getFunction(): std::string } enum LogLevel { DEBUG INFO WARN ERROR FATAL } interface LogAppender { + log(event: LogEvent::ptr) + setFormatter(formatter: LogFormatter::ptr) + getFormatter(): LogFormatter::ptr } class LogFormatter { + format(event: LogEvent::ptr): std::string } class Logger { - m_name: std::string - m_level: LogLevel::Level - m_mutex: MutexType - m_appenders: std::list<LogAppender::ptr> - m_formatter: LogFormatter::ptr - m_root: Logger::ptr + Logger(name: const std::string& = "root") + log(level: LogLevel::Level, event: LogEvent::ptr) + debug(event: LogEvent::ptr) + info(event: LogEvent::ptr) + warn(event: LogEvent::ptr) + error(event: LogEvent::ptr) + fatal(event: LogEvent::ptr) + addAppender(appender: LogAppender::ptr) + delAppender(appender: LogAppender::ptr) + clearAppenders() + getLevel(): LogLevel::Level + setLevel(val: LogLevel::Level) + getName(): const std::string& + setFormatter(val: LogFormatter::ptr) + setFormatter(val: const std::string&) + getFormatter(): LogFormatter::ptr + toYamlString(): std::string } LogEvent "1" *-- "1" LogLevel Logger "1" *-- "0..*" LogAppender Logger "1" *-- "1" LogFormatter @enduml

从应用输出日志看,输出出口就定在了 Logger::log​这个函数中:

void Logger::log(LogLevel::Level level, LogEvent::ptr event) {
    if (level >= m_level) {
        auto self = shared_from_this();
        MutexType::Lock lock(m_mutex);
        if (!m_appenders.empty()) {
            for (auto& i : m_appenders) {
                i->log(self, level, event);
            }
        } else if (m_root) {
            m_root->log(level, event);
        }
    }
}

从上面代码中 appenders 可以看出,日志流的输出可以时多样的。

2.2.5、LogAppender

@startuml class Logger { - level: LogLevel::Level - appenders: std::vector<LogAppender::ptr> + log(level: LogLevel::Level, event: LogEvent::ptr): void + addAppender(appender: LogAppender::ptr): void + removeAppender(appender: LogAppender::ptr): void } class LogAppender { - level: LogLevel::Level - formatter: LogFormatter::ptr + log(logger: Logger, level: LogLevel::Level, event: LogEvent::ptr): void + setFormatter(formatter: LogFormatter::ptr): void + getFormatter(): LogFormatter::ptr + getLevel(): LogLevel::Level + setLevel(level: LogLevel::Level): void } class LogFormatter { - pattern: std::string + format(event: LogEvent::ptr): std::string } enum LogLevel::Level { TRACE DEBUG INFO WARN ERROR FATAL } Logger "1" --> "*" LogAppender LogAppender "1" --> "1" LogFormatter Logger --> LogLevel::Level @enduml

三、模块应用实例

支持流式日志风格写日志和格式化风格写日志,支持日志格式自定义,日志级别,多日志分离等等功能

流式日志使用:SYLAR_LOG_INFO(g_logger) << "this is a log";

格式化日志使用:SYLAR_LOG_FMT_INFO(g_logger, "%s", "this is a log");

支持时间,线程 id,线程名称,日志级别,日志名称,文件名,行号等内容的自由配置

四、移植使用


  1. 把 Switch Case 干掉

    宏定义的方法

    const char* LogLevel::ToString(LogLevel::Level level) {
        switch(level) {
    #define XX(name) \
        case LogLevel::name: \
            return #name; \
            break;
        XX(DEBUG);
        XX(INFO);
        XX(WARN);
        XX(ERROR);
        XX(FATAL);
    #undef XX
        default:
            return "UNKNOW";
        }
        return "UNKNOW";
    }
    

3 操作
SoaringGuy 在 2023-11-04 16:16:01 更新了该帖
SoaringGuy 在 2023-11-04 16:10:07 更新了该帖
SoaringGuy 在 2023-11-04 14:48:01 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...