从零实现一个 C++ 轻量级日志系统:原理与实践

🔥个人主页:Cx330🌸

❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》

《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔

《Git深度解析》:版本管理实战全解 《Qt 极境架构》

🌟心向往之行必能


🎥Cx330🌸的简介:


目录

前言:

[一. 日志系统的设计理念](#一. 日志系统的设计理念)

[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 类实现)

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

六、日志系统源码

[6.1 完整Logger.hpp代码](#6.1 完整Logger.hpp代码)

[6.2 完整测试代码](#6.2 完整测试代码)

[6.3 优化方向](#6.3 优化方向)

写在最后


前言:

日志系统可以说是每个程序员都绕不开的话题。在大型C++项目中,日志打印和记录几乎是日常开发中最常用的功能之一 ------ 排查Bug、追踪调用链、监控线上服务状态,都离不开一套好用的日志工具。

提到C++日志库,有一些人会想:日志系统到底是怎么实现的? 抛开spdlog这些现成的库,我们自己能不能从零手搓一个可用的日志组件出来?

其实手搓一个简易日志工具并不复杂,核心思路就是 将格式化后的信息输出到不同的目的地,比如控制台、文件等。在这个思路的指引下,我们可以一步步搭建自己的日志系统,并顺便搞懂:

  • 日志级别是怎么分类和管理的;

  • 日志消息如何格式化并分派到不同的输出端;

  • 异步日志如何做到高性能;

  • RAII机制在日志系统中的应用。

如果你曾对这些问题感到好奇,那这篇文章就是写给你的。下面,我们就从零开始,手搓一个实用且可扩展的C++日志工具。


一. 日志系统的设计理念

1.1 日志的核心组成要素

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

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

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

bash 复制代码
[2026-04-16 21:33:18] [DEBUG] [1030871] [Main.cc] [10] - hello world hello Cx330
         日期          日志等级    pid     源文件   行号 -         内容       root   

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

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

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

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

1.3 为什么选择策略模式?

  1. 分离关注点:日志类(上下文)只负责构建格式化后的日志消息,具体的"写到哪里"交由独立的对象完成。

  2. 运行时切换:程序运行时可随时切换日志策略,比如调试阶段用控制台输出,生产环境用文件持久化。

  3. 扩展性极佳:新增一种输出方式(比如 UDP 网络发送),只需实现一个新的类,日志类完全不需要改动。

  4. 支持组合:日志类可以持有多个对象,一条日志同时送给控制台、文件、远程服务器,这正是策略模式在集合层面的灵活运用。


二、代码设计:实现一个完整的日志库

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

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

多线程环境下,控制台、日志文件都是临界资源,多个线程同时写入会导致内容交错、乱序,必须通过互斥量保证临界区的原子性。我们基于 Linux 原生的pthread_mutex封装互斥锁,并通过 RAII 机制管理锁的生命周期,避免手动解锁导致的死锁、内存泄漏问题,这也是 C++11 std::lock_guard的核心实现原理。

  • Mutex.hpp
bash 复制代码
#ifndef __MUTEX_HPP
#define __MUTEX_HPP

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

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock,nullptr);
    }    
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    pthread_mutex_t *Orgin()
    {
        return &_lock;
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

// 锁的开关
class LockGuard
{
public:
    LockGuard(Mutex *lockp):_lockp(lockp)
    {
        _lockp->Lock();
    }
    ~LockGuard()
    {
        _lockp->Unlock();
    }
private:
    Mutex *_lockp;
};

#endif

核心设计解析

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

2.2 格式化时间戳模块

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

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

  • 时间戳实现代码(在命名空间里面)
bash 复制代码
// 获取当前时间的字符串表示(格式:YYYY-MM-DD HH:MM:SS)
std::string GetTimeStamp()
{
    // 获取从1970-01-01 UTC到当前时刻的秒数(Unix时间戳)
    time_t timestamp = time(nullptr);

    // 定义tm结构体用于存储分解后的本地时间
    struct tm data_time;
    // 将时间戳转换为本地时间(线程安全版本,localtime_r是POSIX标准)
    localtime_r(&timestamp, &data_time);

    // 缓冲区,用于存放格式化后的时间字符串
    char data_time_str[128];
    // 使用snprintf进行格式化,限制最大写入长度,防止溢出
    // 格式:年-月-日 时:分:秒,各部分不足两位时补零
    snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
             // tm_year 从1900年开始计数,需要加1900得到实际年份
             data_time.tm_year + 1900,
             // tm_mon 范围0~11,需要加1转换为实际月份
             data_time.tm_mon + 1,
             data_time.tm_mday,   // 日(1~31)
             data_time.tm_hour,   // 小时(0~23)
             data_time.tm_min,    // 分钟(0~59)
             data_time.tm_sec);   // 秒(0~60,闰秒时可达60)

    // 返回std::string对象,自动拷贝缓冲区内容
    return data_time_str;
}

细节解析

  • 可重入性保障 :使用localtime_r替代localtime,确保多线程环境下时间转换不会出现数据竞争
  • 格式化补零 :通过**%02d**确保月、日、时、分、秒始终是两位数字,保证日志格式的一致性
  • 时间偏移修正tm_year需要 + 1900 得到真实年份,tm_mon需要 + 1 得到真实月份,这是tm结构体的标准规范
  • 测试代码
bash 复制代码
#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实现类型安全的日志等级,避免普通枚举的隐式类型转换问题,同时提供枚举到字符串的转换能力。

  • 日志等级实现代码(在命名空间里面,我没带上)
bash 复制代码
// 日志等级枚举,用于区分事件的严重程度
enum LogLevel
{
    DEBUG,   // 调试信息,仅开发阶段使用,生产环境通常关闭
    INFO,    // 常规信息,如服务启动、正常业务流转
    WARNING, // 警告信息,表示潜在问题,但系统仍可正常运行
    ERROR,   // 运行时错误,某个功能可能受损,需要关注
    FATAL    // 致命错误,程序即将终止(如内存分配失败)
};

// 将日志等级枚举转换为对应的字符串(用于日志输出)
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语句实现枚举到字符串的映射,确保日志中输出可读性强的等级名称,而非整型数字
  • 测试代码
bash 复制代码
#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

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

bash 复制代码
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),核心是保证多线程环境下的输出原子性,避免日志交错。用到了我们自己的互斥锁记得包含对应头文件,我这里就不写了

bash 复制代码
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库处理目录和文件操作(记得带上对应头文件)。

bash 复制代码
#include <fstream>
#include <filesystem>
bash 复制代码
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的异常,避免目录创建失败导致程序崩溃
  • 线程安全:文件写入全程加锁,保证多线程环境下不会出现半行日志、内容交错的问题

测试代码

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

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

4.1 Logger 主类的整体架构

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

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

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

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

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

4.3 完整的 Logger 类实现

bash 复制代码
#include <memory>
#include <sstream>
#include <unistd.h>
cpp 复制代码
namespace LogModule
{
    // 真正要的日志类
    class Logger
    {
    public: 
        Logger()
        {
            UseConsoleLogStrategy();
        }
        ~Logger(){}
        // 显示器的刷新策略
        void UseConsoleLogStrategy()
        {
            _strategy = std::make_unique<ConsoleLogStrategy>();
        }
        // 文件的刷新策略
        void UseFileLogStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }

        // 内部类:一条日志
        // 目标是把一个类对象,变成一个string
        class LogMessage
        {
        public:
            LogMessage(LogLevel level,std::string &filename,int line,Logger &self)
                :_level(level),
                 _curr_time(GetTimeStamp()),
                 _pid(getpid()),
                 _filename(filename),
                 _line(line),
                 _logger(self)
            {   
                std::stringstream ss; 
                ss << "[" << _curr_time << "] "
                   << "[" << LogLevel2String(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << "- ";
                _loginfo = ss.str();
            }

            template<typename T>
            LogMessage &operator << (const T &info) 
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return  *this;
            }

            ~LogMessage() // RAII风格的日志刷新
            {
                if(_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }
        private:
            LogLevel _level;         // 日志等级
            std::string _curr_time;  // 当前时间
            pid_t _pid;              // 进程pid
            std::string _filename;   // 文件名
            int _line;               // 行号
            std::string _loginfo;    // 一条完整的日志
            Logger &_logger;         // 外部类的引用
        };

        // LogMessage 对象打印日志的时候,故意返回一个临时的 LogMessage对象
        // 为什么要返回临时内部类对象?
        LogMessage operator()(LogLevel level,std::string filename, int line)
        {
            return LogMessage(level,filename,line,*this);
        }
    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
    };
    
    Logger logger;
    // 使用宏,包装我们的日志打印过程,宏有一个特点,#define A B,B替换成A
    #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:编译期自动替换为当前源文件名
  • FILE:编译期自动替换为当前代码行号
  • LOG宏将繁琐的参数传递封装为极简的调用方式,完全对齐 glog 的使用风格

全局单例 :定义全局的logger对象,整个程序共用一个日志实例,避免重复创建,同时保证策略切换全局生效


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


六、日志系统源码

6.1 完整Logger.hpp代码

cpp 复制代码
#ifndef __LOGGER_HPP
#define __LOGGER_HPP

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

namespace LogModule
{
    // 1.获取时间
    std::string GetTimeStamp()
    {
        time_t timestamp = time(nullptr);
        struct tm data_time;
        localtime_r(&timestamp, &data_time);

        char data_time_str[128];
        snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
                 data_time.tm_year + 1900, // 从1900开始记的
                 data_time.tm_mon + 1,     // 默认月份从0开始记的
                 data_time.tm_mday,
                 data_time.tm_hour,
                 data_time.tm_min,
                 data_time.tm_sec);
        return data_time_str;
    }

    enum LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 2.日志等级
    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 &logmessage) = 0;
    };

    // 子类:继承纯虚接口类
    // 策略1
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy(){}
        ~ConsoleLogStrategy(){}
        virtual void SyncLog(const std::string &logmessage) override
        {
            LockGuard lockguard(&_mutex); 
            std::cout<<logmessage<<std::endl;
        }
    private:
        Mutex _mutex;
    };

    static const std::string glogdir = "./log/";
    static const std::string glogfilename = "log.log";
    // 子类:继承纯虚接口类
    // 策略2
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &dir = glogdir,const std::string &filename = glogfilename)
            :_logdir(dir),_logfilename(filename)
        {
            // log/log.txt
            LockGuard lockguard(&_mutex);
            if(std::filesystem::exists(_logdir))
            {
                return;
            }
            else
            {
                try
                {
                    std::filesystem::create_directories(_logdir);
                }
                catch (const std::filesystem::filesystem_error &e)
                {
                    std::cerr<< e.what() <<std::endl;
                }
            }
        }
        ~FileLogStrategy()
        {}
        void SyncLog(const std::string &logmessage) override
        {
            std::string target = _logdir + _logfilename;
            std::ofstream out(target,std::ios::app); // 追加写入文件
            if(!out.is_open())
            {
                return;
            }

            // 方法1:
            // out.write(logmessage.c_str(), logmessage.size());
            // out.write("\n", 1); // 写入换行符

            // 方法2:
            // std::string line = logmessage + '\n';
            // out.write(line.c_str(), line.size());
            
            // 方法3:
            out << logmessage << '\n';

            out.close();
        }
    private:
        std::string _logdir;
        std::string _logfilename; // ./log/XXX.log
        Mutex _mutex;
    };

    // 真正要的日志类
    class Logger
    {
    public: 
        Logger()
        {
            UseConsoleLogStrategy();
        }
        ~Logger(){}
        // 显示器的刷新策略
        void UseConsoleLogStrategy()
        {
            _strategy = std::make_unique<ConsoleLogStrategy>();
        }
        // 文件的刷新策略
        void UseFileLogStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }

        // 内部类:一条日志
        // 目标是把一个类对象,变成一个string
        class LogMessage
        {
        public:
            LogMessage(LogLevel level,std::string &filename,int line,Logger &self)
                :_level(level),
                 _curr_time(GetTimeStamp()),
                 _pid(getpid()),
                 _filename(filename),
                 _line(line),
                 _logger(self)
            {   
                std::stringstream ss; 
                ss << "[" << _curr_time << "] "
                   << "[" << LogLevel2String(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << "- ";
                _loginfo = ss.str();
            }

            template<typename T>
            LogMessage &operator << (const T &info) 
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return  *this;
            }

            ~LogMessage() // RAII风格的日志刷新
            {
                if(_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }
        private:
            LogLevel _level;         // 日志等级
            std::string _curr_time;  // 当前时间
            pid_t _pid;              // 进程pid
            std::string _filename;   // 文件名
            int _line;               // 行号
            std::string _loginfo;    // 一条完整的日志
            Logger &_logger;         // 外部类的引用
        };

        // LogMessage 对象打印日志的时候,故意返回一个临时的 LogMessage对象
        // 为什么要返回临时内部类对象?
        LogMessage operator()(LogLevel level,std::string filename, int line)
        {
            return LogMessage(level,filename,line,*this);
        }
    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
    };
    
    Logger logger;
    // 使用宏,包装我们的日志打印过程,宏有一个特点,#define A B,B替换成A
    #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 优化方向

  1. 修复策略切换与日志记录的数据竞争 -- 对Logger::_strategy 的读写加锁(如shared_mutex + shared_ptr),避免多线程切换策略时崩溃。

  2. 文件策略避免每次写入打开/关闭文件 -- 在FileLogStrategy 构造时持有一个 ofstream 对象,减少系统调用开销。

  3. 增加日志等级过滤 -- 在 LOG 宏或 LogMessage 构造前检查等级阈值,避免无效的字符串构造。

  4. 优化字符串拼接效率 -- 使用 ostringstream成员变量或 fmt库,减少临时对象与内存分配。

  5. 单例化全局 Logger -- 采用 Meyers Singleton,解决静态初始化顺序问题。

  6. 完善文件策略的目录创建 -- 使用std::call_once 或程序启动时统一创建,避免多实例并发创建目录的竞态。

  7. 规范异常处理与降级策略 -- 文件打开失败时记录到 stderr 或抛异常,避免静默丢失日志。

  8. 支持异步日志(高吞吐场景) -- 引入后台线程与无锁队列,解耦业务线程与 I/O 操作。

  9. 增强可重入性与信号安全 -- 提供信号安全的专用接口(如 write系统调用),或明确禁止在信号处理函数中使用。

  10. 支持自定义日志格式 -- 抽象 Formatter接口,允许用户定制输出布局。


