【Linux C++ 日志系统实战】日志消息对象 LogMessage 完整实现:流式拼装 + 标准化输出

前言

在 Linux 高性能日志系统里,LogMessage(日志消息对象) 是最核心的 "数据载体"。它负责:

  • 统一日志格式

  • 自动携带时间、线程 ID、日志级别

  • 记录文件名、函数名、行号

  • 支持 << 流式输入任意类型数据

  • 最终输出一行标准日志


一、日志流对象的核心设计思路

日志流对象的核心目标是将日志的元信息(时间、线程 ID、文件 / 函数 / 行号)与业务内容(自定义文本、数值等)解耦,同时通过流式接口简化日志编写体验 。本次实现的LogMessage类具备以下核心特性:

  1. 封装日志等级(DEBUG/INFO/ERROR 等)、日志头(元信息)、日志正文;

  2. 基于 C++ 重载operator<<实现流式写入,支持任意可输出类型的日志内容;

  3. 适配 Linux 系统特性,获取线程 ID(TID)等系统级元信息;

  4. 提供日志内容格式化输出能力,统一日志格式。


二、公共定义 LogCommon.hpp

日志级别、缓冲区大小、级别字符串映射,所有日志组件共用。

cpp 复制代码
#ifndef LOG_COMMON_HPP
#define LOG_COMMON_HPP

namespace tulun
{
    // 缓冲区大小定义
    static const int SMALL_BUFF_LEN = 128;
    static const int MEDIAN_BUFF_LEN = 512;
    static const int LARGE_BUFF_LEN = 1024;

    // C++11 强类型枚举,日志级别
    enum class LOG_LEVEL
    {
        TRACE = 0,
        DEBUG,
        INFO,
        WARN,
        ERROR,
        FATAL,
        NUM_LOG_LEVELS,
    };

    // 级别 -> 字符串
    static const char * LLTOSTR[]=
    {
        "TRACE",
        "DEBUG",
        "INFO",
        "WARN",
        "ERROR",
        "FATAL",
        "NUM_LOG_LEVELS",
    };
}

#endif

三、核心代码结构与头文件设计(LogMessage.hpp)

首先看日志流对象的头文件定义,它决定了类的对外接口和核心成员:

cpp 复制代码
// C++ STL
#include <string>
#include <sstream>
using namespace std;

// 日志公共定义(含LOG_LEVEL枚举、LLTOSTR等级字符串映射等)
#include "LogCommon.hpp"

#ifndef LOG_MESSAGE_HPP
#define LOG_MESSAGE_HPP
namespace tulun
{
    class LogMessage
    {
    private:
        // 日志头:存储时间、线程ID、等级、文件/函数/行号等元信息
        std::string header_;
        // 日志正文:存储业务写入的日志内容
        std::string text_;
        // 日志等级:标识日志重要程度
        tulun::LOG_LEVEL level_;

    public:
        // 构造函数:初始化日志等级和元信息
        LogMessage(const tulun::LOG_LEVEL &level,
                   const std::string &filename,
                   const std::string &funcname,
                   const int line);
        // 默认的拷贝构造、赋值重载、析构函数
        LogMessage(const LogMessage &) = default;
        LogMessage &operator=(const LogMessage&) = default;
        ~LogMessage() = default;

        // 核心接口:获取日志等级、格式化输出完整日志
        const tulun::LOG_LEVEL & getLogLevel() const;
        const std::string toString() const;

        // 流式写入核心:重载operator<<,支持任意可输出类型
        template<class _Ty>
        LogMessage & operator<<(const _Ty &text)
        {
            std::ostringstream oss; 
            oss<<" : "<<text;
            text_ += oss.str();
            return *this;
        }
    };
} // namespace tulun
#endif

关键设计点解析

  1. 成员变量私有化header_/text_/level_均为私有成员,通过公共接口暴露必要操作,保证数据封装性;

  2. 模板化 operator<< :使用模板泛型支持任意可被ostringstream输出的类型(字符串、整数、浮点数等),返回对象自身引用以支持链式调用;

  3. 默认函数显式声明 :拷贝构造、赋值重载等默认函数显式声明为default,保证类的可拷贝性(适配日志系统后续的转发、存储逻辑)。


四、Linux 系统适配与核心方法实现(LogMessage.cpp)

