C++工业级日志项目(二)日志等级+消息封装
1:上一篇
上一篇我们完成了项目的工具模块util.hpp的实现,实现了跨平台的时间获取,文件存在判断,目录创建等基础功能。
这一篇我要实现日志库最核心的两个基础模块:日志等级定义和日志消息封装。
这两个模块是整个日志库的骨架,所有后续的格式化、输出、调度逻辑都完全基于它们。
2:日志等级设计
1:为什么要有日志等级
日志等级是日志库最基础也是最重要的功能之一,没有等级过滤的日志库是完全不可用的。
在开发环境中,我们需要输出大量的 DEBUG 级别的调试信息来排查问题;但到了生产环境,这些调试信息不仅毫无用处,还会产生巨大的磁盘 IO 开销,甚至拖慢整个程序的性能。通过设置不同的日志等级,我们可以在不修改代码的情况下,灵活控制日志的输出详细程度。
2:最终确定的日志等级
| 等级 | 含义 | 使用场景 |
|---|---|---|
| UNKNOW | 未知等级 | 用于默认值和错误处理 |
| DEBUG | 调试信息 | 开发环境,记录程序内部的详细运行状态 |
| INFO | 普通信息 | 生产环境,记录程序的正常运行事件(如启动、停止、连接成功) |
| WARN | 警告信息 | 不影响程序运行,但可能存在潜在问题(如配置缺失、参数异常) |
| ERR | 错误信息 | 程序某个功能模块出错,但整体还能继续运行(如数据库连接失败、接口调用超时) |
| FATAL | 致命错误 | 程序无法继续运行,必须立即退出(如内存耗尽、核心依赖缺失) |
| OFF | 关闭所有日志 | 用于完全禁用某个日志器的输出 |
3:为什么用类嵌套而不是直接使用强制枚举
一开始我想直接用 enum class LogLevel,但后来发现了一个问题:如果以后要给日志等级添加辅助方法(比如判断等级高低、比较等级),直接用 enum class 就不太方便了。所以我最终采用了类嵌套枚举的设计:
class LogLevel {
public:
enum class value {
UNKNOW=0,
DEBUG,
INFO,
WARN,
ERR,
FATAL,
OFF
};
// 这里可以添加各种静态辅助方法
};
优势
- 扩展性强:以后可以很方便地在 LogLevel 类中添加静态辅助方法,比如
isHigherThan()、isLowerThan()等 - 封装性好:所有和日志等级相关的逻辑都集中在 LogLevel 类中,符合单一职责原则
- 同样具备强类型安全:内部的 enum class 依然是强类型,不会有隐式转换的问题
- 避免命名冲突:所有等级都在
LogLevel::value作用域下,不会和其他同名常量冲突
4:等级转换字符串函数
我还实现了一个静态的 ToString 函数,把枚举值转换成对应的字符串,方便后续格式化输出。这里我返回的是 const char* 而不是 std::string,原因很简单:
- 字符串都是字面量,生命周期和程序一样长,不需要动态分配内存
- 返回 const char * 效率更高,避免了 std::string 的拷贝开销
- 后续格式化的时候可以直接用,不需要再调用 c_str ()
5:完整的level.hpp的实现
#pragma once
namespace my_log {
/**
* @brief 日志等级类
* 定义了所有日志等级,并提供等级转字符串的静态方法
*/
class LogLevel {
public:
/**
* @brief 日志等级枚举值
* 从低到高依次为:UNKNOW < DEBUG < INFO < WARN < ERR < FATAL < OFF
*/
enum class value {
UNKNOW = 0, // 未知等级,用于默认值
DEBUG, // 调试信息
INFO, // 普通信息
WARN, // 警告信息
ERR, // 错误信息
FATAL, // 致命错误
OFF // 关闭所有日志
};
/**
* @brief 将日志等级转换为对应的字符串
* @param level 日志等级枚举值
* @return const char* 等级对应的字符串字面量
*/
static const char* ToString(LogLevel::value level)
{
switch (level){
case LogLevel::value::DEBUG: return "DEBUG";
case LogLevel::value::INFO: return "INFO";
case LogLevel::value::WARN: return "WARN";
case LogLevel::value::ERR: return "ERR";
case LogLevel::value::FATAL: return "FATAL";
case LogLevel::value::OFF: return "OFF";
}
return "UNKNOW";
}
};
}
踩过的坑:
一开始我把错误等级命名为ERROR,结果在 Windows 下编译的时候,和windows.h里定义的#define ERROR 0宏冲突了,编译器报错。其实看VS2022的扩展可以很清楚看到ERROR本身就是宏了,最后改成了ERR才解决。这也让我深刻体会到了宏的危害,以后能不用宏就尽量不用。
3:日志消息封装LogMsg
1:为什么要封装日志消息
一条完整的日志绝不仅仅是一段文本,它包含了很多重要的元信息,这些信息在排查问题的时候至关重要。我最终确定了 7 个必须包含的字段:
- 日志时间戳:用于过滤日志输出时间,排查特定时间段的问题
- 日志等级:用于日志过滤和分析,快速定位错误等级
- 源代码文件名:定位出现错误的代码文件
- 源代码行号:精确到行号,直接跳转到出错的代码位置
- 线程 ID:用于过滤出错的线程,排查多线程并发问题
- 日志器名称:支持多日志器同时使用,区分不同模块的日志
- 日志主体消息:实际的日志内容
如果每次输出日志都要把这 7 个参数传来传去,不仅代码会变得非常冗长,而且很容易传错参数。把所有这些信息封装成一个LogMsg结构体,只需要传递一个结构体对象,就可以传递所有日志信息,非常简洁高效。
而且,日志消息一旦创建就不应该被修改,是一个不可变对象。这样在多线程环境下传递的时候,不需要加锁,天然线程安全。
2:LogMsg结构体字段设计
我设计的LogMsg结构体包含以下 7 个字段,每个字段都有明确的作用:
| 字段名 | 类型 | 作用 |
|---|---|---|
| _ctime | time_t | 日志产生的时间戳(秒级) |
| _level | LogLevel::value | 日志的等级 |
| _filename | std::string | 产生日志的代码文件名(纯文件名) |
| _line | size_t | 产生日志的代码行号 |
| _tid | std::thread::id | 产生日志的线程 ID |
| _logger | std::string | 产生日志的日志器名称 |
| _payload | std::string | 日志的实际内容 |
3:构造函数设计
构造函数是LogMsg结构体的核心,我在这里做了很多自动处理的工作,尽量减少用户的输入:
- 自动获取时间戳:调用
util::Date::GetTime()获取当前时间,不需要用户手动传入 - 自动获取线程 ID:调用
std::this_thread::get_id()获取当前线程 ID,不需要用户手动传入 - 参数顺序优化:把最常变化的参数放在前面,不常变化的放在后面,方便用户调用
4:完整代码实现
#pragma once
#include "level.hpp"
#include "util.hpp"
#include <iostream>
#include <string>
#include <thread>
namespace my_log {
/**
* @brief 日志消息结构体
* 封装了一条完整日志的所有元信息和内容
* 一旦创建即为不可变对象,天然线程安全
*/
struct LogMsg {
time_t _ctime; // 日志产生时间戳(秒级)
LogLevel::value _level; // 日志等级
std::string _filename; // 源代码文件名(纯文件名)
size_t _line; // 源代码行号
std::thread::id _tid; // 产生日志的线程ID
std::string _logger; // 产生日志的日志器名称
std::string _payload; // 日志实际内容
/**
* @brief 构造函数
* @param level 日志等级
* @param line 代码行号(由__LINE__宏传入)
* @param filename 源代码文件名(纯文件名,由外部接口预处理后传入)
* @param logger 日志器名称
* @param msg 日志实际内容
* @note 时间戳和线程ID会自动获取,不需要手动传入
*/
LogMsg(
LogLevel::value level,
size_t line,
const std::string filename,
const std::string logger,
const std::string msg)
:_ctime(util::Date::GetTime()),
_level(level),
_filename(filename),
_line(line),
_tid(std::this_thread::get_id()),
_logger(logger),
_payload(msg){ }
};
}
设计思考
- 自动获取线程 ID:这是我这次新增的一个非常重要的字段。在多线程程序中,很多问题都是并发导致的,有了线程 ID,我们可以很方便地过滤出某个特定线程的所有日志,快速定位并发问题
- 初始化列表:所有字段都在初始化列表中初始化,比在构造函数体内赋值效率更高
- 不可变对象:所有字段都是 public 的,但一旦构造完成就不应该被修改。我没有把它们设为 private,是因为后续格式化模块需要直接访问这些字段,设为 public 更方便,而且日志消息只是一个数据载体,没有业务逻辑
4:测试代码(AI生成)
#include "message.hpp"
#include <iostream>
int main()
{
std::cout << "==================== 日志等级与消息测试 ====================\n" << std::endl;
// 1. 测试日志等级转换
std::cout << "1. 日志等级转换测试:" << std::endl;
std::cout << " UNKNOW -> " << my_log::LogLevel::ToString(my_log::LogLevel::value::UNKNOW) << std::endl;
std::cout << " DEBUG -> " << my_log::LogLevel::ToString(my_log::LogLevel::value::DEBUG) << std::endl;
std::cout << " INFO -> " << my_log::LogLevel::ToString(my_log::LogLevel::value::INFO) << std::endl;
std::cout << " WARN -> " << my_log::LogLevel::ToString(my_log::LogLevel::value::WARN) << std::endl;
std::cout << " ERR -> " << my_log::LogLevel::ToString(my_log::LogLevel::value::ERR) << std::endl;
std::cout << " FATAL -> " << my_log::LogLevel::ToString(my_log::LogLevel::value::FATAL) << std::endl;
std::cout << " OFF -> " << my_log::LogLevel::ToString(my_log::LogLevel::value::OFF) << std::endl;
std::cout << " ✅ 日志等级转换正常\n" << std::endl;
// 2. 测试日志消息封装
std::cout << "2. 日志消息封装测试:" << std::endl;
my_log::LogMsg msg(
my_log::LogLevel::value::INFO,
__LINE__,
"test_message.cpp",
"root_logger",
"这是一条测试日志"
);
std::cout << " 时间戳:" << msg._ctime << std::endl;
std::cout << " 等级:" << my_log::LogLevel::ToString(msg._level) << std::endl;
std::cout << " 文件名:" << msg._filename << std::endl;
std::cout << " 行号:" << msg._line << std::endl;
std::cout << " 线程ID:" << msg._tid << std::endl;
std::cout << " 日志器:" << msg._logger << std::endl;
std::cout << " 内容:" << msg._payload << std::endl;
std::cout << " ✅ 日志消息封装正常\n" << std::endl;
// 3. 测试多线程日志消息
std::cout << "3. 多线程日志消息测试:" << std::endl;
std::thread t1([](){
my_log::LogMsg msg(
my_log::LogLevel::value::DEBUG,
__LINE__,
"test_message.cpp",
"thread_logger",
"线程1的日志消息"
);
std::cout << " 线程1 ID:" << msg._tid << std::endl;
});
std::thread t2([](){
my_log::LogMsg msg(
my_log::LogLevel::value::DEBUG,
__LINE__,
"test_message.cpp",
"thread_logger",
"线程2的日志消息"
);
std::cout << " 线程2 ID:" << msg._tid << std::endl;
});
t1.join();
t2.join();
std::cout << " ✅ 多线程日志消息正常\n" << std::endl;
std::cout << "==================== 所有测试通过 ====================" << std::endl;
return 0;
}
5:总结
完成的工作
- 设计并实现了日志等级模块
level.hpp,采用类嵌套枚举的设计,定义了七级日志等级 - 实现了日志等级转字符串函数
ToString,返回 const char * 字面量,效率更高 - 设计并实现了日志消息结构体
LogMsg,封装了 7 个日志元信息字段 - 在
LogMsg构造函数中自动获取时间戳和线程 ID,减少用户输入 - 编写了全面的测试用例,包括单线程和多线程场景
- 在 Windows 和 Linux 两个平台完成了跨平台测试
更多推荐


所有评论(0)