前言
在 Linux 高性能日志系统里,LogMessage(日志消息对象) 是最核心的 "数据载体"。它负责:
统一日志格式
自动携带时间、线程 ID、日志级别
记录文件名、函数名、行号
支持
<<流式输入任意类型数据最终输出一行标准日志
一、日志流对象的核心设计思路
日志流对象的核心目标是将日志的元信息(时间、线程 ID、文件 / 函数 / 行号)与业务内容(自定义文本、数值等)解耦,同时通过流式接口简化日志编写体验 。本次实现的LogMessage类具备以下核心特性:
-
封装日志等级(DEBUG/INFO/ERROR 等)、日志头(元信息)、日志正文;
-
基于 C++ 重载
operator<<实现流式写入,支持任意可输出类型的日志内容; -
适配 Linux 系统特性,获取线程 ID(TID)等系统级元信息;
-
提供日志内容格式化输出能力,统一日志格式。
二、公共定义 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
关键设计点解析
-
成员变量私有化 :
header_/text_/level_均为私有成员,通过公共接口暴露必要操作,保证数据封装性; -
模板化 operator<< :使用模板泛型支持任意可被
ostringstream输出的类型(字符串、整数、浮点数等),返回对象自身引用以支持链式调用; -
默认函数显式声明 :拷贝构造、赋值重载等默认函数显式声明为
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; // 返回自身,支持链式调用
}
该实现的优势:
-
泛型支持 :模板参数
_Ty兼容所有可被ostringstream输出的类型(字符串、int、double、自定义类型 <若重载了 operator<<>); -
链式调用 :返回
*this使得logmes<<"hello"<<123<<3.14成为可能; -
分隔符统一 :每个写入项前添加
:,避免日志内容粘连,提升可读性。
五、使用示例与日志输出效果(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 获取
微秒级高精度时间戳
<<流式输入任意类型自动截取文件名,日志更简洁
低开销、高可用、可直接上生产