【C++ Linux编程进阶 从0实现muduo库系列】第五讲:实现C++日志库

发布于:2025-03-27 ⋅ 阅读:(30) ⋅ 点赞:(0)

重点内容

视频讲解:《C++Linux编程进阶:从0实现muduo C++网络框架系列》-第5讲.实现C++日志库

代码改动

lesson5代码

  • 实现:base/LogStream.h/cc

  • 实现:base/Logging.h/cc

  • examples/test_basic_log.cc

  • examples/test_logging.cc

特别要注意CMakeLists.txt的 宏定义改动,debug模式时,lesson4声明的DEBUG宏定义和日志库的Debug日志级别符号有冲突,如果不修改会产生意想不到的编译报错。

1. 日志系统整体架构

1.1 日志系统整体架构

LOG_DEBUG << "debug log test";

LOG_INFO << "info log test";

1.2 模块具体作用

1.2.1 日志宏 (LOG_XXX)

比如LOG_DEBUG, LOG_INFO等

提供简洁易用的接口,自动捕获文件名和行号,根据当前日志级别决定是否记录日志,创建临时Logger对象并返回流式接口

使用范例:

LOG_DEBUG << "debug log test";
LOG_INFO << "int: " << 42;

1.2.2 Logger 类

日志系统核心类,管理日志生命周期,提供静态方法控制全局日志行为(级别、输出方式),根据日志级别和上下文创建日志实例,析构时完成日志输出。

LOG_INFO << "info log test"; 实际调用

if (mymuduo::Logger::logLevel() <= mymuduo::Logger::INFO) \
  mymuduo::Logger(__FILE__, __LINE__).stream() << "info log test";

1.2.3 Logger::Impl 类

日志格式化分为两大模块:

Logger的内部实现类,封装日志格式化逻辑,管理时间戳和源文件信息,处理日志的具体格式,添加行号、文件名等上下文信息。

比如日志格式:20250324 09:08:15.248185 WARN warn 输出 - test_basic_log.cc:26

  • 20250324 09:08:15.248185 时间戳

  • WARN 日志级别打印

  • test_basic_log.cc 文件名

  • :26 行号

1.2.4 LogStream 类

提供流式接口(<<操作符),管理内部缓冲区,实现各种数据类型的格式化转换,特别优化了数值转换效率,支持链式调用。

包括Logger::impl类里的时间戳 日志级别 文件名 行号的缓存,

这里我们重点讲流式接口 <<操作符,范例如下所示:

mymuduo::LogStream logstream;
logstream << "LogStream 输出" << 78 << "abc";
std::cout << "cout: " << logstream.buffer().toString() << std::endl;

输出:

cout: LogStream 输出78abc

1.2.5 FixedBuffer 类

预分配固定大小的内存缓冲区,避免动态内存分配,提供高效的追加、重置等基本操作,作为LogStream的内部存储。

1.2.6 输出函数 (OutputFunc/FlushFunc)

日志最终写入的目的地,函数指针类型(OutputFunc和FlushFunc),允许自定义日志输出目标和刷新方式,可以是控制台、文件、网络套接字或自定义设备,由OutputFunc和FlushFunc控制,支持灵活配置。

1.2.7 日志级别 (LogLevel)

定义日志的严重程度(TRACE、DEBUG、INFO、WARN、ERROR、FATAL),控制日志过滤,配合宏实现条件记录,支持运行时调整。

1.2.8 Fmt 格式化类

结合C风格格式化字符串的灵活性和C++类型安全性,预格式化数据到内部缓冲区,通过<<操作符集成到LogStream中。

  // 测试整数格式化
    Fmt intFmt("%d", 42);
    std::cout << "整数格式化: " << intFmt.data() << std::endl;
    
    // 测试浮点数格式化
    Fmt floatFmt("%.2f", 3.14159);
    std::cout << "浮点数格式化: " << floatFmt.data() << std::endl;

打印输出:

整数格式化: 42 浮点数格式化: 3.14

1.2.9 SourceFile 类

高效处理源文件路径,从完整路径中提取文件名,避免运行时重复计算,优化日志性能,存储在Logger::Impl中提供位置信息。

// 测试不同路径形式
std::cout << "原始的文件名获取:" << __FILE__ << std::endl;
const char* paths[] = {
    "/home/user/project/file.cpp",
    "src/file.cpp",
    "file.cpp"
};

for (const char* path : paths)
{
    Logger::SourceFile sf(path);
    std::cout << "原始路径: " << path << "\n";
    std::cout << "提取文件名: " << sf.data_ << "\n";
    std::cout << "文件名长度: " << sf.size_ << "\n\n";
}

打印输出

