跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

Linux 下基于策略模式的手写高性能日志模块实现

在 Linux 环境下使用 C++ 手写高性能日志模块的设计与实现。内容涵盖池化技术概念、日志系统设计(等级、时间戳、文件/控制台输出)、策略模式的应用以及线程安全处理。通过运算符重载实现灵活的日志拼接,为后续线程池实战奠定基础。

晚风叙旧发布于 2026/3/23更新于 2026/7/615K 浏览
Linux 下基于策略模式的手写高性能日志模块实现

背景与目标

在掌握线程控制、锁以及条件变量这些并发编程的利器之后,我们终于可以着手设计并实现一个真正实用的组件——线程池。

但在敲下第一行代码之前,有一个更宏观、更具指导性的思想需要先深入理解,那就是'池化技术'(Pooling Technology)。这不仅是线程池的核心,也是构建几乎所有高性能后台服务的基石。

什么是池化技术?

让我们先抛开代码,来看一个生活中的例子:

想象一下,你经营着一家非常火爆的网约车公司。每当有乘客下单时,你才开始打电话招募司机、给他们注册、分配车辆。等这一套流程走完,乘客早已不耐烦地取消了订单。

聪明的做法是什么?你提前招募并培训好一批司机,让他们在几个热门地段的'司机站'里随时待命。订单一来,你立刻从站里派一位空闲的司机出发。任务完成后,司机不是解雇回家,而是返回站点继续等待下一个订单。

这个'司机站'就是'池'。池化技术的核心思想就是:将一批昂贵的、需要频繁使用的资源预先创建好并统一管理起来,当需要时直接从'池'中获取,用完后不是销毁,而是归还给'池',以供后续复用。

为什么要池化?

正如下图提到的,池化技术旨在'减少底层重复工作'。

然而,如果我们直接一头扎进复杂的并发代码中,就好比在没有地图和手电筒的情况下探索一个漆黑的洞穴——我们很快就会迷失方向。

这个'手电筒'和'地图',就是日志系统。在多线程环境中,断点调试(GDB)的作用会因为线程间的时序和调度问题而大打折扣。一个可靠的、能够记录关键信息和错误状态的日志系统,是我们分析、调试和监控我们未来线程池运行状态的生命线。

但在动手写日志系统之前,我们先要学习一种能让它变得无比灵活和强大的设计模式——策略模式 (Strategy Pattern)。

日志与策略模式

为了讲解日志模式,这里不得不提到设计模式。IT 行业涌入的人很多,为了让新手们不太拖大佬的后腿,于是大佬们针对一些经典的常见场景,给定了一些对应的解决方案,这就是设计模式。在编写过程中会参考谷歌是如何搭建日志框架,如何进行设计的。

日志认识

计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。

那么一个日志有哪些必要的指标呢?格式应该是怎样的呢?

必要:

  1. 时间戳 (记录出现问题时的时间)
  2. 日志内容 (我报的内容是啥错?)
  3. 日志等级 (这个问题是正常输出的;是申请失败,不影响运行;还是引起错误,可以重新重启的呢?)
  • DEBUG: 做测试
  • INFO: 正常输出
  • WARNING: 申请失败,不影响运行
  • ERROR: 引起错误,可以重新重启
  • FATAL: 不一定是正常结束,需要排查

可选:

  1. 文件名和行号
  2. 进程,线程相关 id 信息等

日志格式:

[可读性很好的时间] [日志等级] [进程 pid] [打印对应日志的文件名][行号] - 消息内容,支持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG]    - hello world
     - hello world
[202938]
[main.cc]
[17]
[2024-08-04 12:27:03]
[WARNING]
[202938]
[main.cc]
[23]

代码编写

进行日志格式代码设计的时候,需要注意以下两个核心问题:

  1. 刷新策略 --> 是向显示器刷新,还是文件刷新,还是网络等等呢?
  2. 需要刷新那么就一定得构建一条完整的日志,而这需要日志格式的支持。
日志等级
// 规定出场景的日志等级
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };
std::string Level2String(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";
    }
}

这里通过枚举分为 5 个日志等级,那为什么还需要 Level2String 这个函数呢?

DEBUG 等在编译后就是数字,为了将枚举值转换为人类可读的字符串,因此用 Level2String 函数将数字转为字符串。

时间戳

系统提供一个获取时间戳的系统调用:time_t time(time_t *tloc); tloc 指的是时区,默认为 nullptr 即可。time_t 是什么呢?long int 一个长整数。

// 1. 获取时间戳
time_t currtime = time(nullptr);

