【 C++ AI 大模型接入 SDK】 - 日志模块


大家好,我是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 采用单例模式------整个程序只需要一个日志器。

实现要点:

  1. 私有构造函数 :外部无法 new Logger() 创建对象

比如:Logger log; // ❌ 不允许

  1. 删除拷贝和赋值:防止通过拷贝产生多个实例

delete 表示:禁止这个函数使用。都是为了防止对象被复制,保证唯一实例。

  1. 静态成员 _logger:持有 spdlog 的日志器对象,全局唯一

static属于类本身而不是某个对象,此时是整个类只有这一份,所有对象共享!

  1. 静态成员 _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)
        {   
            // 第二次检查(加锁后确认)
            // ... 创建日志器
        }
        // ... 设置格式和级别
    }
}

这是经典的双检锁单例模式,解决两个问题:

  1. 线程安全 :多线程可能同时调用 initLogger,需要加锁保护
  2. 性能:第一次检查不加锁,日志器已创建后直接返回,避免每次加锁的开销

线程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 对象;设置日志输出格式(时间戳、日志器名称、日志等级、日志消息);初始化异步线程池;以及控制日志输出到控制台或文件。

  1. 单例模式 + 双检锁:保证全局唯一日志器,同时兼顾线程安全与性能,避免重复创建 logger 对象
  2. 异步日志:基于 spdlog 的异步线程池机制,业务线程只负责将日志放入队列,后台线程负责真正的 IO 输出,从而避免日志阻塞业务线程
  3. 灵活输出:通过参数控制日志输出到控制台或文件,方便不同环境下使用
  4. 宏自动定位:自动记录日志来源文件与代码行号,无需手动填写
cpp 复制代码
__FILE__
__LINE__
  1. fmt 风格格式化:底层基于 fmt 的 {} 占位符格式化,相比传统 printf 风格更加安全、清晰、现代化
  2. 分层设计:整个日志系统被拆分为"日志正文"和"日志元信息"两部分;其中宏负责动态生成与代码位置相关的内容(文件名、行号、用户日志),而set_pattern()则统一控制时间戳、日志等级、logger 名称等全局日志格式。这种分层设计提高了日志系统的灵活性、可维护性与扩展性。
cpp 复制代码
set_pattern()
相关推荐
3Tony1 小时前
解决VScode报错:preLaunchTask“C/C++: gcc.exe 生成活动文件“已终止,退出代码为 -1.
c++·ide·vscode
谙弆悕博士2 小时前
【附C源码】二叉搜索树的C语言实现
c语言·开发语言·数据结构·算法·二叉树·项目实战·数据结构与算法
C+++Python2 小时前
C++ 泛型编程 极简示例代码
开发语言·c++
Dxy12393102162 小时前
如何使用jQuery获取一类元素并遍历它们
前端·javascript·jquery
Rust研习社2 小时前
Ubuntu 全面拥抱 Rust 后,我意识到 Rust 社区要变了
linux·服务器·开发语言·后端·ubuntu·rust
宵时待雨2 小时前
回溯算法专题2:二叉树中的深搜
开发语言·数据结构·c++·笔记·算法·深度优先
jiayong232 小时前
第 43 课:任务详情抽屉里的批量处理闭环与删除联动
java·开发语言·前端
likerhood2 小时前
Java 访问修饰符:public、protected、private讲解
java·开发语言·javascript
刀法如飞2 小时前
JavaScript 数组去重的 20 种实现方式,学会用不同思路解决问题
前端·javascript·算法