
大家好,我是Halcyon.平安
欢迎文末添加好友交流,共同进步!

一、模块概述
日志模块是 SDK 中最底层的模块,被所有其他模块依赖。本项目使用 spdlog ------ 一个高性能的 C++ 日志库,支持异步写入、格式化输出、日志级别过滤等功能。
本模块包含两个文件:

| 文件 | 功能说明 |
|---|---|
| include/util/myLog.h | Logger 类声明 + 6 个日志宏(TRACE ~ CRIT) |
| src/util/myLog.cpp | Logger 类实现(初始化 spdlog、双检锁单例) |
二、Logger 类 --- 声明(myLog.h)
2.1 完整源码
cpp
#pragma once
#include <mutex>
#include <spdlog/logger.h>
#include <spdlog/spdlog.h>
namespace mylog{
class Logger{
public:
static void initLogger(const std::string& loggerName,
const std::string& loggerFile,
spdlog::level::level_enum logLevel = spdlog::level::info);
static std::shared_ptr<spdlog::logger> getLogger();
private:
Logger();
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
static std::shared_ptr<spdlog::logger> _logger;
static std::mutex _mutex;
};
} // end mylog
2.2 逐段解析
单例模式设计
cpp
class Logger{
// ...
private:
Logger(); // 私有构造,禁止外部实例化
Logger(const Logger&) = delete; // 禁止拷贝构造
Logger& operator=(const Logger&) = delete; // 禁止赋值
private:
static std::shared_ptr<spdlog::logger> _logger; // 唯一的日志器实例
static std::mutex _mutex; // 用于线程安全
};
Logger 采用单例模式------整个程序只需要一个日志器。
实现要点:
- 私有构造函数 :外部无法
new Logger()创建对象
比如:Logger log; // ❌ 不允许
- 删除拷贝和赋值:防止通过拷贝产生多个实例


delete表示:禁止这个函数使用。都是为了防止对象被复制,保证唯一实例。
- 静态成员
_logger:持有 spdlog 的日志器对象,全局唯一