可是时间戳我们看不懂啊?同样需要转为人类看的时间,提供了一套函数把时间戳转换为人眼可读的语言。

struct tm *localtime(const time_t *timep); struct tm *localtime_r(const time_t *restrict timep, struct tm *restrict result); timep 是输入参数;restrict result 是输出参数。

这两个函数都将一个指向 time_t 类型(通常是自 1970-01-01 00:00:00 +0000 (UTC) 以来的秒数)的指针作为输入,并将其转换为本地时间(根据系统设定的时区进行计算),然后将转换后的结果填充到一个 struct tm 结构体中。

带 _r 是什么意思呢?凡是带 _r 的代表该函数是可被重入的。

struct tm {
    int tm_sec;   /* Seconds [0, 60] */
    int tm_min;   /* Minutes [0, 59] */
    int tm_hour;  /* Hour [0, 23] */
    int tm_mday;  /* Day of the month [1, 31] */
    int tm_mon;   /* Month [0, 11] (January = 0) */
    int tm_year;  /* Year minus 1900 */
    int tm_wday;  /* Day of the week [0, 6] (Sunday = 0) */
    int tm_yday;  /* Day of the year [0, 365] (Jan/01 = 0) */
    int tm_isdst; /* Daylight savings flag */
    long tm_gmtoff; /* Seconds East of UTC */
    const char *tm_zone; /* Timezone abbreviation */
}; 
// 2. 如何把时间戳转换成为 20XX-08-04 12:27:03
struct tm currtm;
localtime_r(&currtime, &currtm);
// 3. 转换成为字符串 -- debug?
char timebuffer[64];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d", currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday, currtm.tm_hour, currtm.tm_min, currtm.tm_sec);
文件名和行号

#define LOG(level) logger(level, __FILE__, __LINE__) __FILE__:自动替换为当前源代码文件的文件名(字符串类型);__LINE__:自动替换为当前代码所在的行号(整数类型)。

刷新策略

假设这时已经拿到了一条完整的日志,而这条日志是向显示器刷新还是向文件刷新还是网络刷新?由我们进行选择,因此我们必须要写一个关于刷新策略的类。这里通过定义:

  • LogStrategy 的基类 -- 定义统一的日志行为,定义所有日志输出策略必须实现的核心方法,确保不同输出方式的接口一致性。
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备 (显示器,文件)
// 基类方法
class LogStrategy {
public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &logmessage) = 0;
};
  • ConsoleLogStrategy—— 控制台日志输出

将来我们的日志可以被多种线程访问的,如果刷新测量是往显示器做刷新,一旦刷新往显示器上打,显示器作为临界资源就需要被保护起来,因此需要进行加锁啊!

// 显示器刷新
class ConsoleLogStrategy : public LogStrategy {
public:
    ~ConsoleLogStrategy() { }
    void SyncLog(const std::string &logmessage) override {
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }
private:
    Mutex _lock;
};
  • FileLogStrategy—— 文件日志输出

当刷新策略设定为向文件刷新日志时,自然会想到对日志信息按等级分类 —— 比如哪些属于 INFO 级、哪些是 ERROR 级、哪些又该归为 FATAL 级,再将不同等级的日志分别写入对应文件中。但随之会遇到一个问题:若把这些日志文件都直接放在当前目录下,随着时间推移,文件会越积越多,整个目录会显得杂乱无章。所以很自然地会考虑创建一个专门的 log 目录,用来统一存放这些日志文件。可这里又有了新的疑问:我们目前只学过用 mkdir 指令来创建目录,却还没接触过通过系统调用来创建目录的方法,那该如何通过系统调用实现 log 目录的创建呢?

int mkdir(const char *pathname, mode_t mode);

这里并不用上面的系统调用,提供 C++17 的文件操作 --> 判断目录是否存在,不存在就创建。

std::filesystem::exists(_dir_path_name); //检查指定路径对应的目录或文件是否存在
std::filesystem::create_directories(_dir_path_name); //用于递归创建目录
const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";

