手搓工业级 C++ 线程安全日志系统:基于策略模式解耦,兼容 glog 使用风格


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一. 日志系统的核心设计理念](#一. 日志系统的核心设计理念)
    • [1.1 日志的核心组成要素](#1.1 日志的核心组成要素)
    • [1.2 日志系统的两大核心阶段](#1.2 日志系统的两大核心阶段)
    • [1.3 为什么选择策略模式?](#1.3 为什么选择策略模式?)
  • [二. 前置基础模块实现](#二. 前置基础模块实现)
    • [2.1 RAII 风格互斥锁封装(线程安全基石)](#2.1 RAII 风格互斥锁封装(线程安全基石))
    • [2.2 格式化时间戳模块(附对应模块测试)](#2.2 格式化时间戳模块(附对应模块测试))
    • [2.3 类型安全的日志等级模块](#2.3 类型安全的日志等级模块)
  • [三. 基于策略模式的日志刷新核心实现](#三. 基于策略模式的日志刷新核心实现)
    • [3.1 抽象策略基类 LogStrategy](#3.1 抽象策略基类 LogStrategy)
    • [3.2 控制台日志策略 ConsoleLogStrategy](#3.2 控制台日志策略 ConsoleLogStrategy)
    • [3.3 文件日志策略 FileLogStrategy](#3.3 文件日志策略 FileLogStrategy)
  • [四. 日志主体类与流式输出设计](#四. 日志主体类与流式输出设计)
    • [4.1 Logger 主类的整体架构](#4.1 Logger 主类的整体架构)
    • [4.2 LogMessage 内部类:RAII 实现日志自动刷新](#4.2 LogMessage 内部类:RAII 实现日志自动刷新)
    • [4.3 完整的 Logger 类实现](#4.3 完整的 Logger 类实现)
  • [五. 日志系统的线程安全与可重入性深度解析](#五. 日志系统的线程安全与可重入性深度解析)
  • [六. 实战:日志系统完整使用示例(附带完整Logger.hpp代码呈现)](#六. 实战:日志系统完整使用示例(附带完整Logger.hpp代码呈现))
    • [6.1 完整Logger.hpp代码](#6.1 完整Logger.hpp代码)
    • [6.2 完整测试代码](#6.2 完整测试代码)
    • [6.3 进阶优化方向](#6.3 进阶优化方向)
  • 结尾:

前言:

在 Linux 后端开发、多线程服务端编程的场景中,日志系统是定位问题、监控服务状态的核心基础设施。很多初学者习惯用std::cout直接打印调试信息,但在多线程环境下,会出现日志内容交错、输出乱序的问题;同时,硬编码的输出方式无法灵活切换日志目的地(控制台 / 文件 / 网络),也不支持日志分级、问题定位等工业级需求。市面上已有成熟的日志库如 spdlog、glog、Boost.Log,但从零实现一个线程安全的日志系统,能让我们深度理解设计模式、线程互斥同步、RAII 资源管理、可重入函数等 Linux 系统编程的核心知识点。本文将基于策略模式,从零实现一个兼容 glog 使用风格、支持多线程安全、可灵活扩展的 C++ 日志系统,所有代码与设计均贴合工业级开发规范。


一. 日志系统的核心设计理念

1.1 日志的核心组成要素

一条合格的工业级日志,必须包含必选字段可选扩展字段,确保问题可追溯、状态可监控:

  • 必选核心字段
    • 时间戳:可读性强的年月日时分秒格式,精准定位事件发生时间
    • 日志等级:区分事件严重程度,支持分级过滤与告警
    • 日志内容:用户自定义的业务 / 调试信息
  • 可选扩展字段
    • 进程 PID / 线程 ID:多进程 / 多线程环境下定位执行流
    • 文件名与行号:精准定位日志打印的代码位置
    • 自定义扩展字段:如模块名、用户 ID 等业务信息

本文实现的日志格式如下,完全兼容主流日志库的规范:

bash 复制代码
[2026-04-16 21:33:18] [DEBUG] [1030871] [Main.cc] [10] - hello world hello Lotso, 3.14


1.2 日志系统的两大核心阶段

日志的生命周期可拆分为两个完全解耦的阶段,这是我们设计的核心依据:

  1. 日志形成阶段:将时间戳、等级、文件名、行号、用户内容等信息,拼接成一条完整的格式化字符串,与日志输出目的地无关
  2. 日志刷新阶段:将格式化完成的日志字符串,写入到指定目的地(控制台、文件、数据库、网络等),仅关注写入逻辑

两个阶段解耦后,我们可以独立扩展刷新逻辑,而无需修改日志格式化的核心代码,这正是策略模式的最佳应用场景。

  • 可以看看图中的初始化代码框架,我们使用了命名空间,所以后面的代码中可能大家看起来会有缩进啥的,有的我有给带上,有的没有,大家可以最后再看看整体代码,里面包含完整的头文件和其他注意的地方

1.3 为什么选择策略模式?

策略模式是行为型设计模式的一种,核心思想是定义一系列算法(策略),将每个算法封装起来,并让它们可以互相替换

在日志系统中,不同的日志刷新方式就是不同的策略:控制台输出、文件持久化、网络上报都是独立的刷新算法。使用策略模式带来了这些核心优势:

  • 开闭原则:新增日志刷新目的地(如数据库),只需新增一个策略类,无需修改原有代码
  • 解耦:日志格式化核心逻辑与刷新逻辑完全分离,代码职责单一
  • 动态切换:程序运行时可随时切换日志策略,比如调试阶段用控制台输出,生产环境用文件持久化

二. 前置基础模块实现

日志系统的核心前提是线程安全,同时需要时间戳、日志等级等基础能力支撑,我们先实现这些底层模块。

2.1 RAII 风格互斥锁封装(线程安全基石)

多线程环境下,控制台、日志文件都是临界资源,多个线程同时写入会导致内容交错、乱序,必须通过互斥量保证临界区的原子性。

我们基于 Linux 原生的pthread_mutex封装互斥锁,并通过 RAII 机制管理锁的生命周期,避免手动解锁导致的死锁、内存泄漏问题,这也是 C++11 std::lock_guard的核心实现原理。这个工作我们之前就做过了,直接拿过来就可以了

  • Mutex.hpp
cpp 复制代码
#ifndef MUTEX_HPP
#define MUTEX_HPP

#include <iostream>
#include <pthread.h>

// 互斥锁封装类:提供加锁/解锁及获取原始锁的接口
class Mutex
{
public:
    // 构造函数:初始化互斥锁
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    // 析构函数:销毁互斥锁
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
    // 加锁操作
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    // 解锁操作
    void UnLock()
    {
        pthread_mutex_unlock(&_lock);
    }
    // 获取原始互斥锁指针,用于需要原生 pthread_mutex_t 的接口
    pthread_mutex_t* Origin()
    {
        return &_lock;
    }
private:
    pthread_mutex_t _lock;  // POSIX 互斥锁
};

// RAII 风格的锁守卫类:构造时加锁,析构时解锁,自动管理锁的生命周期
class LockGuard
{
public:
    // 构造函数:接收一个 Mutex 指针,并立即加锁
    LockGuard(Mutex* lockptr) : _lockptr(lockptr)
    {
        _lockptr->Lock();
    }
    // 析构函数:自动解锁
    ~LockGuard()
    {
        _lockptr->UnLock();
    }
private:
    Mutex* _lockptr;  // 指向被管理的互斥锁
};

#endif

核心设计解析

  1. 禁用拷贝:互斥量是系统资源,不允许拷贝和赋值,避免重复释放、死锁等问题(这个里面没有,大家可以自己加上)
  2. RAII 机制LockGuard在对象构造时加锁,析构时自动解锁,即使代码中途抛出异常,也能保证锁被释放,彻底避免手动解锁的遗漏
  3. 接口封装 :屏蔽原生pthread库的接口细节,提供更符合 C++ 面向对象的使用方式

2.2 格式化时间戳模块(附对应模块测试)

时间戳是日志的核心字段,我们需要实现秒级、可重入、格式化 的时间戳获取功能。

这里重点注意:C 标准库的localtime函数是不可重入的,多线程环境下会出现数据错乱,因此必须使用可重入版本localtime_r,它由调用者提供结构体缓冲区,避免了全局静态变量的竞态问题。

  • 时间戳实现代码(在命名空间里面,我没带上)
cpp 复制代码
	// 1. 获取时间
    std::string GetTimeStamp()
    {
        time_t currentTime = time(nullptr); // 默认获取当前时区的时间(获取到的是秒级时间戳)
        
        // 我们希望把这个时间转换成年-月-日 时:分:秒
        struct tm dataTime;
        
        // 使用带 _r (Reentrant) 后缀的版本 localtime_r 而不是普通的 localtime。
        // 因为普通版本内部使用静态全局变量保存结果,在多线程写日志时极易发生数据覆盖错乱;
        // _r 版本要求我们自己传入存放结果的地址(&dataTime),保证了多线程环境下的绝对安全。
        localtime_r(&currentTime, &dataTime);

        // 准备一个足够大的字符数组作为格式化字符串的缓冲区
        char dataTimeStr[128];
        
        // 使用 snprintf 将时间结构体安全地格式化为类似 [2026-04-16 19:21:32] 的排版
        // %4d: 4位数字对齐; %02d: 2位数字,不足两位的在高位自动补0(如 4月 显示为 04)
        snprintf(dataTimeStr, sizeof(dataTimeStr), "%4d-%02d-%02d %02d:%02d:%02d", 
                 dataTime.tm_year + 1900, // 坑点修复:tm_year 表示的是自 1900 年起经过的年数,必须加上 1900
                 dataTime.tm_mon + 1,     // 坑点修复:tm_mon 范围是 [0, 11](0代表1月),必须加上 1
                 dataTime.tm_mday,        // 日([1, 31])
                 dataTime.tm_hour,        // 时
                 dataTime.tm_min,         // 分
                 dataTime.tm_sec          // 秒
                );
        
        // 字符数组会自动隐式转换为 std::string 对象返回
        return dataTimeStr;
    }

核心细节解析

  • 可重入性保障 :使用localtime_r替代localtime,确保多线程环境下时间转换不会出现数据竞争
  • 格式化补零 :通过%02d确保月、日、时、分、秒始终是两位数字,保证日志格式的一致性
  • 时间偏移修正tm_year需要 + 1900 得到真实年份,tm_mon需要 + 1 得到真实月份,这是tm结构体的标准规范


  • 测试代码
cpp 复制代码
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;

// 测试时间戳模块
void testTime()
{
    for(int i = 0; i < 5; i++)
    {
        std::cout << GetTimeStamp() << std::endl;
        sleep(1);
    }
}

int main()
{
    // 1. 测试时间
    testTime();
    return 0;
}

2.3 类型安全的日志等级模块

日志等级用于区分事件的严重程度,我们使用 C++11 的enum class实现类型安全的日志等级,避免普通枚举的隐式类型转换问题,同时提供枚举到字符串的转换能力。

  • 日志等级实现代码(在命名空间里面,我没带上)
cpp 复制代码
// 2. 日志等级 -- 枚举类型(整数)转换成字符串类型
    // 使用 enum class (强类型枚举) 而不是普通 enum 
    // 优势:1. 具有独立的作用域,避免命名冲突;2. 不允许隐式类型转换,更加类型安全
    enum class LogLevel
    {
        DEBUG,   // 调试信息:用于开发过程中输出详细状态,帮助定位问题。生产环境通常关闭
        INFO,    // 常规信息:记录程序的关键运行节点,用于了解系统正常运行的状态
        WARNING, // 警告信息:出现了预期之外的情况,但系统仍能继续运行,需要引起关注
        ERROR,   // 错误信息:发生了运行时错误,导致当前操作失败,但主程序依然存活
        FATAL    // 致命错误:最高严重级别。出现了无法恢复的问题,程序即将崩溃或被迫退出
    };

    /**
     * @brief 将日志等级枚举转换为可读的字符串
     * @param level 日志等级枚举值
     * @return 对应的字符串表示。由于 cout 不直接支持打印强类型枚举,必须进行此类映射转换
     */
    std::string LogLevel2String(LogLevel level)
    {
        switch(level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            // 防御性编程:处理未知的枚举值,防止程序出现未定义行为
            return "UNKNOWN";
        }
    }

核心设计解析

  • 类型安全enum class不会隐式转换为整型,避免了错误的等级赋值,编译期即可发现类型问题
  • 等级分层:遵循业界通用的 5 级日志规范,覆盖从调试到致命错误的全场景
  • 字符串转换 :通过 switch 语句实现枚举到字符串的映射,确保日志中输出可读性强的等级名称,而非整型数字


  • 测试代码
cpp 复制代码
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;

// 测试日志类枚举类型转字符类型模块
void testEnum()
{
    std::cout << LogLevel2String(LogLevel::DEBUG) << std::endl;
    std::cout << LogLevel2String(LogLevel::INFO) << std::endl;
    std::cout << LogLevel2String(LogLevel::WARNING) << std::endl;
    std::cout << LogLevel2String(LogLevel::ERROR) << std::endl;
    std::cout << LogLevel2String(LogLevel::FATAL) << std::endl;
}

int main()
{
    // 2. 测试枚举类转字符串类型
    testEnum();
    return 0;
}

三. 基于策略模式的日志刷新核心实现

基于策略模式的设计,我们先定义抽象的刷新策略基类,再分别实现控制台和文件两种具体的刷新策略,后续可无限扩展其他策略。

3.1 抽象策略基类 LogStrategy

抽象基类定义了所有刷新策略必须实现的纯虚接口,同时使用虚析构函数确保子类对象能正确析构。

cpp 复制代码
namespace LogModule 
{
    // 3. 刷新策略
    // 基类: 策略模式 (Strategy Pattern)
    // 核心思想:将"日志的产生"与"日志的刷新目的地"解耦。
    // 通过定义统一的接口,使得程序可以在运行时动态决定将日志输出到控制台、文件、数据库或网络。
    class LogStrategy
    {
    public:
        // 虚析构函数:在多态体系中,基类必须拥有虚析构函数。
        // 这样当我们通过基类指针删除派生类对象时,才能确保调用到子类的析构函数,防止内存泄漏。
        virtual ~LogStrategy() = default; // 不在这里析构

        // 核心刷新接口:这是一个纯虚函数。
        // 纯虚函数的核心作用是定义一种"契约",强制派生类(子类)必须实现具体的逻辑。
        // 不同的子类可以根据自己的策略(如 ConsoleStrategy 或 FileStrategy)来实现不同的刷新行为。
        virtual void SyncLog(const std::string &message) = 0; // 强制子类对其进行重写
    };
}

设计说明

  • 纯虚函数SyncLog定义了策略的统一接口,入参是格式化完成的日志字符串,子类只需关注具体的写入逻辑
  • 虚析构函数是 C++ 多态的基础规范,避免通过基类指针释放子类对象时出现内存泄漏

3.2 控制台日志策略 ConsoleLogStrategy

控制台策略负责将日志输出到标准错误流(stderr),核心是保证多线程环境下的输出原子性,避免日志交错。用到了我们自己的互斥锁记得包含对应头文件,我这里就不写了

cpp 复制代码
namespace LogModule
{
    // 策略1: 控制台日志策略
    // 子类:继承自策略基类,用于将日志直接刷新到标准输出(显示器),常用于本地开发与调试 
    class ConsoleLogStrategy: public LogStrategy
    {
    public:
        // 构造函数与析构函数:当前策略不涉及复杂资源申请,故使用默认实现即可 
        ConsoleLogStrategy(){}
        ~ConsoleLogStrategy(){}

        /**
         * @brief 实现具体的日志同步逻辑------刷新到控制台
         * @param message 组装好的完整日志字符串
         */
        void SyncLog(const std::string &message) override // 检查重写的错误
        {
            // 【核心原理】显示器(stdout)在多线程环境下属于"临界资源"。
            // 如果不加保护,多个线程同时调用 std::cout 会导致各条日志的字符在屏幕上发生"交织"或乱码 。
            // 使用自定义的 LockGuard 配合互斥锁,确保这一系列操作的原子性 。
            LockGuard logGuard(&_mutex);
            std::cout << message << std::endl;
        }

    private:
        // 互斥锁:专门用于保护当前控制台输出的原子性,防止并发打印时消息错乱 
        Mutex _mutex;
    };
}

核心细节解析

  • 线程安全保障:控制台是全局临界资源,通过互斥锁保证同一时刻只有一个线程能执行输出操作,彻底避免多线程日志交错
  • stderr 输出 :使用std::cerr而非std::cout,因为 stderr 无缓冲区,日志会实时输出,避免程序崩溃时缓冲区日志丢失
  • RAII 锁管理 :使用LockGuard自动管理锁,无需手动解锁,代码更健壮


3.3 文件日志策略 FileLogStrategy

文件策略负责将日志持久化到磁盘文件,核心功能包括:自动创建日志目录、追加模式写入、线程安全保障,使用 C++17 的filesystem库处理目录和文件操作(记得带上对应头文件)。

cpp 复制代码
#include <fstream>
#include <filesystem>
cpp 复制代码
namespace LogModule 
{
    // 定义全局默认路径与文件名常量 
    const static std::string gdefaultlogdir = "./log/";
    const static std::string gdefaultlogfilename = "log.txt";

    // 策略2:文件类日志策略
    // 子类:继承自策略基类,实现将日志持久化到磁盘文件的逻辑 
    class FileLogStrategy: public LogStrategy
    {
    public:
        /**
         * @brief 构造函数:初始化日志路径并确保目录环境就绪
         * @param logdir 日志存储目录
         * @param logfilename 日志文件名称
         */
        FileLogStrategy(const std::string &logdir = gdefaultlogdir, const std::string &logfilename = gdefaultlogfilename)
            :_logdir(logdir),
            _logfilename(logfilename)
        {
            // 【重点】构造阶段即进行加锁保护。
            // 理由:判断目录是否存在并创建目录属于"先检查再执行(Check-Then-Act)"模式,
            // 必须保证这一系列操作的原子性,防止多线程同时创建导致竞态冲突 。
            LockGuard lockGuard(&_mutex);
            
            // 使用 C++17 的 <filesystem> 库进行跨平台路径检查 
            if(std::filesystem::exists(_logdir))
            {
                return;
            }
            else 
            {
                try 
                {
                    // 递归创建目录(类似于 Linux 命令 mkdir -p),如果路径中包含多级不存在的目录会一并创建 
                    std::filesystem::create_directories(_logdir);
                } 
                catch (std::filesystem::filesystem_error &e) 
                {
                    // 捕获文件系统异常(如权限不足、磁盘空间不足等)并输出错误信息 
                    std::cerr << e.what() << std::endl;
                }
            }
        }

        // 析构函数:由于不涉及手动管理的堆内存或特殊文件句柄(使用局部变量流管理),故使用默认实现
        ~FileLogStrategy(){}

        /**
         * @brief 执行具体的日志落盘操作
         * @param message 待写入的完整日志字符串
         */
        void SyncLog(const std::string &message) override
        {
            // 加锁保护:防止多线程同时写入同一文件导致内容交织(Interleaving)乱码 
            LockGuard logGuard(&_mutex);
            
            // 构造完整的目标文件路径
            std::string target = _logdir + _logfilename;
            
            // 以追加模式(std::ios::app)打开文件流:
            // 核心逻辑:保证每条新日志都写在文件末尾,不会覆盖已有日志内容 。
            std::ofstream out(target, std::ios::app); // 追加

            if(!out.is_open()) // 打开文件检查
            {
                return; // 如果因权限或路径问题打开失败,则放弃本次写入,防止程序崩溃
            }
            
            // 将消息流式写入文件,并手动添加换行符以符合日志排版规范 
            out << message << "\n"; // 流式写入
            
            // 文件流离开作用域或显式调用 close 会自动触发刷新并关闭文件
            out.close();
        }

    private:
        std::string _logdir;      // 存储目录路径
        std::string _logfilename; // 存储文件名称
        Mutex _mutex;             // 用于保障当前策略类实例在多线程环境下的线程安全 
    };
}

核心设计解析

  • 自动目录创建 :构造函数中检查日志目录是否存在,不存在则通过create_directories递归创建,避免手动创建目录的繁琐
  • 追加模式写入 :使用std::ios::app打开文件,所有日志都会追加到文件末尾,不会覆盖历史日志,符合日志系统的通用规范
  • 异常处理 :目录创建时捕获filesystem的异常,避免目录创建失败导致程序崩溃
  • 线程安全:文件写入全程加锁,保证多线程环境下不会出现半行日志、内容交错的问题



测试代码

cpp 复制代码
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;

// 测试刷新策略
void testStrategy()
{
    std::string message1 = "console: hello Log, hello Lotso!";
    std::string message2 = "file: hello Log, hello Lotso!";

    std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleLogStrategy>(); // 父类指针指向子类对象
    strategy->SyncLog(message1);
    strategy->SyncLog(message1);
    strategy->SyncLog(message1);
    strategy->SyncLog(message1);
    strategy->SyncLog(message1);

    strategy = std::make_unique<FileLogStrategy>(); // 父类指针指向子类对象
    strategy->SyncLog(message2);
    strategy->SyncLog(message2);
    strategy->SyncLog(message2);
    strategy->SyncLog(message2);
    strategy->SyncLog(message2);
}

int main()
{
    // 3. 测试策略
    testStrategy();
    return 0;
}

四. 日志主体类与流式输出设计

完成基础模块和策略模式的实现后,我们来实现日志系统的主体类,核心目标是:兼容 glog 的流式调用风格、自动拼接日志元信息、RAII 自动触发日志刷新

4.1 Logger 主类的整体架构

Logger类是日志系统的对外入口,核心职责包括:

  • 管理当前使用的日志刷新策略,支持动态切换
  • 提供仿函数接口,生成日志消息对象
  • 封装策略切换的便捷接口

4.2 LogMessage 内部类:RAII 实现日志自动刷新

LogMessageLogger的内部类,是整个日志系统最巧妙的设计:

  • 构造函数中完成日志元信息(时间、等级、PID、文件名、行号)的拼接
  • 重载<<运算符,支持流式拼接任意类型的日志内容
  • 析构函数中自动触发日志刷新,利用临时对象的生命周期实现 "写完即刷新"

4.3 完整的 Logger 类实现

cpp 复制代码
#include <memory>
#include <sstream>
#include <unistd.h>
cpp 复制代码
namespace LogModule // 定义日志模块命名空间
{
    /**
     * @brief Logger 类:日志系统的核心统筹管理者
     * 负责维护日志刷新策略(显示器/文件)并作为产生日志消息的入口
     */
    class Logger 
    {
    public:
        // 构造函数:初始化时默认开启控制台刷新策略
        Logger()
        {
            UseConsoleLogStrategy();
        }

        // 切换策略:动态更换为控制台输出策略
        void UseConsoleLogStrategy()
        {
            _strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 切换策略:动态更换为文件输出策略
        void UseFileLogStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }

        ~Logger(){};
    
        /**
         * @brief LogMessage 内部类:代表单条日志消息的生命周期管理
         * 核心设计思想:利用临时对象的生命周期(RAII)实现日志的自动组装与刷新
         */
        class LogMessage
        {
        public:
            /**
             * @brief 构造函数:构建日志的"左半部分"(前缀信息)
             * 包括时间、等级、PID、文件名和行号,并预置到流中
             */
            LogMessage(LogLevel level, const std::string &filename, int line, Logger &self)
                : _currenttime(GetTimeStamp()) // 获取当前格式化时间
                , _loglevel(LogLevel2String(level)) // 等级转字符串
                , _pid(getpid()) // 获取进程 PID 
                , _filename(filename)
                , _line(line)
                , _logger(self) // 持有外部统帅类的引用,用于后续刷新
            {
                std::stringstream ss;
                // 像拼积木一样组装固定格式的前缀
                ss << "[" << _currenttime << "] "
                   << "[" << _loglevel << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << "- ";
                
                _loginfo = ss.str(); // 直接拼上去
            }

            /**
             * @brief 析构函数:整个设计的灵魂(RAII 自动刷新)
             * 当这一行日志代码执行完毕(临时对象销毁)时,自动触发物理刷新
             */
            ~LogMessage()
            {
                if(_logger._strategy)
                {
                    // 走到尽头了,调用刷新策略刷新出来
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

            /**
             * @brief 模板重载 <<:构建日志的"右半部分"(用户自定义内容)
             * 利用模板接纳任意类型,并通过 stringstream 实现安全的字符串转换与链式拼接
             */
            template <typename T>
            LogMessage& operator << (const T& info)
            {
                std::stringstream ss;
                ss << info; // 自动处理 int, double, string 等类型转换
                _loginfo += ss.str(); // 将内容追加到完整日志字符串中
                return *this; // 返回自身引用,支持像 cout 一样的连续 << 调用
            }  
        private:
            std::string _currenttime;
            std::string _loglevel;
            int _pid;
            std::string _filename;
            int _line;
            std::string _loginfo; // 存储整条待刷新的日志字符串

            Logger &_logger; // 外部类引用:让消息知道自己隶属于哪个 Logger
        };

        /**
         * @brief 仿函数重载:作为"桥梁"连接宏调用与内部消息对象
         * Logger对象打印日志的时候,故意返回一个LogMessage的临时对象
         */
        LogMessage operator() (LogLevel level, const std::string filename, int line)
        {
            // 创建并返回临时对象,开启后续的流式 << 操作
            return LogMessage(level, filename, line, *this);
        }
    private:
        std::unique_ptr<LogStrategy> _strategy; // 多态策略指针:决定日志去向
    };

    // 定义一个全局模块的Logger对象, 方便后续的使用
    Logger logger;

/**
 * @brief LOG 宏:对外提供的极简调用接口
 * 自动捕获当前代码的 __FILE__ (文件名) 和 __LINE__ (行号)
 */
#define LOG(level) logger(level, __FILE__, __LINE__)

// 还是使用宏: 提供动态调整日志策略的便捷开关
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
}

核心设计深度解析

  • 仿函数机制 :重载operator()Logger对象可以像函数一样调用,返回一个LogMessage临时对象,这是实现流式调用的核心
  • RAII 自动刷新LogMessage是临时对象,当整条LOG(xxx) << "xxx"语句执行完毕后,临时对象会被析构,析构函数中自动调用策略的刷新接口,无需用户手动触发刷新
  • 模板化流式运算符 :通过模板重载<<运算符,支持 int、double、string、char 等任意可流输出的类型,实现和std::cout一致的使用体验


  • 预定义宏封装
    • __FILE__:编译期自动替换为当前源文件名
    • __LINE__:编译期自动替换为当前代码行号
    • LOG宏将繁琐的参数传递封装为极简的调用方式,完全对齐 glog 的使用风格
  • 全局单例 :定义全局的logger对象,整个程序共用一个日志实例,避免重复创建,同时保证策略切换全局生效


  • 测试代码
cpp 复制代码
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;

// 总体测试
void test()
{
    // 开启控制台策略
    ENABLE_CONSOLE_LOG_STRATEGY();
    LOG(LogLevel::DEBUG) << "CONSOLE: hello Lotso " << 7.9 << " bd";
    LOG(LogLevel::INFO) << "CONSOLE: hello Lotso " << 7.9 << " bd";
    LOG(LogLevel::WARNING) << "CONSOLE: hello Lotso " << 7.9 << " bd";
    LOG(LogLevel::ERROR) << "CONSOLE: hello Lotso " << 7.9 << " bd";
    LOG(LogLevel::FATAL) << "CONSOLE: hello Lotso " << 7.9 << " bd";

    // 开启文件策略
    ENABLE_FILE_LOG_STRATEGY();
    LOG(LogLevel::DEBUG) << "FILE: hello Lotso " << 7.9 << " bd";
    LOG(LogLevel::INFO) << "FILE: Lotso " << 7.9 << " bd";
    LOG(LogLevel::WARNING) << "FILE: hello Lotso " << 7.9 << " bd";
    LOG(LogLevel::ERROR) << "FILE: hello Lotso " << 7.9 << " bd";
    LOG(LogLevel::FATAL) << "FILE: hello Lotso " << 7.9 << " bd";
}

int main()
{
    // 4. 整体测试
    test();
    return 0;
}

五. 日志系统的线程安全与可重入性深度解析

多线程环境下,日志系统的线程安全是重中之重,我们从多个维度做了全面保障:

  • 临界资源的互斥保护:控制台、日志文件都是全局临界资源,所有写入操作都通过互斥锁保证原子性,同一时刻只有一个线程能执行写入操作,彻底避免内容交错、乱序。
  • 可重入函数的使用 :时间戳获取使用localtime_r而非不可重入的localtime,避免多线程同时调用时出现时间数据错乱;所有函数均不使用全局静态变量,所有状态都保存在对象内部,保证重入安全。
  • 锁的粒度与 RAII 管理 :锁的粒度严格控制在写入操作的最小范围,避免长时间持有锁导致性能下降;同时通过LockGuard的 RAII 机制保证锁一定会被释放,即使写入过程中出现异常,也不会出现死锁。
  • 无锁的日志格式化阶段 :日志的格式化(拼接头部、用户内容)是在每个线程的LogMessage临时对象中完成的,每个线程的日志格式化完全独立,没有共享资源竞争,无需加锁,最大化提升了并发性能。

六. 实战:日志系统完整使用示例(附带完整Logger.hpp代码呈现)

我们通过一个完整的示例,展示日志系统的基础使用、策略切换、多线程安全验证。

6.1 完整Logger.hpp代码

cpp 复制代码
#ifndef LOGGER_HPP
#define LOGGER_HPP

#include <fstream>
#include <iostream>
#include <ctime>
#include <cstdio>
#include <memory>
#include <sstream>
#include <string>
#include <filesystem>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    // 1. 获取时间
    std::string GetTimeStamp()
    {
        time_t currentTime = time(nullptr); // 默认获取当前时区的时间
        // 我们希望把这个时间转换成年-月-日 时:分:秒
        struct tm dataTime;
        
        // 使用线程安全的版本 localtime_r,防止在多线程并发获取时间时
        // 因为共享静态全局变量而导致的时间数据覆盖错乱。
        localtime_r(&currentTime, &dataTime);

        char dataTimeStr[128];
        // 使用 snprintf 保证缓冲区不溢出,%02d 确保时间位宽不足时自动补0(如09秒)
        snprintf(dataTimeStr, sizeof(dataTimeStr), "%4d-%02d-%02d %02d:%02d:%02d", 
                 dataTime.tm_year + 1900, // tm_year 是从1900年开始计算的偏移量
                 dataTime.tm_mon + 1,     // tm_mon 范围是 [0, 11],需加1修正
                 dataTime.tm_mday,
                 dataTime.tm_hour,
                 dataTime.tm_min,
                 dataTime.tm_sec
                );
        
        return dataTimeStr;
    }

    // 2. 日志等级 -- 枚举类型(整数)转换成字符串类型
    // 使用 enum class 强类型枚举,避免命名污染,提高类型检查的严谨性
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    /**
     * @brief 辅助函数:将枚举常量映射为可读字符串
     * 解决强类型枚举无法直接通过 std::cout 打印的问题
     */
    std::string LogLevel2String(LogLevel level)
    {
        switch(level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    // 3. 刷新策略
    // 基类: 策略模式
    // 设计意图:将"日志消息的生成"与"日志消息的输出去向"解耦,方便后续扩展网络、数据库等输出端
    class LogStrategy
    {
    public:
        // 虚析构函数:确保通过基类指针释放子类对象时,子类的资源(如文件句柄)能被正确释放
        virtual ~LogStrategy() = default; // 不在这里析构
        // 纯虚函数:定义统一的刷新接口规范
        virtual void SyncLog(const std::string &message) = 0; // 强制子类对其进行重写
    };

    // 策略1: 控制台日志策略
    // 子类
    class ConsoleLogStrategy: public LogStrategy
    {
    public:
        ConsoleLogStrategy(){}
        ~ConsoleLogStrategy(){}
        void SyncLog(const std::string &message) override // 检查重写的错误
        {
            // 显示器在多线程下是"临界资源",加锁防止多线程输出字符交织(Interleaving)
            LockGuard logGuard(&_mutex);
            std::cout << message << std::endl;
        }
    private:
        Mutex _mutex;
    };

    const static std::string gdefaultlogdir = "./log/";
    const static std::string gdefaultlogfilename = "log.txt";

    // 策略2:文件类日志策略
    // 子类
    class FileLogStrategy: public LogStrategy
    {
    public:
        // 构造函数:初始化路径并利用 C++17 库确保目录环境就绪
        FileLogStrategy(const std::string &logdir = gdefaultlogdir, const std::string &logfilename = gdefaultlogfilename)
            :_logdir(logdir),
            _logfilename(logfilename)
        {
            // 创建目录前加锁,防止多线程同时执行判断与创建操作引发的竞态条件
            LockGuard lockGuard(&_mutex);
            if(std::filesystem::exists(_logdir))
            {
                return;
            }
            else 
            {
                try 
                {
                    // 递归创建目录(mkdir -p),若权限不足或磁盘满会抛出异常
                    std::filesystem::create_directories(_logdir);
                } 
                catch (std::filesystem::filesystem_error &e) 
                {
                    std::cerr << e.what() << std::endl;
                }
            }
        }
        ~FileLogStrategy(){}

        void SyncLog(const std::string &message) override
        {
            // 文件 I/O 是昂贵的临界资源操作,加锁保证单条日志写入的原子性
            LockGuard logGuard(&_mutex);
            std::string target = _logdir + _logfilename;
            
            // 使用 std::ios::app (append) 追加模式,保证新旧日志共存而不被覆盖
            std::ofstream out(target, std::ios::app); // 追加
            if(!out.is_open()) // 打开文件
            {
                return;
            }
            out << message << "\n"; // 流式写入并换行
            out.close(); // 关闭文件流,触发缓冲区刷新
        }
    private:
        std::string _logdir;
        std::string _logfilename;
        Mutex _mutex;
    };

    /**
     * @brief Logger 类:日志系统的中央控制器
     * 内部嵌套了 LogMessage 类来实现精妙的 RAII 自动刷新机制
     */
    class Logger 
    {
    public:
        Logger()
        {
            UseConsoleLogStrategy(); // 默认策略
        }
        void UseConsoleLogStrategy()
        {
            _strategy = std::make_unique<ConsoleLogStrategy>();
        }
        void UseFileLogStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }
        ~Logger(){};
    
        // 内部类:负责单条日志的组装和析构刷新
        class LogMessage
        {
        public:
            // 构造函数:预组装日志"前缀"部分
            LogMessage(LogLevel level, const std::string &filename, int line, Logger &self)
                : _currenttime(GetTimeStamp())
                , _loglevel(LogLevel2String(level))
                , _pid(getpid())
                , _filename(filename)
                , _line(line)
                , _logger(self) // 保存引用,以便在析构时找到所属的 Logger 进行刷新
            {
                std::stringstream ss;
                ss << "[" << _currenttime << "] "
                   << "[" << _loglevel << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << "- ";
                
                _loginfo = ss.str(); // 此时前缀已拼入缓冲区
            }

            /**
             * @brief 核心设计:RAII 机制触发刷新
             * 当 LOG(...) << "msg"; 这行语句执行完毕,临时对象生命周期结束,
             * 在析构函数中调用策略接口,保证日志在写完即刻、必然被刷出。
             */
            ~LogMessage()
            {
                if(_logger._strategy)
                {
                    // 走到尽头了,调用刷新策略刷新出来
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

            // 用模版重载 << 运算符:接纳各种类型(int, string, double等)
            template <typename T>
            LogMessage& operator << (const T& info)
            {
                std::stringstream ss;
                ss << info; // 自动完成类型转换
                _loginfo += ss.str(); // 追加到内容主体中
                return *this; // 返回引用支持链式调用,如 LOG << a << b << c;
            }   
        private:
            std::string _currenttime;
            std::string _loglevel;
            int _pid;
            std::string _filename;
            int _line;
            std::string _loginfo;

            Logger &_logger; // 外部类引用:用于访问具体刷新策略
        };

        /**
         * @brief 重载仿函数 operator()
         * 这是桥梁:将宏参数传入,并返回一个持有 Logger 权限的临时消息对象
         */
        LogMessage operator() (LogLevel level, const std::string filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }
    private:
        // 使用 unique_ptr 配合策略基类实现运行时多态
        std::unique_ptr<LogStrategy> _strategy; // 策略
    };

    // 定义一个全局模块的Logger对象, 方便后续的使用
    Logger logger;

// 定义宏:捕获编译器内置变量 __FILE__ 和 __LINE__,简化用户调用 API
#define LOG(level) logger(level, __FILE__, __LINE__)

// 便捷切换输出目的地的宏定义
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
}
#endif

6.2 完整测试代码

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Logger.hpp" // 我们实现的日志头文件

using namespace LogModule;

// 多线程测试函数:10个线程同时打印日志
void *thread_log_test(void *arg)
{
    char *thread_name = (char *)arg;
    for (int i = 0; i < 5; i++)
    {
        LOG(LogLevel::INFO) << thread_name << " 执行日志打印, 循环次数: " << i;
        usleep(1000);
    }
    return nullptr;
}

int main()
{
    // 1. 基础控制台日志输出
    std::cout << "===== 控制台日志测试 =====" << std::endl;
    ENABLE_CONSOLE_LOG_STRATEGY();
    LOG(LogLevel::DEBUG) << "这是DEBUG调试日志, 数值: " << 3.14159;
    LOG(LogLevel::INFO) << "这是INFO常规日志, 服务启动成功";
    LOG(LogLevel::WARNING) << "这是WARNING警告日志, 配置缺失, 使用默认值";
    LOG(LogLevel::ERROR) << "这是ERROR错误日志, 文件读取失败";
    LOG(LogLevel::FATAL) << "这是FATAL致命日志, 内存耗尽, 服务退出";

    // 2. 切换为文件日志策略
    std::cout << "\n===== 文件日志测试 =====" << std::endl;
    ENABLE_FILE_LOG_STRATEGY();
    LOG(LogLevel::INFO) << "切换为文件日志策略, 日志将持久化到./log/log.txt";
    LOG(LogLevel::DEBUG) << "文件日志测试, 支持链式拼接: " << "字符串 " << 1234 << " 浮点数 " << 2.71828;

    // 3. 多线程线程安全测试
    std::cout << "\n===== 多线程日志测试 =====" << std::endl;
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, thread_log_test, (void *)"thread-1");
    pthread_create(&t2, nullptr, thread_log_test, (void *)"thread-2");
    pthread_create(&t3, nullptr, thread_log_test, (void *)"thread-3");
    pthread_create(&t4, nullptr, thread_log_test, (void *)"thread-4");

    // 等待所有线程执行完毕
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);

    LOG(LogLevel::INFO) << "多线程日志测试完成, 无乱序、无交错";
    return 0;
}

6.3 进阶优化方向

本篇博客实现的日志系统已满足基础的工业级需求,还可以从以下方向做进阶优化,适配更高性能、更复杂的业务场景:

  • 异步日志机制:当前是同步写入,磁盘 IO 会阻塞业务线程。可实现双缓冲区异步日志,业务线程将日志写入内存缓冲区,后台线程专门负责磁盘写入,彻底消除 IO 阻塞。
  • 日志滚动与分片:支持按文件大小、按天 / 小时切割日志文件,避免单个日志文件过大,同时支持过期日志自动清理。
  • 日志分级过滤:支持设置全局日志等级,比如生产环境关闭 DEBUG 级日志,减少日志量和 IO 开销。
  • 更多策略扩展:实现网络日志策略(上报到日志中心)、数据库日志策略、syslog 系统日志策略等,基于策略模式可无缝扩展。
  • 线程 ID 打印:新增线程 ID 字段,多线程环境下问题定位更精准。
  • 日志格式化优化:支持用户自定义日志格式,适配不同的日志采集规范。

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:从零实现一个线程安全的日志系统,不仅是造轮子的过程,更是对 Linux 多线程编程、设计模式、C++RAII 机制、可重入函数等核心知识点的深度实践。本文基于策略模式实现的日志系统,做到了格式化与刷新逻辑解耦、线程安全、使用便捷、可无限扩展,完全兼容 glog 的流式使用风格,同时通过源码级的拆解,让我们理解了成熟日志库背后的底层实现原理。在实际开发中,我们可以直接使用 spdlog 等成熟的开源库,但只有理解了底层实现,才能在遇到日志乱序、程序崩溃日志丢失、多线程性能瓶颈等问题时,精准定位并解决问题,这也是底层能力的核心价值。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
2402_854808372 小时前
c++ RAII机制详解 c++如何利用RAII管理资源
jvm·数据库·python
pearlthriving2 小时前
STL容器及其底层
开发语言·c++·算法
吕源林2 小时前
CSS如何使用Less的Merge功能合并多个属性值_通过逗号或空格组织css参数
jvm·数据库·python
qq_330037992 小时前
Go语言如何写负载均衡器_Go语言负载均衡器实战教程【完整】
jvm·数据库·python
2501_914245932 小时前
如何验证SQL删除操作的影响行数_通过ROW_COUNT获取反馈
jvm·数据库·python
2301_816660212 小时前
如何处理DG Broker的ORA-16664错误_主备库网络通信与TNS配置排查
jvm·数据库·python
2601_949815332 小时前
Node.js HTTP模块详解:创建服务器、响应请求与客户端请求
服务器·http·node.js
2401_835956812 小时前
mysql如何配置用户只读权限_授予SELECT权限与限制操作
jvm·数据库·python
银河麒麟操作系统2 小时前
服务器通用(全架构)【系统安全加固方案】
安全·系统安全