头文件定义了接口,而源文件则实现了具体的逻辑,尤其是 Linux 系统特性的适配是核心亮点。

4.1 依赖的 Linux 系统接口

实现中用到了 Linux 系统调用获取线程 ID,核心头文件与函数:

cpp 复制代码
// Linux系统调用头文件:获取线程ID
#include <sys/syscall.h>
// 通用系统调用(如gettid依赖)
#include <unistd.h>

4.2 构造函数:日志元信息的组装

构造函数是日志头(header_)的核心生成逻辑,也是 Linux 系统特性适配的关键:

cpp 复制代码
namespace tulun
{
    LogMessage::LogMessage(const tulun::LOG_LEVEL &level,
                           const std::string &filename,
                           const std::string &funcname,
                           const int line)
        : level_(level)
    {
        // 1. 获取Linux下的线程ID(TID):syscall(SYS_gettid)
        // 注:pthread_self()返回的是线程标识,非系统级TID,此处用syscall更精准
        pid_t tid = ::syscall(SYS_gettid);
        
        // 2. 组装日志头:标准化日志元信息格式
        std::ostringstream oss;
        // 2.1 拼接当前时间(Timestamp::Now()为自定义时间戳类,输出格式化时间)
        oss << tulun::Timestamp::Now().toFormattedString() << " ";
        // 2.2 拼接线程ID(TID)
        oss << ::to_string(tid) << " ";
        // 2.3 拼接日志等级(LLTOSTR为LOG_LEVEL到字符串的映射数组)
        oss << tulun::LLTOSTR[static_cast<int>(level_)] << " ";
        // 2.4 拼接文件名(只保留最后一个/后的部分,简化日志)
        int pos = filename.find_last_of('/');
        std::string filestr = filename.substr(pos + 1);
        // 2.5 拼接函数名、行号
        oss << filestr << " " << funcname << " " << line << " ";
        
        // 3. 赋值给日志头成员
        header_ = oss.str();
    }

    // 获取日志等级:只读接口
    const tulun::LOG_LEVEL &LogMessage::getLogLevel() const
    {
        return level_;
    }

    // 格式化输出完整日志:日志头 + 日志正文
    const std::string LogMessage::toString() const
    {
        return header_ + text_;
    }
} // namespace tulun

核心实现亮点

  • 线程 ID 的精准获取 :Linux 下pthread_self()返回的是进程内的线程标识,而syscall(SYS_gettid)能获取系统级的线程 ID(与ps -L看到的 TID 一致),更便于问题定位;

  • 文件名简化处理 :通过find_last_of('/')截取文件名,避免日志中出现冗长的绝对路径,提升日志可读性;

  • 时间戳标准化 :依赖自定义Timestamp类的toFormattedString()方法,保证日志时间格式统一(如2024-03-30 15:20:30.123)。

4.3 流式写入:operator<< 的模板实现

头文件中模板化的operator<<是日志流的核心,其实现逻辑:

cpp 复制代码
template<class _Ty>
LogMessage & operator<<(const _Ty &text)
{
    std::ostringstream oss; 
    oss<<" : "<<text;  // 为每个写入项添加分隔符,提升可读性
    text_ += oss.str(); // 追加到日志正文
    return *this;       // 返回自身,支持链式调用
}

该实现的优势:

  1. 泛型支持 :模板参数_Ty兼容所有可被ostringstream输出的类型(字符串、int、double、自定义类型 <若重载了 operator<<>);

  2. 链式调用 :返回*this使得logmes<<"hello"<<123<<3.14成为可能;

  3. 分隔符统一 :每个写入项前添加:,避免日志内容粘连,提升可读性。


五、使用示例与日志输出效果(Test03_30_LogMess.cpp)

通过测试代码可直观看到日志流对象的使用方式和输出效果:

cpp 复制代码
#include<iostream>
using namespace std;

#include "LogMessage.hpp"

int main()
{
    // 构造日志对象:指定等级、当前文件/函数/行号(通过宏自动获取)
    tulun::LogMessage logmes(tulun::LOG_LEVEL::DEBUG,
                             __FILE__, __func__, __LINE__);

    // 流式写入不同类型的日志内容
    logmes<<"syr hello";
    logmes<<"2026001";
    logmes<<12.23;
    
    // 输出完整日志
    cout<<logmes.toString()<<endl;
    return 0;
}