写在最后

至此,我们完整剖析了一个现代 C++ 流式日志系统的核心设计:从日志等级的枚举定义,到策略模式解耦输出目标,再到 RAII + 临时对象实现的自动刷新,以及线程安全与可重入性的深度考量。

这个日志库虽然只有短短几百行代码,却凝聚了策略模式、RAII、智能指针、临时对象生命周期、流式接口、宏与预定义标识符等 C++ 关键思想。更重要的是,通过分析它的线程安全漏洞和性能瓶颈,我们更能理解并发编程的复杂性 以及工程落地必须权衡的取舍

当然,任何代码都不是完美的。你已看到它的十项优化方向------从修复数据竞争到支持异步日志,每一点改进都能让这个轮子更滚得更远。如果你正在为自己项目的日志组件苦恼,不妨从这份代码出发,增加等级过滤、持久的文件流、可配置格式等特性,打造一个真正生产就绪的日志库。

最后,感谢你跟随本文深入到这个看似简单却暗藏玄机的模块中。日志是系统的"黑匣子",好的日志设计能让你在排查问题时事半功倍。希望这份解析能为你带来实实在在的启发,也欢迎在评论区分享你的日志系统实践或疑问。

Happy Logging! 📝

相关推荐
Agent产品评测局1 小时前
国产vs海外AI Agent方案,制造业场景适配性横评:企业级自动化选型全景深度解析
运维·人工智能·ai·chatgpt·自动化
Mike117.1 小时前
GBase 8a 慢任务处理时 KILL 和 PROCESSLIST 的使用边界
大数据·数据库
程序leo源1 小时前
Linux深度理解
linux·运维·服务器·c语言·c++·青少年编程·c#
AI玫瑰助手1 小时前
Python流程控制:while循环嵌套与死循环避免技巧
开发语言·python·信息可视化
Quinn271 小时前
正点原子 RK3562 Android14 Ubuntu 编译 SDK 环境准备:依赖、repo 与 Swap 配置一次搞定
linux·运维·ubuntu·mpu·正点原子·rk3562·arm linux
计算机安禾1 小时前
【c++面向对象编程】第7篇:static成员:属于类而不是对象的变量和函数
java·c++·算法
影sir1 小时前
STL容器——list类
c++·链表·stl·list
怀旧,1 小时前
【Linux系统编程】22. 线程同步与互斥(上)
linux·运维·服务器
ooseabiscuit1 小时前
PHP与C++:Web与系统编程的终极对决
前端·c++·php