// 文件刷新
class FileLogStrategy : public LogStrategy {
public:
    FileLogStrategy(const std::string &dir = logdefaultdir, const std::string filename = logfilename) : _dir_path_name(dir), _filename(filename) {
        LockGuard lockguard(&_lock);
        if (std::filesystem::exists(_dir_path_name)) {
            return;
        }
        // 可能父目录不允许创建等问题,所以需要 try 一下
        try {
            std::filesystem::create_directories(_dir_path_name);
        } catch (const std::filesystem::filesystem_error &e) {
            std::cerr << e.what() << "\r\n";
        }
    }
    void SyncLog(const std::string &logmessage) override {
        {
            LockGuard lockguard(&_lock);
            std::string target = _dir_path_name;
            target += "/";
            target += _filename;
            std::ofstream out(target.c_str(), std::ios::app); // append
            if (!out.is_open()) {
                return;
            }
            out << logmessage << "\n"; // out.write
            out.close();
        }
    }
    ~FileLogStrategy() { }
private:
    std::string _dir_path_name; // log
    std::string _filename;      // hello.log => log/hello.log
    Mutex _lock;
};
// 网络刷新
激活策略
class Logger {
public:
    Logger() { }
    void EnableConsoleLogStrategy() { _strategy = std::make_unique<ConsoleLogStrategy>(); }
    void EnableFileLogStrategy() { _strategy = std::make_unique<FileLogStrategy>(); }
    ~Logger() { }
private:
    std::unique_ptr<LogStrategy> _strategy;
};
日志内容

实现日志信息的方法有很多种,这里介绍一种谷歌实现日志的十分巧妙的方法:

首先定义了一个内部类 Logger,为了能支持将一条日志信息刷新出去,将外部类的刷新策略也带进内部类里,这样一旦形成日志信息,就可以把数据刷新出去。

class LogMessage {
private:
    std::string _curr_time;     // 日志时间
    LogLevel _level;            // 日志等级
    pid_t _pid;                 // 进程 pid
    std::string _filename;
    int _line;
    std::string _loginfo;       // 一条合并完成的,完整的日志信息
    Logger &_logger;            // 提供刷新策略的具体做法
};
LOG(LogLevel::ERROR) << "hello world" << ", 3.14 " << 123;

LogMessage 的构造如下:

LogMessage(LogLevel level, std::string &filename, int line, Logger &logger) : _curr_time(GetCurrentTime()), _level(level), _pid(getpid()), _filename(filename), _line(line), _logger(logger) {
    std::stringstream ss;
    ss << "[" << _curr_time << "] " << "[" << Level2String(_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 operator()(LogLevel level, std::string filename, int line) {
    return LogMessage(level, filename, line, *this);
}

是啊总算可以串起来了:

LOG(LogLevel::ERROR) << "hello world" << ", 3.14 " << 123;

会被宏替换为 logger(LogLevel::ERROR, __FILE__, __LINE__) << "hello world" << ", 3.14 " << 123;

接着 logger(LogLevel::ERROR, __FILE__, __LINE__) 会调用 () 运算符重载,故意拷贝,形成 LogMessage 临时对象,拿到了除日志内容的相关信息,需要注意的是 () 重载返回的是一个临时对象啊!后续在被 << 时,会被持续引用,直到没有 <<,_loginfo 已经有一条完整的信息了。此时还差一口气没有补上,如何把这条日志信息刷新出去呢?由于该临时对象没被 << 时,生命周期结束了,此时会被析构啊!那么我们在析构函数这将信息刷新出去不就行了?

总结

本文介绍了 Linux 日志系统的设计与实现,重点讲解了日志格式设计和刷新策略。主要内容包括:

  1. 日志等级划分(DEBUG/INFO/WARNING/ERROR/FATAL)及对应字符串转换;
  2. 时间戳获取与格式化;
  3. 基于策略模式的日志输出方式(控制台输出、文件输出等);
  4. 采用 Google 风格的日志内容构建方法,通过运算符重载实现灵活的日志信息拼接。文章还详细阐述了文件日志策略中的目录创建、线程安全处理等关键技术点,最终实现了一个支持多级别、多输出方式的灵活日志系统框架。

完整代码示例

#pragma once
#include <iostream>
#include <string>
#include <filesystem> // C++17 文件操作
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp"

// 1. 将一条日志信息进行分等级
// 2. 获得时间戳,转换为年月日
// 3. 选择刷新策略
// 4. 把消息带出去

// 规定出场景的日志等级
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };

std::string Level2String(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";
    }
}

// 20XX-08-04 12:27:03
std::string GetCurrentTime() {
    // 1. 获取时间戳
    time_t currtime = time(nullptr);
    // 2. 如何把时间戳转换成为 20XX-08-04 12:27:03
    struct tm currtm;
    localtime_r(&currtime, &currtm);
    // 3. 转换成为字符串 -- debug?
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d", currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday, currtm.tm_hour, currtm.tm_min, currtm.tm_sec);
    return timebuffer;
}