典型输出结果:

bash 复制代码
2026/04/03-11:22:47.371020Z 4181 DEBUG Test03_30_LogMess.cpp main 10  : syr hello : 2026001 : 12.23

输出格式解析:

bash 复制代码
[格式化时间] [线程ID] [日志等级] [文件名] [函数名] [行号] [日志内容1] [日志内容2] [日志内容3]

六、Linux 环境下的优化与扩展思考

基于当前实现,可结合 Linux 系统特性进一步优化日志流对象:

1 性能优化

  • 减少内存拷贝 :当前text_使用+=拼接字符串,高频日志场景下可预分配内存(如reserve);

  • 避免重复系统调用 :线程 ID 在构造函数中只获取一次,避免多次调用syscall带来的性能损耗;

  • 异步日志适配 :可将LogMessage对象放入异步队列,避免日志写入阻塞业务线程(Linux 下可结合 epoll / 线程池实现)。

2 功能扩展

  • 日志等级过滤:结合 Linux 环境变量,在构造函数中判断是否需要生成该等级的日志,减少无效日志组装;

  • 支持日志着色:Linux 终端下可通过 ANSI 转义序列,为不同等级的日志添加颜色(如 ERROR 红色、DEBUG 绿色);

  • 兼容 syslog :可扩展toSyslog()方法,将日志输出到 Linux 系统的 syslog 服务,整合系统日志管理。

3 兼容性考虑

  • syscall(SYS_gettid)在不同 Linux 内核版本下的兼容性:可封装成工具函数,兼容gettid()的不同实现;

  • 线程安全:若日志对象跨线程使用,需添加互斥锁(Linux 下可使用pthread_mutex_t)保护text_header_的读写。


七、关键技术点详解

1. 为什么用 syscall (SYS_gettid) 获取线程 ID?

  • pthread_self() 获取的是虚拟线程 ID,不同进程可能重复

  • syscall(SYS_gettid) 获取的是 Linux 内核真实 TID

  • 可以用 ps -L -p 进程ID 精准定位线程

  • 日志排错、定位卡死线程必备

2. 日志头为什么要存文件名、函数、行号?

  • 线上出问题不需要翻代码

  • 直接根据日志定位到哪一行

  • 极大提升排查效率

3. operator<< 为什么用模板?

  • 支持 string /int/double / 自定义类型

  • 写法和 cout 一致,用户零学习成本

  • 返回 *this 支持链式调用

4. 为什么要分 header_ 和 text_?

  • 构造时一次性生成日志头

  • 用户只拼接 text,性能更高

  • 格式统一,不会乱

5. 线程安全吗?

  • 单线程构造单条日志:安全

  • 多条日志跨线程:安全(各写各的对象)

  • 后续写入文件时再加锁即可


八、总结

LogMessage 是整个日志系统的灵魂单元,它实现了:

标准日志格式(时间 + TID + 级别 + 文件 + 函数 + 行号)

Linux 真实线程 ID 获取

微秒级高精度时间戳

<< 流式输入任意类型

自动截取文件名,日志更简洁

低开销、高可用、可直接上生产

相关推荐
杨云龙UP5 小时前
Oracle 中 NOMOUNT、MOUNT、OPEN 怎么理解? 在不同场景下如何操作?_20260402
linux·运维·数据库·oracle
Amctwd5 小时前
【Linux】OpenCode 安装教程
linux·运维·服务器
xiaoye-duck5 小时前
【C++:哈希表封装】哈希表封装 myunordered_map/myunordered_set 实战:底层原理 + 完整实现
数据结构·c++·散列表
A.A呐5 小时前
【C++第二十三章】C++11
开发语言·c++
亿秒签到6 小时前
L2-007 家庭房产
数据结构·c++·算法
wwj888wwj6 小时前
Docker基础(复习)
java·linux·运维·docker
paldier6 小时前
rootfs挂载失败(error -5)的一个可能
linux
paeamecium6 小时前
【PAT甲级真题】- Longest Symmetric String (25)
数据结构·c++·算法·pat考试
A.A呐7 小时前
【C++第二十二章】哈希与散列
c++·算法·哈希算法