原始的文件名获取:/home/lqf/long/spark_muduo/lesson5/examples/test_basic_log.cc 原始路径: /home/user/project/file.cpp 提取文件名: file.cpp 文件名长度: 8 原始路径: src/file.cpp 提取文件名: file.cpp 文件名长度: 8 原始路径: file.cpp 提取文件名: file.cpp 文件名长度: 8

2 格式化日志输出LogStream类

2.1 核心设计理念

LogStream 的核心设计理念是提供一个高效、类型安全、易用的日志流式接口,主要体现在:

1.流式语法设计:

通过重载 << 操作符,实现类似 std::cout 的链式调用语法

并重载不同的数据类型

  • self& operator<<(short);

  • self& operator<<(unsigned short);

  • self& operator<<(const char* str)

  • self& operator<<(const std::string& v)

每个 << 操作符返回自身引用(self&),支持链式表达式,比如LOG_INFO << "int: " << 42;

2.高效内存管理(设计FixedBuffer类):

  • 使用预分配的固定大小缓冲区而非动态内存分配

  • 避免了频繁的内存分配/释放操作,减少内存碎片

2.2 设计框架图

2.3 核心实现分析

2.3.1 内存管理策略

LogStream 通过 FixedBuffer 模板类管理内存,采用两种预定义大小:

// 在detail命名空间中定义两种常用缓冲区大小
const int kSmallBuffer = 4000;         // 4KB,用于一般日志消息
const int kLargeBuffer = 4000*1000;    // 4MB,用于特别长的日志消息

// 定义LogStream使用的缓冲区类型
typedef detail::FixedBuffer<detail::kSmallBuffer> Buffer;

FixedBuffer 的核心在于预分配内存并使用指针跟踪当前位置:

template<int SIZE>
class FixedBuffer : noncopyable {
private:
    char data_[SIZE];  // 固定大小的字符数组
    char* cur_;        // 当前写入位置
    
public:
    FixedBuffer() : cur_(data_) {}
    
    void append(const char* buf, size_t len) {
        if (avail() > static_cast<int>(len)) {
            memcpy(cur_, buf, len);
            cur_ += len;
        }
    }
    
    // 其他辅助方法...
};

这种设计避免了动态内存分配,特别适合短生命周期、高频调用的日志场景。

2.3.2 优化的数值转换算法

LogStream 对数值转换做了专门优化,使用自定义的转换算法替代标准库函数:

// Wilson的高效整数转字符串算法
template<typename T>
size_t convert(char buf[], T value) {
    T i = value;
    char* p = buf;

    do {
        int lsd = static_cast<int>(i % 10);
        i /= 10;
        *p++ = zero[lsd];  // 使用预定义的字符表  查表获取数字
    } while (i != 0);

    if (value < 0) {
        *p++ = '-';
    }
    *p = '\0';
    std::reverse(buf, p);  // 反转得到正确顺序

    return p - buf;
}

// 十六进制转换算法,用于指针
size_t convertHex(char buf[], uintptr_t value) {
    // 类似实现...
}

这些算法直接操作字符数组,避免了格式化函数的开销和临时对象创建。

2.3.3 流式接口实现

LogStream 通过大量的操作符重载实现流式接口:

// 将不同类型的数据格式化写入缓冲区
LogStream& LogStream::operator<<(int v) {
    formatInteger(v);
    return *this;  // 返回自身引用,支持链式调用
}

LogStream& LogStream::operator<<(const char* str) {
    if (str) {
        buffer_.append(str, strlen(str));
    }
    else {
        buffer_.append("(null)", 6);
    }
    return *this;
}

// 其他类型的重载...

关键是每个操作符都返回自身引用(*this),使得多个 << 操作可以连续调用。

2.4 类型安全与格式化

LogStream 通过模板和静态断言提供类型安全保证:

// 将所有整数类型统一处理
template<typename T>
void LogStream::formatInteger(T v) {
    // 实现整数格式化...
}

// Fmt类使用静态断言确保类型安全
template<typename T>
Fmt::Fmt(const char* fmt, T val) {
    // 编译期类型检查
    static_assert(std::is_arithmetic<T>::value == true, 
                 "Must be arithmetic type");
    
    // 格式化...
}

3 章节总结

1.重点内容:

  1. 理解如何使用日志级别控制日志是否记录

  2. 理解LogStream如何使用输出操作符 以C++ cout方式格式化日志。

2.扩展:

  1. 异步日志支持:

  2. 将 LogStream 与异步日志系统集成,提高性能

  3. 设计双缓冲机制,支持高并发场景

  4. 日志压缩与滚动:

  5. 支持日志文件自动滚动和压缩

  6. 实现日志清理策略,避免磁盘占用过多