日志系统优化文章的第二篇,第一篇为:Logger 类的实现与改进 ,部分已在上一篇博客介绍过的内容,将不再重述。
此版本优化后,打印日志如下:
既然是在做优化,那么就应该先思考一个优秀的日志类需要哪些模块和功能:
- 基本功能:
- 日志级别:支持多种日志级别(例如 DEBUG、INFO、WARNING、ERROR、CRITICAL),以便根据重要性记录不同的日志信息。
- 日志消息格式化:支持自定义日志消息格式,包括时间戳、日志级别、消息内容、文件名、行号等。
- 输出目标:支持将日志输出到多种目标,例如控制台、文件、远程服务器(如Syslog、HTTP端点)等。
- 高级功能:
- 日志轮转:支持日志文件的轮转(按大小、按时间),以防止单个日志文件过大。
- 异步日志:支持异步记录日志,以提高性能,避免日志记录影响主线程的执行。
- 日志过滤:支持根据日志级别、模块、关键词等进行日志过滤,记录特定的日志信息。
- 上下文信息:支持记录上下文信息(例如请求ID、用户ID),便于日志的关联和追踪。
- 性能和安全
- 高性能:优化日志记录性能,确保在高并发环境下高效运行。
- 安全性:支持日志加密和敏感信息屏蔽,确保日志信息安全。
- 配置管理
- 扩展性
- 监控和分析
- 兼容性和标准化
后四点在我们的项目暂且不谈,主要实现完善前三点。
日志级别
日志级别应该是一个人是日类中必要的东西,但是在我初步实现的日志类中,并没有实现这一功能,算是一个很大的败笔了。在华为的面试的手势环节,有一个很重要的知识点就是与这一部分相关。
日志级别用于控制记录日志的详细程度和重要性,常见的级别有DEBUG、INFO、WARNING、ERROR等。在代码中,这些级别是通过LogLevel
枚举定义的,可能包含DEBUG
、INFO
、WARNING
、ERROR
等值。,具体代码如下:
1 | enum class LogLevel { |
enum 枚举类型
关于 enum 枚举类型的使用可以参考这一篇文章:C++ 枚举类型详解
需要补充的是,在 C++11 及更高的版本,引入了 enum class,提供了更强的类型安全性,避免了枚举值的隐式转换,也就是上面代码的写法。
其优势在于;
- 作用域更明确: 枚举成员必须通过枚举类来访问
- 类型安全: 不同的枚举类不会相互混淆
隐式转换:指编译器在需要时自动将一种数据类型转换为另一种数据类型。
在示例代码中,有一条语句看起来很迷惑:currentLevel > LogLevel::DEBUG
事实上,在 C++ 中,枚举成员实际上是具有整数值的枚举类型。因此,枚举成员之间的比较操作是可以进行的,因为它们在底层表示为整数。
底层整数表示:每个枚举成员在定义时会被分配一个整数值,从 0 开始递增(除非显式指定)。例如:
1 | enum class LogLevel { |
而同时,也重载了相关的比较运算符用于对枚举类型进行比较,当然处于类型安全的考虑,当然要同一枚举类型才可以比较。
日志颜色
日志的颜色功能主要用于在控制台输出时,以不同的颜色区分日志级别,使得日志信息更加直观。颜色设置是通过ANSI转义序列实现的。这些序列在支持ANSI颜色的终端(如Linux终端、一些Windows终端等)中能够正确显示带颜色的文本。
核心代码如下:
1 | QString Logger::logLevelToString(LogLevel level, bool useColor) { |
消息格式化
为了提高日志的可读性,日志消息需要经过格式化处理,包含多种信息,例如时间、线程ID、日志级别、源文件和行号等。格式化后的日志示例如下:
1 | [INFO] [线程ID: 1234] This is an info message [source.cpp:42 - main()] |
在代码中,格式化是通过log()
方法实现的:
1 | QString logMessage = QString("[%1] [线程ID: %2] %3%4 %5") |
这段代码生成的日志信息中包含了:
- 日志级别(
INFO
) - 线程ID(
1234
) - 标识符(如果有)
- 实际的日志消息(
This is an info message
) - 源文件信息(
source.cpp:42 - main()
)
arg() 函数 类似于 C 中的 printf 用法
上下文信息
上下文信息这个词在高中做阅读理解时,应该是经常遇到的,老师会说“根据上下文找到答案”。在日志系统中,也是差不多的意思,我们通过上下文信息,可以清楚地知道日志是在哪个线程、哪个位置产生的。
在本项目中,主要由以下几部分组成:
- 线程ID:通过
getCurrentThreadId()
获取当前线程ID,并在日志中显示。这样,在调试多线程应用时,我们可以轻松区分出不同线程的日志。 - 源信息:包括文件名、行号和函数名,这些信息在
log()
方法中通过参数传递,并记录在日志消息中,用于精确定位日志发生的位置。 - 标识符:
identifier
是一个可选的标识符,可以通过它为某些特定上下文添加标签,方便后续的日志查询和分析。
如函数所示 void Logger::log(const QString &message, LogLevel level, const char* file, int line, const char* function)
,传入了源文件名、行号与函数名。
具体的获取方式如下:
1 |
这些宏在调用时会自动将当前的文件名(__FILE__
)、行号(__LINE__
)和函数名(__FUNCTION__
)传递给log()
方法。例如:
1 | LOG_ERROR("An error occurred"); |
实际上等同于:
1 | Logger::instance().log("An error occurred", LogLevel::ERROR, "source.cpp", 42, "main"); |
宏的使用大大简化了调用过程,开发者不需要手动传入文件名、行号和函数名,可以自动获取日志记录发生的位置。
输出目标多样化
目前就是一个判断,分别在控制台和 log 文件中输出,后续将继续完善。
安全性
这一部分在上一篇文章中详细解释过,通过QMutex
和QWaitCondition
确保日志记录的线程安全性。
QMutexLocker
确保在访问共享资源(如日志队列)时加锁,以避免多个线程同时修改队列QWaitCondition
用于在日志队列为空时,使日志线程进入等待状态,直到有新日志消息时被唤醒- 这样设计可以有效避免线程资源浪费,当有新日志时立即唤醒线程进行处理。