尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

【C++ AI 大模型接入 SDK】— 日志模块
📅 发布时间:2026/7/3 7:32:09

一、模块概述

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

本模块包含两个文件:

文件功能说明
include/util/myLog.hLogger 类声明 + 6 个日志宏(TRACE ~ CRIT)
src/util/myLog.cppLogger 类实现(初始化 spdlog、双检锁单例)

二、Logger 类 — 声明(myLog.h)

2.1 完整源码

#pragmaonce#include<mutex>#include<spdlog/logger.h>#include<spdlog/spdlog.h>namespacebite{classLogger{public:staticvoidinitLogger(conststd::string&loggerName,conststd::string&loggerFile,spdlog::level::level_enum logLevel=spdlog::level::info);staticstd::shared_ptr<spdlog::logger>getLogger();private:Logger();Logger(constLogger&)=delete;Logger&operator=(constLogger&)=delete;private:staticstd::shared_ptr<spdlog::logger>_logger;staticstd::mutex _mutex;};}// end bite

2.2 逐段解析

单例模式设计
classLogger{// ...private:Logger();// 私有构造,禁止外部实例化Logger(constLogger&)=delete;// 禁止拷贝构造Logger&operator=(constLogger&)=delete;// 禁止赋值private:staticstd::shared_ptr<spdlog::logger>_logger;// 唯一的日志器实例staticstd::mutex _mutex;// 用于线程安全};

Logger 采用单例模式——整个程序只需要一个日志器。

实现要点:

  1. 私有构造函数:外部无法new Logger()创建对象
比如:Logger log; // ❌ 不允许
  1. 删除拷贝和赋值:防止通过拷贝产生多个实例

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

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

  1. 静态成员_mutex:配合双检锁保证线程安全,同一时刻:只能一个线程写。

所有方法都是static的,通过Logger::initLogger(...)和Logger::getLogger()直接调用,无需创建 Logger 对象。

接口说明
// 初始化日志器(只能调用一次)staticvoidinitLogger(conststd::string&loggerName,conststd::string&loggerFile,spdlog::level::level_enum logLevel=spdlog::level::info);// 获取日志器实例staticstd::shared_ptr<spdlog::logger>getLogger();
参数说明
loggerName日志器名称,会出现在日志输出中,如 “ChatServer”
loggerFile日志输出目标,传 “stdout” 输出到控制台,传文件路径输出到文件
logLevel日志级别过滤,默认 info,低于该级别的日志不会被输出

三、Logger 类 — 实现(myLog.cpp)

3.1 完整源码

#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>//异步日志支持namespacebite{std::shared_ptr<spdlog::logger>Logger::_logger=nullptr;std::mutex Logger::_mutex;Logger::Logger(){}voidLogger::initLogger(conststd::string&loggerName,conststd::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,后台线程数 1spdlog::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 bite

3.2 逐段解析

静态成员初始化
std::shared_ptr<spdlog::logger>Logger::_logger=nullptr;std::mutex Logger::_mutex;

类的静态成员变量需要在类外定义和初始化。_logger初始为nullptr,在initLogger中才创建真正的 spdlog 日志器。

双检锁(Double-Checked Locking)
voidLogger::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 → 直接返回(不加锁)
异步日志
spdlog::init_thread_pool(32768,1);

启用异步日志模式:

参数一:队列大小,参数二:后台线程数量
  • 日志消息先写入一个大小为 32768 的队列
  • 由1 个后台线程负责将队列中的日志写入目标(控制台/文件)
  • 调用日志的线程不会因 IO 操作而阻塞,提高性能
业务线程:INFO("xxx") → 写入队列 → 立即返回 ↓ 后台线程: 从队列取出 → 写入控制台/文件
日志输出目标选择
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>创建异步写入文件的日志器
日志格式设置
_logger->set_pattern("[%H:%M:%S][%n][%-7l]%v");_logger->set_level(logLevel);

格式化占位符说明:

占位符含义输出示例
%H:%M:%S时:分:秒9:04:03
%n日志器名称ChatServer
%-7l日志级别,左对齐,宽度 7info
%v实际日志消息init model success

输出效果:

[09:04:03][ChatServer][info ][ DataManager.cpp:15] Database opened successfully: chat.db [09:04:04][ChatServer][error ][ DoubaoProvider.cpp:18] api_key not found

四、日志宏定义

4.1 六个日志宏

#defineTRACE(format,...)bite::Logger::getLogger()->trace(std::string("[{:>10s}:{:<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)#defineDBG(format,...)bite::Logger::getLogger()->debug(std::string("[{:>10s}:{:<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)#defineINFO(format,...)bite::Logger::getLogger()->info(std::string("[{:>10s}:{:<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)#defineWARN(format,...)bite::Logger::getLogger()->warn(std::string("[{:>10s}:{:<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)#defineERR(format,...)bite::Logger::getLogger()->error(std::string("[{:>10s}:{:<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)#defineCRIT(format,...)bite::Logger::getLogger()->critical(std::string("[{:>10s}:{:<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)

日志级别从低到高:

宏级别使用场景
TRACEtrace最详细的追踪信息,通常调试时使用
DBGdebug调试信息
INFOinfo常规运行信息(默认级别)
WARNwarning警告,不影响运行但需关注
ERRerror错误,操作失败
CRITcritical严重错误,可能导致程序崩溃

4.2 宏的工作原理

在实际项目中,我们通常不会直接调用 spdlog 的info()、debug()等接口,而是会进一步封装成自己的日志宏,例如:

#defineINFO(format,...)\bite::Logger::getLogger()->info(std::string("[{:>10s}:{:<4d}]")+format,__FILE__,__LINE__,##__VA_ARGS__)

这段代码第一次看可能会觉得特别复杂,但其实它本质上就是在帮我们“自动补全日志信息”。以后我们只需要简单写一句:

INFO("用户ID = {}",uid);

日志系统就会自动帮我们把“文件名”和“代码行号”也一起打印出来,比如:

[main.cpp:35]用户ID=1001

这样做的最大好处就是:后期排查 Bug 的时候,我们能立刻知道日志是从哪一行代码打印出来的,而不用全局搜索,大型项目里这个功能非常重要。

接下来我们拆开来看。首先:

#defineINFO(format,...)

这里的#define是 C/C++ 中的宏定义,本质上就是“文本替换”。编译之前,编译器会先把:

INFO("hello");

替换成:

mylog::Logger::getLogger()->info(...);

其中format表示格式字符串,而...则表示**“可变参数”**,也就是说参数数量不固定。比如下面这些写法都合法:

INFO("hello");INFO("id = {}",id);INFO("{} {}",a,b);

随后重点来了:

__FILE__和__LINE__

它们是 C++ 提供的预定义宏。__FILE__表示当前文件名,而__LINE__表示当前代码所在行号。比如:

main.cpp35

因此日志系统就能自动知道:

这是 main.cpp 第35行打印的日志

再来看这部分:

"[{:>10s}:{:<4d}]"

这里使用的是 fmt 风格格式化语法,因为 spdlog 底层实际上就是基于 fmt 库实现的。

其中:

{:>10s}

表示字符串右对齐,占 10 个字符宽度;而:

{:<4d}

表示整数左对齐,占 4 个字符宽度。

这样做是为了让日志输出更加整齐,例如:

[ main.cpp:35 ]

整体看起来会非常规范。

最后:

##__VA_ARGS__

表示把用户传入的可变参数继续转发给 spdlog。例如:

INFO("uid = {}",uid);

最终会变成:

logger->info("[{:>10s}:{:<4d}]uid = {}",__FILE__,__LINE__,uid);

随后 spdlog 会自动把:

  • 文件名填入第一个{}
  • 行号填入第二个{}
  • uid 填入最后一个{}

最终生成完整日志。

实际上,像 glog、spdlog 等成熟日志库,底层都大量使用这种“宏 + 文件名 + 行号”的设计方式,因为日志系统最核心的目标就是:快速定位问题。

五、使用方式

5.1 初始化

在程序入口处调用一次:

#include<ai_chat_sdk/util/myLog.h>intmain(){// 初始化日志:名称 "ChatServer",输出到控制台,INFO 级别bite::Logger::initLogger("ChatServer","stdout",spdlog::level::info);// 之后在任意位置使用日志宏INFO("服务器启动成功, 端口: {}",8080);ERR("连接模型失败: {}","timeout");return0;}//效果[14:25:31][ChatServer][info][main.cpp:8]服务器启动成功,端口:8080[14:25:31][ChatServer][error][main.cpp:9]连接模型失败:timeout

5.2 在 SDK 各模块中的使用

日志宏在整个 SDK 中被广泛使用,以LLMManager为例:

boolLLMManager::registerProvider(conststd::string&modelName,std::unique_ptr<LLMProvider>provider){if(!provider){ERR("cannot register nullptr provider, modelName = {}",modelName);// 错误日志returnfalse;}_providers[modelName]=std::move(provider);INFO("register provider success, modelName = {}",modelName);// 信息日志returntrue;}

输出效果:

[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=doubao-pro[09:04:04][ChatServer][error][DoubaoProvider.cpp:18]api_key not found

日志中的文件名和行号能帮助快速定位问题所在位置。

六、设计总结

myLog.h其中包括:日志初始化、日志获取函数的声明;通过私有构造、禁止外界实例化、禁止拷贝与赋值,保证全局仅存在一个日志对象(单例模式);定义静态成员_logger(日志器对象)与_mutex(线程锁),实现全局共享;同时定义日志宏,实现“文件名 + 行号 + 格式化内容 + 可变参数”的自动拼接,最终通过:

Logger::getLogger()

间接获取_logger,并调用真正的 spdlog 日志接口。

myLog.cpp主要负责:日志初始化与getLogger()函数的实现;创建 logger 对象;设置日志输出格式(时间戳、日志器名称、日志等级、日志消息);初始化异步线程池;以及控制日志输出到控制台或文件。

  1. 单例模式 + 双检锁:保证全局唯一日志器,同时兼顾线程安全与性能,避免重复创建 logger 对象

  2. 异步日志:基于 spdlog 的异步线程池机制,业务线程只负责将日志放入队列,后台线程负责真正的 IO 输出,从而避免日志阻塞业务线程

  3. 灵活输出:通过参数控制日志输出到控制台或文件,方便不同环境下使用

  4. 宏自动定位:自动记录日志来源文件与代码行号,无需手动填写

  5. fmt 风格格式化:底层基于 fmt 的{}占位符格式化,相比传统printf风格更加安全、清晰、现代化

  6. 分层设计:整个日志系统被拆分为“日志正文”和“日志元信息”两部分;其中宏负责动态生成与代码位置相关的内容(文件名、行号、用户日志),而set_pattern()则统一控制时间戳、日志等级、logger 名称等全局日志格式。这种分层设计提高了日志系统的灵活性、可维护性与扩展性。

相关新闻

  • 临时处理PDF不用再找网站:搭建一个随身可用的私人PDF工具箱
  • 如何永久保存微信聊天记录?WeChatMsg开源备份工具终极指南
  • AI学生高效学习法:用豆包实现概念具象化与任务链执行

最新新闻

  • 软考与PMP到底选哪个?(一张决策树图解决90%人的职业卡点)
  • 【软考vsPMP终极抉择指南】:20年项目管理老兵亲授——3大维度对比、5类人群适配图谱、2024通过率与薪资溢价数据全披露
  • 软考高级哪个最值钱?基于人社部2023人才缺口数据、北上广深平均年薪增幅(+37.6%)、以及127家上市公司招聘JD语义分析的终极结论
  • 企业级数据主权解决方案:个人数字资产本地化备份与AI训练架构
  • 微信聊天记录永久保存终极指南:3种格式对比+快速上手方案
  • 社交媒体文案生成器——鸿蒙 + AI 让表达更出彩

日新闻

  • JMeter接口测试实战:从核心元件到复杂场景构建
  • Java Applet版刽子手游戏源码:含完整项目结构、吊杆绘图与胜负逻辑
  • 使用Apache JMeter对RoadRunner PHP应用进行性能测试与调优指南

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号