轻量级日志模块实现:策略模式 + RAII 的工程化实践

目录

一、核心设计思路

二、代码逐模块拆解

[2.1 基础枚举与工具函数(日志元信息封装)](#2.1 基础枚举与工具函数(日志元信息封装))

[2.2 策略模式核心(日志输出策略)](#2.2 策略模式核心(日志输出策略))

[2.2.1 抽象策略接口](#2.2.1 抽象策略接口)

[2.2.2 控制台输出策略](#2.2.2 控制台输出策略)

[2.2.3 文件输出策略](#2.2.3 文件输出策略)

[2.3 Logger 核心类(日志生成 + 策略调度)](#2.3 Logger 核心类(日志生成 + 策略调度))

[2.3.1 核心成员与策略切换](#2.3.1 核心成员与策略切换)

[2.3.2 内部类 LogMessage(RAII 日志构建)](#2.3.2 内部类 LogMessage(RAII 日志构建))

[2.4 宏定义(简化使用)](#2.4 宏定义(简化使用))

[2.5 测试主函数(Main.cc)](#2.5 测试主函数(Main.cc))

三、核心特性总结

四、使用效果示例

[4.1 控制台输出效果](#4.1 控制台输出效果)

[4.2 文件输出效果(./log/log.txt)](#4.2 文件输出效果(./log/log.txt))

五、总结


在 C++ 工程开发中,日志模块是不可或缺的基础组件 ------ 它既要满足 "灵活输出(控制台 / 文件)" 的需求,又要保证线程安全、易用性和可扩展性。本文将拆解一个基于策略模式RAII 思想实现的轻量级日志模块,从设计思路到代码逻辑,完整还原这个高内聚、低耦合的日志组件实现。

一、核心设计思路

整个日志模块的核心是 **"日志生成" 与 "输出策略" 解耦 **:

  • 日志生成:封装固定格式(时间戳、日志等级、进程 ID、文件 / 行号)+ 自定义日志内容,形成完整日志串;
  • 输出策略:通过抽象接口定义日志刷新行为,控制台输出、文件写入作为具体策略,支持动态切换,符合 "开闭原则"。

整体架构如下:

复制代码
┌───────────────┐    ┌─────────────────────────┐
│  Logger核心类  │───>│  LogStrategy抽象策略接口  │
└───────────────┘    └─────────────────────────┘
                              │
                  ┌───────────┴───────────┐
                  ▼                       ▼
        ┌───────────────────┐   ┌─────────────────────┐
        │ ConsoleStrategy   │   │ FileLogStrategy     │
        │ (控制台输出)     │   │ (文件写入)         │
        └───────────────────┘   └─────────────────────┘

二、代码逐模块拆解

2.1 基础枚举与工具函数(日志元信息封装)

首先定义日志的基础元信息:日志等级、高精度时间戳,这是所有日志的 "固定部分"。

cpp 复制代码
namespace NS_LOG_MODULE
{
    // 1. 日志等级枚举
    enum class LogLevel
    {
        INFO,
        WARNING,
        ERROR,
        FATAL,
        DEBUG
    };

    // 日志等级转字符串(便于输出)
    std::string LogLevel2Message(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::INFO: return "INFO";
        case LogLevel::WARNING: return "WARNING";
        case LogLevel::ERROR: return "ERROR";
        case LogLevel::FATAL: return "FATAL";
        case LogLevel::DEBUG: return "DEBUG";
        default: return "UNKNOWN";
        }
    }

    // 2. 高精度时间戳(秒+微秒),保证线程安全
    std::string GetCurrentTime()
    {
        struct timeval current_time;
        gettimeofday(&current_time, nullptr);
        
        struct tm struct_time;
        localtime_r(&(current_time.tv_sec), &struct_time); // 可重入函数,避免多线程问题
        char timestr[128];
        snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
                 struct_time.tm_year + 1900,
                 struct_time.tm_mon + 1,
                 struct_time.tm_mday,
                 struct_time.tm_hour,
                 struct_time.tm_min,
                 struct_time.tm_sec,
                 current_time.tv_usec);
        return timestr;
    }
}

关键细节

  • localtime_r 替代 localtime:前者是可重入函数,多线程环境下不会因共享静态缓冲区导致时间错乱;
  • 时间戳包含微秒级精度:满足高并发场景下的日志时序区分。

2.2 策略模式核心(日志输出策略)

通过抽象接口LogStrategy定义日志刷新行为,控制台 / 文件输出作为具体策略实现,实现 "新增输出方式不修改核心逻辑"。

2.2.1 抽象策略接口
cpp 复制代码
// 策略接口:定义日志刷新的统一行为
class LogStrategy
{
public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &message) = 0; // 纯虚函数,强制子类实现
};
2.2.2 控制台输出策略
cpp 复制代码
// 具体策略1:控制台输出(线程安全)
class ConsoleStrategy : public LogStrategy
{
public:
    void SyncLog(const std::string &message) override
    {
        LockGuard lockguard(_mutex); // RAII锁,自动释放
        std::cerr << message << std::endl;
    }

private:
    Mutex _mutex; // 保证多线程下控制台输出不混乱
};

关键细节

  • Mutex+LockGuard(RAII 锁)保证多线程输出时,日志不会被 "切割";
  • 选择std::cerr而非std::coutcerr无缓冲,日志输出更及时(适合调试 / 错误场景)。
2.2.3 文件输出策略
cpp 复制代码
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";

// 具体策略2:文件写入(自动创建目录+线程安全)
class FileLogStrategy : public LogStrategy
{
public:
    // 构造时自动创建日志目录
    FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
        : _logpath(path), _logfilename(name)
    {
        LockGuard lockguard(_mutex);
        if (!std::filesystem::exists(_logpath))
        {
            try
            {
                std::filesystem::create_directories(_logpath); // C++17跨平台目录创建
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }
    }

    void SyncLog(const std::string &message) override
    {
        LockGuard lockguard(_mutex);
        // 拼接完整日志文件路径
        std::string targetlog = _logpath + (_logpath.back() != '/' ? "/" : "") + _logfilename;
        // 追加模式打开文件(避免覆盖已有日志)
        std::ofstream out(targetlog, std::ios::app);
        if (!out.is_open())
        {
            std::cerr << "open " << targetlog << "failed" << std::endl;
            return;
        }
        out << message << "\n";
        out.close(); // 及时关闭,避免文件句柄泄漏
    }

private:
    std::string _logpath;    // 日志目录
    std::string _logfilename;// 日志文件名
    Mutex _mutex;            // 保证多线程文件写入安全
};

关键细节

  • 构造函数自动创建日志目录:使用std::filesystem(C++17),跨平台兼容;
  • 追加模式写入:std::ios::app保证新日志追加到文件末尾,不覆盖历史;
  • 每次写入后关闭文件:避免长时间占用文件句柄,也避免进程崩溃导致日志丢失。

2.3 Logger 核心类(日志生成 + 策略调度)

Logger类是整个模块的 "中枢":管理输出策略、生成完整日志串,通过内部类LogMessage实现 RAII 风格的日志构建。

2.3.1 核心成员与策略切换
cpp 复制代码
class Logger
{
public:
    // 默认使用控制台策略
    Logger() { UseConsoleStrategy(); }

    // 切换到控制台策略
    void UseConsoleStrategy()
    {
        _strategy = std::make_unique<ConsoleStrategy>();
    }

    // 切换到文件策略
    void UseFileStrategy()
    {
        _strategy = std::make_unique<FileLogStrategy>();
    }

private:
    // 持有当前输出策略(unique_ptr自动管理内存)
    std::unique_ptr<LogStrategy> _strategy;
};

关键细节

  • std::unique_ptr管理策略对象:避免手动内存释放,符合 RAII 思想;
  • 策略切换接口简洁:一行代码即可切换输出方式,无需关注底层实现。
2.3.2 内部类 LogMessage(RAII 日志构建)

LogMessage是日志的 "构建器",负责拼接固定格式 + 自定义内容,并在析构时自动刷新日志(RAII 核心)。

cpp 复制代码
class Logger
{
public:
    // 内部类:封装一条完整的日志
    class LogMessage
    {
    public:
        // 构造:拼接日志固定部分
        LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
            : _level(level), _curr_time(GetCurrentTime()), _pid(getpid()),
              _filename(filename), _line(line), _logger(logger)
        {
            std::stringstream ss;
            ss << "[" << _curr_time << "] "
               << "[" << LogLevel2Message(_level) << "] "
               << "[" << _pid << "] "          // 进程ID
               << "[" << _filename << "] "    // 日志所在文件
               << "[" << _line << "] "        // 日志所在行号
               << " - ";                      // 分隔符
            _loginfo = ss.str(); // 固定部分拼接完成
        }

        // 重载<<:支持链式拼接自定义内容(仿cout)
        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            std::stringstream ss;
            ss << info;
            _loginfo += ss.str();
            return *this; // 返回自身,支持连续<<
        }

        // 析构:自动刷新日志(RAII核心)
        ~LogMessage()
        {
            if (_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
        }

    private:
        LogLevel _level;       // 日志等级
        std::string _curr_time;// 时间戳
        pid_t _pid;            // 进程ID
        std::string _filename; // 所在文件
        int _line;             // 所在行号
        std::string _loginfo;  // 完整日志串
        Logger &_logger;       // 引用外部Logger,用于调用策略刷新
    };

    // 重载():创建LogMessage对象(日志入口)
    LogMessage operator()(LogLevel level, std::string filename, int line)
    {
        return LogMessage(level, filename, line, *this);
    }
};

核心亮点(RAII)

  • 构造LogMessage时拼接固定部分,析构时自动调用SyncLog刷新日志;
  • 无需手动调用 "刷新" 接口:日志对象生命周期结束(如语句执行完)时,日志自动输出,避免遗漏。

2.4 宏定义(简化使用)

为了降低使用成本,封装宏定义,自动带入文件 / 行号等元信息:

cpp 复制代码
// 全局Logger对象,供整个工程使用
Logger logger;

// 日志入口宏:自动带入当前文件、行号
#define LOG(level) logger(level, __FILE__, __LINE__)
// 策略切换宏:一行代码切换输出方式
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

使用简化效果

cpp 复制代码
// 原写法:logger(LogLevel::DEBUG, __FILE__, __LINE__) << "xxx";
// 宏写法:LOG(LogLevel::DEBUG) << "xxx";

2.5 测试主函数(Main.cc

测试代码直观展示了模块的核心用法:切换策略、输出不同等级日志、链式拼接内容。

cpp 复制代码
#include "Logger.hpp"
#include "Mutex.hpp"

using namespace NS_LOG_MODULE;

int main()
{
    // 1. 切换到文件策略,输出日志到./log/log.txt
    ENABLE_FILE_LOG_STRATEGY();
    LOG(LogLevel::DEBUG) << "系统初始化 | 配置文件加载成功 | 版本号: v1.0.0 | 调试参数: " << 3.14 << " | 会话ID: " << 109 << " | 模块: 核心引擎";
    LOG(LogLevel::WARNING) << "资源预警 | 磁盘使用率: 85% | 剩余空间: " << 3.14 << "GB | 阈值ID: " << 109 << " | 建议清理日志文件";
    LOG(LogLevel::FATAL) << "核心错误 | 数据库连接失败 | 错误码: " << 3.14 << " | 重试次数: " << 109 << " | 系统即将退出";
    LOG(LogLevel::ERROR) << "接口调用异常 | 目标服务: user-api | 响应码: " << 3.14 << " | 请求ID: " << 109 << " | 请检查网络连通性";
    LOG(LogLevel::INFO) << "系统启动完成 | 监听端口: 8080 | 启动耗时: " << 3.14 << "s | 进程ID: " << 109 << " | 运行模式: 生产环境";
    
    // 2. 切换到控制台策略,输出日志到终端
    ENABLE_CONSOLE_LOG_STRATEGY();
    LOG(LogLevel::DEBUG) << "用户操作追踪 | 点击按钮: submit | 坐标: " << 3.14 << " | 操作序列: " << 109 << " | 客户端IP: 192.168.1.1";
    LOG(LogLevel::WARNING) << "参数校验提醒 | 输入值: " << 3.14 << " 超出合理范围 | 字段ID: " << 109 << " | 请校验用户输入";
    LOG(LogLevel::FATAL) << "内存溢出 | 已分配内存: " << 3.14 << "GB | 上限值: " << 109 << "GB | 强制终止任务";
    LOG(LogLevel::ERROR) << "文件读写失败 | 路径: ./data/config.json | 错误码: " << 3.14 << " | 文件大小: " << 109 << "KB | 权限不足";
    LOG(LogLevel::INFO) << "任务执行完成 | 任务名称: 数据同步 | 处理条数: " << 3.14 << "万 | 耗时: " << 109 << "ms | 状态: 成功";

    return 0;
}

三、核心特性总结

这个轻量级日志模块虽小,但处处体现 C++ 工程化思想:

特性 实现方式
线程安全 所有输出 / 写入操作加MutexLockGuard自动释放锁
灵活扩展 策略模式:新增输出方式(如网络日志)只需继承LogStrategy实现SyncLog
易用性 仿cout<<操作符,宏定义简化文件 / 行号传入
资源安全 RAII 思想:unique_ptr管理策略对象、LockGuard管理锁、LogMessage自动刷新
工程化细节 可重入函数、高精度时间戳、跨平台目录创建、文件追加写入

四、使用效果示例

4.1 控制台输出效果

cpp 复制代码
[2024-05-22 10:15:30.123461] [DEBUG] [18965] [./Main.cc] [16] - 用户操作追踪 | 点击按钮: submit | 坐标: 3.14 | 操作序列: 109 | 客户端IP: 192.168.1.1
[2024-05-22 10:15:30.123462] [WARNING] [18965] [./Main.cc] [17] - 参数校验提醒 | 输入值: 3.14 超出合理范围 | 字段ID: 109 | 请校验用户输入
[2024-05-22 10:15:30.123463] [FATAL] [18965] [./Main.cc] [18] - 内存溢出 | 已分配内存: 3.14GB | 上限值: 109GB | 强制终止任务
[2024-05-22 10:15:30.123464] [ERROR] [18965] [./Main.cc] [19] - 文件读写失败 | 路径: ./data/config.json | 错误码: 3.14 | 文件大小: 109KB | 权限不足
[2024-05-22 10:15:30.123465] [INFO] [18965] [./Main.cc] [20] - 任务执行完成 | 任务名称: 数据同步 | 处理条数: 3.14万 | 耗时: 109ms | 状态: 成功

4.2 文件输出效果(./log/log.txt)

cpp 复制代码
[2024-05-22 10:15:30.123456] [DEBUG] [18965] [./Main.cc] [9] - 系统初始化 | 配置文件加载成功 | 版本号: v1.0.0 | 调试参数: 3.14 | 会话ID: 109 | 模块: 核心引擎
[2024-05-22 10:15:30.123457] [WARNING] [18965] [./Main.cc] [10] - 资源预警 | 磁盘使用率: 85% | 剩余空间: 3.14GB | 阈值ID: 109 | 建议清理日志文件
[2024-05-22 10:15:30.123458] [FATAL] [18965] [./Main.cc] [11] - 核心错误 | 数据库连接失败 | 错误码: 3.14 | 重试次数: 109 | 系统即将退出
[2024-05-22 10:15:30.123459] [ERROR] [18965] [./Main.cc] [12] - 接口调用异常 | 目标服务: user-api | 响应码: 3.14 | 请求ID: 109 | 请检查网络连通性
[2024-05-22 10:15:30.123460] [INFO] [18965] [./Main.cc] [13] - 系统启动完成 | 监听端口: 8080 | 启动耗时: 3.14s | 进程ID: 109 | 运行模式: 生产环境

五、总结

这个日志模块以 "极简接口 + 灵活扩展" 为核心,通过策略模式解耦输出逻辑,通过 RAII 保证资源安全,通过工程化细节(可重入函数、线程安全、跨平台)保证稳定性。它既满足了中小型项目的日志需求,又为后续扩展(如按等级分文件、日志切割、网络输出)预留了清晰的扩展路径,是 C++ 设计模式与工程化思想结合的典型实践。

对于开发者而言,这个模块的价值不仅在于 "可用的日志工具",更在于学习如何用面向对象思想拆解问题、用 C++ 现代特性保证代码健壮性 ------ 这也是从 "写代码" 到 "做工程" 的关键一步。

相关推荐
heiqizero2 小时前
spark01-创建RDD
linux·前端·python
Agent手记2 小时前
生产工单下发不及时,频繁导致交付延期怎么办? 2026企业级智能体自动化实操指南
运维·ai·自动化
水木流年追梦2 小时前
CodeTop Top 300 热门题目8-字符串解码
linux·运维·服务器·前端·算法·leetcode
杨云龙UP2 小时前
Docker MySQL 5.7 全库备份到异地服务器实践记录_20260427
linux·运维·服务器·数据库·mysql·docker·容器
剩下了什么2 小时前
dockerfile-知识概念介绍
linux·运维·服务器
勤劳的进取家2 小时前
如何配置服务器代理转发
运维·服务器
jialan752 小时前
上海服务器 CentOS7.6 mysql8 redis jdk17 Tomcat10
运维·服务器·redis
sulikey2 小时前
个人Linux操作系统学习笔记1 - Linux权限与工具
linux·笔记·学习
秦歌6662 小时前
RAG-6-高级RAG实战案例:自适应路由 + 自评估重写 + 网络回退
java·服务器·前端·人工智能·python