日志系统优化文章的第二篇,第一篇为:Logger 类的实现与改进 ,部分已在上一篇博客介绍过的内容,将不再重述。

此版本优化后,打印日志如下:

image.png

既然是在做优化,那么就应该先思考一个优秀的日志类需要哪些模块和功能:

  1. 基本功能:
    1. 日志级别:支持多种日志级别(例如 DEBUG、INFO、WARNING、ERROR、CRITICAL),以便根据重要性记录不同的日志信息。
    2. 日志消息格式化:支持自定义日志消息格式,包括时间戳、日志级别、消息内容、文件名、行号等。
    3. 输出目标:支持将日志输出到多种目标,例如控制台、文件、远程服务器(如Syslog、HTTP端点)等。
  2. 高级功能:
    1. 日志轮转:支持日志文件的轮转(按大小、按时间),以防止单个日志文件过大。
    2. 异步日志:支持异步记录日志,以提高性能,避免日志记录影响主线程的执行。
    3. 日志过滤:支持根据日志级别、模块、关键词等进行日志过滤,记录特定的日志信息。
    4. 上下文信息:支持记录上下文信息(例如请求ID、用户ID),便于日志的关联和追踪。
  3. 性能和安全
    1. 高性能:优化日志记录性能,确保在高并发环境下高效运行。
    2. 安全性:支持日志加密和敏感信息屏蔽,确保日志信息安全。
  4. 配置管理
  5. 扩展性
  6. 监控和分析
  7. 兼容性和标准化

后四点在我们的项目暂且不谈,主要实现完善前三点。

日志级别

日志级别应该是一个人是日类中必要的东西,但是在我初步实现的日志类中,并没有实现这一功能,算是一个很大的败笔了。在华为的面试的手势环节,有一个很重要的知识点就是与这一部分相关。

日志级别用于控制记录日志的详细程度和重要性,常见的级别有DEBUG、INFO、WARNING、ERROR等。在代码中,这些级别是通过LogLevel枚举定义的,可能包含DEBUGINFOWARNINGERROR等值。,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum class LogLevel {  
DEBUG,
INFO,
WARNING,
ERROR
};

// 使用示例
LogLevel level = LogLevel::ERROR;

if(currentLevel > LogLevel::DEBUG) {
// do sonething
}

enum 枚举类型

关于 enum 枚举类型的使用可以参考这一篇文章:C++ 枚举类型详解

需要补充的是,在 C++11 及更高的版本,引入了 enum class,提供了更强的类型安全性,避免了枚举值的隐式转换,也就是上面代码的写法。
其优势在于;

  1. 作用域更明确: 枚举成员必须通过枚举类来访问
  2. 类型安全: 不同的枚举类不会相互混淆

隐式转换:指编译器在需要时自动将一种数据类型转换为另一种数据类型。

在示例代码中,有一条语句看起来很迷惑:currentLevel > LogLevel::DEBUG
事实上,在 C++ 中,枚举成员实际上是具有整数值的枚举类型。因此,枚举成员之间的比较操作是可以进行的,因为它们在底层表示为整数。

底层整数表示:每个枚举成员在定义时会被分配一个整数值,从 0 开始递增(除非显式指定)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum class LogLevel {  
DEBUG, // 0
INFO, // 1
WARNING,// 2
ERROR // 3
};

// 使用示例
LogLevel level = LogLevel::ERROR;

if(currentLevel > LogLevel::DEBUG) {
// do sonething
}

而同时,也重载了相关的比较运算符用于对枚举类型进行比较,当然处于类型安全的考虑,当然要同一枚举类型才可以比较

日志颜色

日志的颜色功能主要用于在控制台输出时,以不同的颜色区分日志级别,使得日志信息更加直观。颜色设置是通过ANSI转义序列实现的。这些序列在支持ANSI颜色的终端(如Linux终端、一些Windows终端等)中能够正确显示带颜色的文本。

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
QString Logger::logLevelToString(LogLevel level, bool useColor) {
if (useColor) {
switch (level) {
case LogLevel::DEBUG: return "\033[36mDEBUG\033[0m"; // 青色
case LogLevel::INFO: return "\033[37mINFO\033[0m"; // 白色
case LogLevel::WARNING: return "\033[33mWARNING\033[0m"; // 黄色
case LogLevel::ERROR: return "\033[31mERROR\033[0m"; // 红色
default: return "\033[37mUNKNOWN\033[0m"; // 默认白色
}
} else {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
}

消息格式化

为了提高日志的可读性,日志消息需要经过格式化处理,包含多种信息,例如时间、线程ID、日志级别、源文件和行号等。格式化后的日志示例如下:

1
[INFO] [线程ID: 1234] This is an info message [source.cpp:42 - main()]

在代码中,格式化是通过log()方法实现的:

1
2
3
4
5
6
QString logMessage = QString("[%1] [线程ID: %2] %3%4 %5")
.arg(logLevelToString(level, false)) // 文件日志不带颜色
.arg(threadId)
.arg(identifierPart)
.arg(message)
.arg(sourceInfo);

这段代码生成的日志信息中包含了:

  • 日志级别(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
2
3
4
5
6
7
8
9
10
11
12
#define LOG_ERROR(message) \
Logger::instance().log(message, LogLevel::ERROR, __FILE__, __LINE__, __FUNCTION__)

#define LOG_WARNING(message) \
Logger::instance().log(message, LogLevel::WARNING, __FILE__, __LINE__, __FUNCTION__)

#define LOG_INFO(message) \
Logger::instance().log(message, LogLevel::INFO, __FILE__, __LINE__, __FUNCTION__)

#define LOG_DEBUG(message) \
Logger::instance().log(message, LogLevel::DEBUG, __FILE__, __LINE__, __FUNCTION__)

这些宏在调用时会自动将当前的文件名(__FILE__)、行号(__LINE__)和函数名(__FUNCTION__)传递给log()方法。例如:

1
LOG_ERROR("An error occurred");

实际上等同于:

1
Logger::instance().log("An error occurred", LogLevel::ERROR, "source.cpp", 42, "main");

宏的使用大大简化了调用过程,开发者不需要手动传入文件名、行号和函数名,可以自动获取日志记录发生的位置。

输出目标多样化

目前就是一个判断,分别在控制台和 log 文件中输出,后续将继续完善。

安全性

这一部分在上一篇文章中详细解释过,通过QMutexQWaitCondition确保日志记录的线程安全性。

  • QMutexLocker 确保在访问共享资源(如日志队列)时加锁,以避免多个线程同时修改队列
  • QWaitCondition 用于在日志队列为空时,使日志线程进入等待状态,直到有新日志消息时被唤醒
  • 这样设计可以有效避免线程资源浪费,当有新日志时立即唤醒线程进行处理。

© 2024 Montee | Powered by Hexo | Theme stellar


Static Badge