static属于类本身而不是某个对象,此时是整个类只有这一份,所有对象共享!
- 静态成员
_mutex:配合双检锁保证线程安全,同一时刻:只能一个线程写。
所有方法都是
static的,通过Logger::initLogger(...)和Logger::getLogger()直接调用,无需创建 Logger 对象。
接口说明
cpp
// 初始化日志器(只能调用一次)
static void initLogger(const std::string& loggerName,
const std::string& loggerFile,
spdlog::level::level_enum logLevel = spdlog::level::info);
// 获取日志器实例
static std::shared_ptr<spdlog::logger> getLogger();
| 参数 | 说明 |
|---|---|
| loggerName | 日志器名称,会出现在日志输出中,如 "ChatServer" |
| loggerFile | 日志输出目标,传 "stdout" 输出到控制台,传文件路径输出到文件 |
| logLevel | 日志级别过滤,默认 info,低于该级别的日志不会被输出 |
三、Logger 类 --- 实现(myLog.cpp)
3.1 完整源码
cpp
#include "../../include/util/myLog.h"
#include <memory> //智能指针
#include <spdlog/spdlog.h> //spdlog核心库
#include <spdlog/sinks/basic_file_sink.h> //文件输出功能
#include <spdlog/sinks/stdout_color_sinks.h> //彩色终端输出
#include <spdlog/async.h> //异步日志支持
namespace mylog{
std::shared_ptr<spdlog::logger> Logger::_logger = nullptr;
std::mutex Logger::_mutex;
Logger::Logger()
{}
void Logger::initLogger(const std::string& loggerName,
const std::string& loggerFile,
spdlog::level::level_enum logLevel){
if(nullptr == _logger){
std::lock_guard<std::mutex> lock(_mutex);
if(nullptr == _logger){
// 日志级别 ≥ logLevel 时立即刷新
spdlog::flush_on(logLevel);
// 启用异步日志,队列大小 32768,后台线程数 1
spdlog::init_thread_pool(32768, 1);
if("stdout" == loggerFile){
// 输出到控制台(带颜色)
_logger = spdlog::stdout_color_mt(loggerName);
}else{
// 输出到文件(异步)
_logger = spdlog::basic_logger_mt<spdlog::async_factory>(loggerName, loggerFile);
}
}
// 设置日志格式:[时分秒][日志器名][日志级别]消息内容
_logger->set_pattern("[%H:%M:%S][%n][%-7l]%v");
_logger->set_level(logLevel);
}
}
std::shared_ptr<spdlog::logger> Logger::getLogger(){
return _logger;
}
} // end mylog
3.2 逐段解析
静态成员初始化
cpp
std::shared_ptr<spdlog::logger> Logger::_logger = nullptr;
std::mutex Logger::_mutex;
类的静态成员变量需要在类外定义和初始化。_logger 初始为 nullptr,在 initLogger 中才创建真正的 spdlog 日志器。
双检锁(Double-Checked Locking)
cpp
void Logger::initLogger(...)
{
if(nullptr == _logger)
{ // 第一次检查(不加锁,快速路径)
std::lock_guard<std::mutex> lock(_mutex);
if(nullptr == _logger)
{
// 第二次检查(加锁后确认)
// ... 创建日志器
}
// ... 设置格式和级别
}
}
这是经典的双检锁单例模式,解决两个问题:
- 线程安全 :多线程可能同时调用
initLogger,需要加锁保护 - 性能:第一次检查不加锁,日志器已创建后直接返回,避免每次加锁的开销
线程A: 第一次检查 _logger == nullptr → 加锁 → 第二次检查 → 创建日志器 → 解锁
线程B: 第一次检查 _logger == nullptr → 等待锁 → 获得锁 → 第二次检查(已不为空)→ 跳过创建 → 解锁
线程C: 第一次检查 _logger != nullptr → 直接返回(不加锁)
异步日志
cpp
spdlog::init_thread_pool(32768, 1);
启用异步日志模式:
参数一:队列大小,参数二:后台线程数量
- 日志消息先写入一个大小为 32768 的队列
- 由 1 个后台线程负责将队列中的日志写入目标(控制台/文件)
- 调用日志的线程不会因 IO 操作而阻塞,提高性能
plain
业务线程:INFO("xxx") → 写入队列 → 立即返回
↓
后台线程: 从队列取出 → 写入控制台/文件
日志输出目标选择
cpp
if("stdout" == loggerFile){
_logger = spdlog::stdout_color_mt(loggerName);
}else{
_logger = spdlog::basic_logger_mt<spdlog::async_factory>(loggerName, loggerFile);
}
根据 loggerFile 参数决定日志输出到哪里:
| 参数值 | 使用方式 | 说明 |
|---|---|---|
| "stdout" | spdlog::stdout_color_mt() | 输出到控制台,带颜色 |
| 文件路径字符串 | spdlog::basic_logger_mt<async_factory>() | 异步写入文件 |
stdout_color_mt创建一个带颜色输出的控制台日志器(mt表示 multi-thread 线程安全)basic_logger_mt<spdlog::async_factory>创建异步写入文件的日志器
日志格式设置
cpp
_logger->set_pattern("[%H:%M:%S][%n][%-7l]%v");
_logger->set_level(logLevel);
格式化占位符说明:
| 占位符 | 含义 | 输出示例 |
|---|---|---|
| %H:%M:%S | 时:分:秒 | 9:04:03 |
| %n | 日志器名称 | ChatServer |
| %-7l | 日志级别,左对齐,宽度 7 | info |
| %v | 实际日志消息 | init model success |
输出效果:
plain
[09:04:03][ChatServer][info ][ DataManager.cpp:15] Database opened successfully: chat.db
[09:04:04][ChatServer][error ][ ChatGPTProvider.cpp:18] api_key not found
四、日志宏定义
4.1 六个日志宏
cpp
#define TRACE(format, ...) mylog::Logger::getLogger()->trace(std::string("[{:>10s}:{:<4d}]")+format, __FILE__, __LINE__, ##__VA_ARGS__)
#define DBG(format, ...) mylog::Logger::getLogger()->debug(std::string("[{:>10s}:{:<4d}]")+format, __FILE__, __LINE__, ##__VA_ARGS__)
#define INFO(format, ...) mylog::Logger::getLogger()->info(std::string("[{:>10s}:{:<4d}]")+format, __FILE__, __LINE__, ##__VA_ARGS__)
#define WARN(format, ...) mylog::Logger::getLogger()->warn(std::string("[{:>10s}:{:<4d}]")+format, __FILE__, __LINE__, ##__VA_ARGS__)
#define ERR(format, ...) mylog::Logger::getLogger()->error(std::string("[{:>10s}:{:<4d}]")+format, __FILE__, __LINE__, ##__VA_ARGS__)
#define CRIT(format, ...) mylog::Logger::getLogger()->critical(std::string("[{:>10s}:{:<4d}]")+format, __FILE__, __LINE__, ##__VA_ARGS__)
日志级别从低到高:
| 宏 | 级别 | 使用场景 |
|---|---|---|
| TRACE | trace | 最详细的追踪信息,通常调试时使用 |
| DBG | debug | 调试信息 |
| INFO | info | 常规运行信息(默认级别) |
| WARN | warning | 警告,不影响运行但需关注 |
| ERR | error | 错误,操作失败 |
| CRIT | critical | 严重错误,可能导致程序崩溃 |
4.2 宏的工作原理
在实际项目中,我们通常不会直接调用 spdlog 的 info()、debug() 等接口,而是会进一步封装成自己的日志宏,例如:
cpp
#define INFO(format, ...) \
mylog::Logger::getLogger()->info(
std::string("[{:>10s}:{:<4d}]")+format,
__FILE__,
__LINE__,
##__VA_ARGS__
)
这段代码第一次看可能会觉得特别复杂,但其实它本质上就是在帮我们"自动补全日志信息"。以后我们只需要简单写一句:
cpp
INFO("用户登录成功");
或者:
cpp
INFO("用户ID = {}", uid);
日志系统就会自动帮我们把"文件名"和"代码行号"也一起打印出来,比如:
bash
[main.cpp:35 ]用户ID = 1001
这样做的最大好处就是:后期排查 Bug 的时候,我们能立刻知道日志是从哪一行代码打印出来的,而不用全局搜索,大型项目里这个功能非常重要。
接下来我们拆开来看。首先:
cpp
#define INFO(format, ...)
这里的 #define 是 C/C++ 中的宏定义,本质上就是"文本替换"。编译之前,编译器会先把:
cpp
INFO("hello");
替换成:
cpp
mylog::Logger::getLogger()->info(...);
其中 format 表示格式字符串,而 ... 则表示"可变参数",也就是说参数数量不固定。比如下面这些写法都合法:
cpp
INFO("hello");
INFO("id = {}", id);
INFO("{} {}", a, b);
随后重点来了:
cpp
__FILE__
__LINE__
它们是 C++ 提供的预定义宏。__FILE__ 表示当前文件名,而 __LINE__ 表示当前代码所在行号,比如:
latex
main.cpp 35
因此日志系统就能自动知道:
latex
这是 main.cpp 第35行打印的日志
再来看这部分:
cpp
"[{:>10s}:{:<4d}]"
这里使用的是 fmt 风格格式化语法,因为 spdlog 底层实际上就是基于 fmt 库实现的。
其中:
cpp
{:>10s}
表示字符串右对齐,占 10 个字符宽度;而:
cpp
{:<4d}
表示整数左对齐,占 4 个字符宽度。
这样做是为了让日志输出更加整齐,例如:
latex
[ main.cpp:35 ]
整体看起来会非常规范。
最后:
cpp
##__VA_ARGS__
表示把用户传入的可变参数继续转发给 spdlog。例如:
cpp
INFO("uid = {}", uid);
最终会变成:
cpp
logger->info(
"[{:>10s}:{:<4d}]uid = {}",
__FILE__,
__LINE__,
uid
);
随后 spdlog 会自动把:
- 文件名填入第一个
{} - 行号填入第二个
{} - uid 填入最后一个
{}
最终生成完整日志。
实际上,像 glog、spdlog 等成熟日志库,底层都大量使用这种"宏 + 文件名 + 行号"的设计方式,因为日志系统最核心的目标就是:快速定位问题。
五、使用方式
5.1 初始化
在程序入口处调用一次:
cpp
#include <ai_chat_sdk/util/myLog.h>
int main() {
// 初始化日志:名称 "ChatServer",输出到控制台,INFO 级别
bite::Logger::initLogger("ChatServer", "stdout", spdlog::level::info);
// 之后在任意位置使用日志宏
INFO("服务器启动成功, 端口: {}", 8080);
ERR("连接模型失败: {}", "timeout");
return 0;
}
[14:25:31][ChatServer][info ][ main.cpp:8 ]服务器启动成功, 端口: 8080
[14:25:31][ChatServer][error ][ main.cpp:9 ]连接模型失败: timeout
5.2 在 SDK 各模块中的使用
日志宏在整个 SDK 中被广泛使用,以 LLMManager 为例:
cpp
bool LLMManager::registerProvider(const std::string& modelName, std::unique_ptr<LLMProvider> provider){
if(!provider){
ERR("cannot register nullptr provider, modelName = {}", modelName); // 错误日志
return false;
}
_providers[modelName] = std::move(provider);
INFO("register provider success, modelName = {}", modelName); // 信息日志
return true;
}
输出效果:
bash
[09:04:03][ChatServer][info ][ LLMManager.cpp:22] register provider success, modelName = deepseek-chat
[09:04:03][ChatServer][info ][ LLMManager.cpp:22] register provider success, modelName = gpt-4o-mini
[09:04:04][ChatServer][error ][ ChatGPTProvider.cpp:18] api_key not found
日志中的文件名和行号能帮助快速定位问题所在位置。
六、设计总结
myLog.h 其中包括:日志初始化、日志获取函数的声明;通过私有构造、禁止外界实例化、禁止拷贝与赋值,保证全局仅存在一个日志对象(单例模式);定义静态成员 _logger(日志器对象)与 _mutex(线程锁),实现全局共享;同时定义日志宏,实现"文件名 + 行号 + 格式化内容 + 可变参数"的自动拼接,最终通过Logger::getLogger()间接获取 _logger,并调用真正的 spdlog 日志接口。
cpp
Logger::getLogger()
myLog.cpp 主要负责:日志初始化与 getLogger() 函数的实现;创建 logger 对象;设置日志输出格式(时间戳、日志器名称、日志等级、日志消息);初始化异步线程池;以及控制日志输出到控制台或文件。
- 单例模式 + 双检锁:保证全局唯一日志器,同时兼顾线程安全与性能,避免重复创建 logger 对象
- 异步日志:基于 spdlog 的异步线程池机制,业务线程只负责将日志放入队列,后台线程负责真正的 IO 输出,从而避免日志阻塞业务线程
- 灵活输出:通过参数控制日志输出到控制台或文件,方便不同环境下使用
- 宏自动定位:自动记录日志来源文件与代码行号,无需手动填写
cpp
__FILE__
__LINE__
- fmt 风格格式化:底层基于 fmt 的
{}占位符格式化,相比传统printf风格更加安全、清晰、现代化 - 分层设计:整个日志系统被拆分为"日志正文"和"日志元信息"两部分;其中宏负责动态生成与代码位置相关的内容(文件名、行号、用户日志),而
set_pattern()则统一控制时间戳、日志等级、logger 名称等全局日志格式。这种分层设计提高了日志系统的灵活性、可维护性与扩展性。
cpp
set_pattern()