///////////////////////////////////////////////////////////////////
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备 (显示器,文件)
// 基类方法
class LogStrategy {
public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &logmessage) = 0;
};

// 显示器刷新
class ConsoleLogStrategy : public LogStrategy {
public:
    ~ConsoleLogStrategy() { }
    void SyncLog(const std::string &logmessage) override {
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }
private:
    Mutex _lock;
};

const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";

// 文件刷新
class FileLogStrategy : public LogStrategy {
public:
    FileLogStrategy(const std::string &dir = logdefaultdir, const std::string filename = logfilename) : _dir_path_name(dir), _filename(filename) {
        LockGuard lockguard(&_lock);
        if (std::filesystem::exists(_dir_path_name)) {
            return;
        }
        try {
            std::filesystem::create_directories(_dir_path_name);
        } catch (const std::filesystem::filesystem_error &e) {
            std::cerr << e.what() << "\r\n";
        }
    }
    void SyncLog(const std::string &logmessage) override {
        {
            LockGuard lockguard(&_lock);
            std::string target = _dir_path_name;
            target += "/";
            target += _filename;
            std::ofstream out(target.c_str(), std::ios::app); // append
            if (!out.is_open()) {
                return;
            }
            out << logmessage << "\n"; // out.write
            out.close();
        }
    }
    ~FileLogStrategy() { }
private:
    std::string _dir_path_name; // log
    std::string _filename;      // hello.log => log/hello.log
    Mutex _lock;
};

// 网络刷新
//////////////////////////////////////////////////////
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger {
public:
    Logger() { }
    void EnableConsoleLogStrategy() { _strategy = std::make_unique<ConsoleLogStrategy>(); }
    void EnableFileLogStrategy() { _strategy = std::make_unique<FileLogStrategy>(); }

    // 形成一条完整日志的方式
    class LogMessage {
    public:
        LogMessage(LogLevel level, std::string &filename, int line, Logger &logger) : _curr_time(GetCurrentTime()), _level(level), _pid(getpid()), _filename(filename), _line(line), _logger(logger) {
            std::stringstream ss;
            ss << "[" << _curr_time << "] " << "[" << Level2String(_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() {
            if (_logger._strategy) {
                _logger._strategy->SyncLog(_loginfo);
            }
        }
    private:
        std::string _curr_time;     // 日志时间
        LogLevel _level;            // 日志等级
        pid_t _pid;                 // 进程 pid
        std::string _filename;
        int _line;
        std::string _loginfo;       // 一条合并完成的,完整的日志信息
        Logger &_logger;            // 提供刷新策略的具体做法
    };

    LogMessage operator()(LogLevel level, std::string filename, int line) {
        return LogMessage(level, filename, line, *this);
    }
    ~Logger() { }
private:
    std::unique_ptr<LogStrategy> _strategy;
};

Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()

目录

  1. 背景与目标
  2. 什么是池化技术?
  3. 为什么要池化?
  4. 日志与策略模式
  5. 日志认识
  6. 代码编写
  7. 日志等级
  8. 时间戳
  9. 文件名和行号
  10. 刷新策略
  11. 激活策略
  12. 日志内容
  13. 总结
  14. 完整代码示例
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • CTF 竞赛常见解题技巧与工具使用指南
  • 二分查找实战:山峰数组的峰顶索引与寻找峰值
  • Linux 路径符号详解:~、. 和 / 的含义与区别
  • Linux 下使用 C++ 实现 UDP 通信的 Socket 编程详解
  • Linux 一切皆文件:深入理解文件与文件 IO
  • Linux 一切皆文件:深入理解文件与文件 IO
  • Linux 工程化实战:构建你的第一个命令行工具项目
  • Linux 文件 I/O 全景指南:从 open 到重定向详解
  • Linux 新手入门:软件安装、Vim 操作与 GCC 编译基础工具链
  • Linux 信号入门:进程通信机制与实战应用
  • Linux 信号量详解与基于环形队列的生产消费模型实现
  • Linux 信号处理:可重入函数与安全实践
  • Linux 线程同步与互斥深度解析:从锁机制到生产者消费者模型
  • Linux 线程控制与 pthread 基础
  • Linux 线程与进程深度剖析:虚实内存转换、实现机制与优缺点
  • Linux 线程概念与 pthread 接口入门
  • C++ 手写线程池:基于策略模式实现日志模块
  • Linux 线程池封装与实现详解
  • Linux 系统安装与部署 Miniconda 详细教程
  • Linux 下 Python 绘图中文乱码解决方案

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online