为了深入理解 spdlog 的设计和实现,让我们从最基础的日志记录方式出发,逐步引入 spdlog,探讨其核心代码及设计决策背后的考虑。
从一个简单的例子开始
在最简单的形式中,日志记录可能仅仅是将信息输出到控制台或写入到一个文本文件中。例如,一个基础的日志记录函数可能看起来像这样:
#include <iostream>
#include <fstream>
#include <string>
void log_message(const std::string& message) {
std::ofstream log_file("log.txt", std::ios::app);
if (log_file.is_open()) {
log_file << message << std::endl;
log_file.close();
} else {
std::cerr << "Failed to open log file!" << std::endl;
}
}
// 使用示例
int main() {
log_message("This is a message.");
return 0;
}
这个函数简单地将传入的消息追加到一个名为log.txt
的文件中。尽管这种方法很直接,但在实际应用中,它有许多限制和不足,例如性能问题、缺乏多级别日志控制、线程安全问题等。因此,在实际应用中,我们需要考虑更多因素来优化日志记录功能,那么,如何确定需要考虑哪些因素呢?
日志设计中我该考虑哪些事情?
在设计和实现日志库时,确实有一些通用的原则和最佳实践可以遵循。这些原则帮助确保日志系统既灵活又高效,能够满足不同应用程序的需求。以下是一些关键方面,通常被认为是设计日志库时需要考虑的:
1. 日志级别
日志级别允许开发者根据消息的重要性对日志进行分类。常见的日志级别包括:
- DEBUG: 用于调试信息,通常在生产环境中被禁用。
- INFO: 用于常规操作信息,如程序启动或正常操作。
- WARNING: 用于警告信息,可能会指出潜在的问题,但不影响系统运行。
- ERROR: 用于错误信息,通常表示操作失败或异常。
- FATAL: 用于致命错误信息,这些错误通常会导致程序终止。
2. 日志格式化
日志格式化是指日志消息的结构和外观。良好的格式化使得日志易于阅读和分析。格式化可能包括:
- 时间戳
- 日志级别
- 消息内容
- 日志发生的源代码文件和行号
- 线程或进程信息
3. 性能
日志记录不应显著影响应用程序的性能。关键的性能因素包括:
- 异步日志记录:将日志消息快速记录到内存中,然后由后台线程负责将其写入存储系统。
- 资源管理:有效管理文件句柄和内存使用。
- 批处理和缓冲:减少对存储系统的访问次数。
4. 线程安全
在多线程环境中,日志库需要保证线程安全,确保日志消息不会因为并发访问而损坏。
5. 配置和定制
提供灵活的配置选项,允许开发者根据需要调整日志级别、输出格式和目的地(如控制台、文件、网络等)。
6. 错误处理
日志库应该能够优雅地处理错误,比如文件写入失败,而不是导致应用程序崩溃。
7. 跨平台支持
考虑到不同的操作系统和环境,日志库应该能够在多个平台上运行,无需或仅需少量修改。
8. 扩展性和插件支持
允许通过插件或扩展来增加新的日志目的地、格式化选项或其他特性。
这些准则能够为我们在设计日志库时提供指导,帮助我们发现问题并进行改进,当然我们也得根据实际情况做出灵活调整,不一定要完全按照这些准则来。
简单改改
给日志分级
首先,我们可以改造我们的基础日志记录函数,让它支持不同的日志级别。这是一个简单的改造,但它让我们能够根据消息的重要性来区分日志输出,这是日志系统的一个基本需求。
#include <iostream>
#include <fstream>
#include <string>
enum LogLevel {
INFO,
WARNING,
ERROR
};
void log_message(const std::string& message, LogLevel level) {
std::ofstream log_file("log.txt", std::ios::app);
if (log_file.is_open()) {
switch(level) {
case INFO:
log_file << "[INFO] ";
break;
case WARNING:
log_file << "[WARNING] ";
break;
case ERROR:
log_file << "[ERROR] ";
break;
}
log_file << message << std::endl;
log_file.close();
} else {
std::cerr << "Failed to open log file!" << std::endl;
}
}
// 使用示例
int main() {
log_message("This is a info message.", LogLevel::INFO);
return 0;
}
> [INFO] This is a info message.
我们看看 spdlog
的的日志等级,以及围绕日志等级,spdlog
做了哪些事
日志级别枚举
spdlog 定义了一个名为level::level_enum
的枚举,用于表示不同的日志级别。这个枚举包括了trace
, debug
, info
, warn
, err
, critical
, off
等级别,涵盖了从最详细的跟踪信息到关键错误和关闭日志记录的所有级别。
namespace spdlog {
namespace level {
enum level_enum
{
trace = 0,
debug,
info,
warn,
err,
critical,
off,
n_levels
};
}
}
日志级别设置和检查
每个 logger 对象都有一个与之关联的当前日志级别,这决定了该 logger 记录哪些级别的消息。spdlog 提供了方法来设置和获取 logger 的当前日志级别。
在spdlog::logger
类中,有关设置和获取日志级别的方法,类似于::
void set_level(spdlog::level::level_enum log_level);
spdlog::level::level_enum level() const;
这些方法允许用户动态改变 logger 的日志级别,以便在不同的情况下记录不同级别的信息。
日志记录函数
spdlog 的每个 logger 对象提供了一系列以日志级别命名的函数,如info()
, debug()
, warn()
等,这些函数使得记录不同级别的日志变得非常简单。
这些函数的实现通常会检查当前的日志级别,只有当消息的级别高于或等于 logger 的当前级别时,消息才会被记录。这是通过内部检查实现的,类似于:
void info(const char* fmt, const Args &... args)
{
if (level() <= spdlog::level::info)
{
log(spdlog::level::info, fmt, args...);
}
}
格式化日志
接下来,我们可以引入简单的日志格式化。日志消息的格式化允许我们以一种更可读和有组织的方式记录信息,比如包含时间戳、日志级别和实际消息。
#include <iostream>
#include <fstream>
#include <string>
#include <ctime>
void log_message(const std::string& message, LogLevel level) {
std::ofstream log_file("log.txt", std::ios::app);
if (log_file.is_open()) {
// 获取当前时间
std::time_t now = std::time(nullptr);
char* dt = std::ctime(&now);
switch(level) {
case INFO:
log_file << "[" << dt << " INFO] ";
break;
case WARNING:
log_file << "[" << dt << " WARNING] ";
break;
case ERROR:
log_file << "[" << dt << " ERROR] ";
break;
}
log_file << message << std::endl;
log_file.close();
} else {
std::cerr << "Failed to open log file!" << std::endl;
}
}
日志格式化是 spdlog
提供的另一个强大功能,允许开发者自定义日志消息的格式,包括日期、时间、日志级别和消息内容等。spdlog
在日志格式化方面的实现主要依赖于fmt
库,这是一个现代 C++
的格式化库,提供了类似于 Python
的格式化语法,使得文本格式化既快速又安全。
模式字符串
在spdlog
中,你可以通过设置模式字符串(pattern string)来自定义日志的输出格式。这个模式字符串使用特殊的占位符,每个占位符都代表了日志消息中的一部分,例如时间、日志级别、消息文本等。
例如,一个常见的模式字符串可能是这样的:
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v");
在这个模式中:
%Y-%m-%d %H:%M:%S
代表日期和时间。%l
代表日志级别。%v
代表实际的日志消息文本。
当 spdlog 处理一个日志消息时,它会根据设置的模式字符串来构造最终的输出字符串。这个过程涉及到解析模式字符串,匹配占位符,并用实际的日志数据(如时间、级别、消息等)替换这些占位符,最后将格式化后的字符串输出到日志的目标位置(如控制台、文件等)。
- 时间相关:
c
、C
、Y
、D
、m
、d
、H
、I
、M
、S
、e
、f
、F
、E
、p
、r
、R
、T (X)
- 日期时间相关:星期几(a, A)、月份名称(b/h, B)、时区(z)
- 线程相关:线程 ID(t)
- 日志级别相关:日志级别完整名称(l)、日志级别简短名称(L)
- 消息文本相关:消息文本(v)
- 进程相关:进程 ID(P)
- 源代码位置相关:源代码位置(@)、源文件名简短和完整形式(s, g)、源代码行号 (#)和函数名 (!)
- 时间间隔相关:自上一条日志消息以来的时间间隔,以纳秒 (u)、微秒 (i)、毫秒 (o)和秒 (O)为单位
- 其他:文字百分号 (%)字符
这些类型提供了丰富的选项,可以用于构建具有不同格式要求的日志消息。此外 spdlog
还支持用户自定义处理逻辑来处理特定的标志,从而实现更灵活和定制化的日志消息格式化功能。
假设我们有一个自定义标志%U
,表示当前用户的用户名。我们想要在日志消息中包含当前用户的用户名。我们可以按照以下步骤实现:
#include "spdlog/spdlog.h"
#include "spdlog/fmt/ostr.h"
#include "spdlog/fmt/fmt.h"
#include "spdlog/pattern_formatter.h"
#include "spdlog/sinks/basic_file_sink.h"
// 示例函数,获取当前用户名
std::string getCurrentUserName() {
// 这里应根据你的平台编写相应的代码
// 例如,在UNIX系统上,可以是 return getlogin();
return "example_user";
}
// 自定义标志处理器
class CustomFlagFormatter : public spdlog::custom_flag_formatter {
public:
// 当遇到自定义标志时调用
void format(const spdlog::details::log_msg&, const std::tm&, spdlog::memory_buf_t& dest) override {
std::string username = getCurrentUserName();
dest.append(username.data(), username.data() + username.size());
}
// 创建一个自定义标志处理器的副本
std::unique_ptr<custom_flag_formatter> clone() const override {
return spdlog::details::make_unique<CustomFlagFormatter>();
}
};
int main() {
// 创建一个控制台logger
auto logger = spdlog::basic_logger_mt("file_logger", "logs/basic-log.txt");
// 获取默认的格式化器
auto formatter = std::make_unique<spdlog::pattern_formatter>();
// 添加自定义标志处理器
formatter->add_flag<CustomFlagFormatter>('U').set_pattern("[%U] %v");
// 设置格式化器
logger->set_formatter(std::move(formatter));
// 测试日志
logger->info("This is a test log message");
return 0;
}
> [example_user] This is a test log message
通过以上步骤,我们成功地实现了在日志消息中包含当前用户的用户名。这个例子展示了如何使用自定义标志处理器来扩展 spdlog 的功能,以满足特定需求。
使用fmt
库实现内容格式化
spdlog 利用fmt
库来执行实际的格式化操作。fmt
库支持类型安全的字符串格式化,这意味着它可以在编译时检查格式字符串是否与提供的参数类型匹配。这种类型安全性减少了运行时错误,并提高了代码的稳定性和安全性。
内部定义的模板类:
template <typename... Args>
inline void info(format_string_t<Args...> fmt, Args &&...args) {
default_logger_raw()->info(fmt, std::forward<Args>(args)...);
}
例如,当你使用 spdlog 记录一个带有整数和字符串的日志消息时,如下:
spdlog::info("User {} has {} messages", "Alice", 10);
在这个例子中,fmt
库负责将"Alice"
和10
这两个参数按照类型安全的方式插入到格式化字符串中,生成最终的日志消息。
改造下我们的代码
#include <iostream>
#include <fstream>
#include <string>
#include <ctime>
#include <fmt/core.h> // 包含 fmt 库的核心功能
enum LogLevel {
INFO,
WARNING,
ERROR
};
// 使用模板和参数包来支持格式化的参数
template <typename... Args>
void log_message(LogLevel level, const std::string& format, Args... args) {
std::ofstream log_file("log.txt", std::ios::app);
if (log_file.is_open()) {
// 获取当前时间
std::time_t now = std::time(nullptr);
std::string dt = std::ctime(&now);
dt.pop_back(); // 移除换行符
std::string level_str;
switch(level) {
case INFO:
level_str = "INFO";
break;
case WARNING:
level_str = "WARNING";
break;
case ERROR:
level_str = "ERROR";
break;
}
// 使用 fmt::format 格式化日志消息,包括传入的参数
auto formatted_message = fmt::format(format, args...);
auto log_entry = fmt::format("[{} {}] {}\n", dt, level_str, formatted_message);
log_file << log_entry;
log_file.close();
} else {
std::cerr << "Failed to open log file!" << std::endl;
}
}
// 使用示例
int main() {
// 使用 INFO 级别记录一条简单消息
log_message(INFO, "This is an info message.");
// 使用 ERROR 级别记录一条格式化消息
log_message(ERROR, "Error code: {}. Error message: {}", 404, "Not Found");
return 0;
}
> [Sat Mar 2 14:59:10 2024 ERROR] Error code: 404. Error message: Not Found
文件读写优化
到目前为止,我们的日志系统每次记录日志时都打开和关闭文件,这在性能上是非常低效的。我们可以通过保持日志文件在应用程序运行期间一直打开来改进这个问题。
#include <iostream>
#include <fstream>
#include <string>
#include <ctime>
#include <fmt/core.h> // 包含 fmt 库的核心功能
static std::ofstream log_file("log.txt", std::ios::app); // 静态日志文件对象
enum LogLevel {
INFO,
WARNING,
ERROR
};
// 日志级别到字符串的转换
const char* toString(LogLevel level) {
switch(level) {
case INFO: return "INFO";
case WARNING: return "WARNING";
case ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
// 获取格式化的当前时间字符串
std::string currentDateTime() {
std::time_t now = std::time(nullptr);
std::string dt = std::ctime(&now);
dt.pop_back(); // 移除换行符
return dt;
}
// 使用模板和参数包来支持格式化的参数
template <typename... Args>
void log_message(LogLevel level, const std::string& format, Args... args) {
if (log_file.is_open()) {
auto log_entry = fmt::format("[{}] [{}] {}\n", currentDateTime(), toString(level), fmt::format(format, args...));
log_file << log_entry;
} else {
std::cerr << "Failed to open log file!" << std::endl;
}
}
int main() {
log_message(INFO, "This is an info message.");
log_message(ERROR, "Error code: {}. Error message: {}", 404, "Not Found");
return 0;
}
> [Mon Mar 4 13:47:15 2024] [INFO] This is an info message.
> [Mon Mar 4 13:47:15 2024] [ERROR] Error code: 404. Error message: Not Found