1:上一篇
在上一篇我们完成了日志消息的封装,可以提供给用户自定义设置日志的输出格式,现在我们继续在基础上封装出一个文件,负责将格式化后的日志数据输出到指定目的地(控制台、文件、网络等)。
模块整体设计思路
日志落地模块的核心设计目标是高扩展性 和低耦合,因此采用了以下经典设计模式与架构思路:
-
抽象基类(LogSink):定义日志输出的统一接口,屏蔽不同落地方向的实现差异;
-
派生类具体化:针对控制台、单个文件、按大小滚动文件等场景,实现基类的纯虚接口;
-
工厂模式(SinkFactory):解耦对象创建与使用,简化不同 Sink 实例的创建流程;
-
跨平台兼容:适配 Windows/Linux 的时间处理接口,保证代码可移植性。
LogSink(抽象基类)
├── StdoutSink(控制台输出)
├── FileSink(单个文件输出)
└── RollSizeSink(按大小滚动文件输出)
SinkFactory(工厂类:创建各类Sink实例)
2:代码模块讲解
1:前置定义与跨平台兼容
cpp
#pragma once
/*
日志落地模块的实现
1抽象落地基类
2派生子类(根据不同的落地方向派生)
3使用工厂模式进行创建与表示的分离
*/
#include "util.hpp" // 工具类(文件操作、时间处理)
#include <memory> // 智能指针
#include <fstream> // 文件流
#include <sstream> // 字符串流
#include <cassert> // 断言
#include <ctime> // 时间处理
#include <iomanip> // 格式化输出(setw/setfill)
#include <chrono> // 时间戳
// 跨平台时间处理宏:解决localtime线程不安全问题
#ifdef _WIN32
// Windows下使用localtime_s(安全版本)
#define LOCAL_TIME_SAFE(timestamp,tm_ptr) localtime_s(tm_ptr,&(timestamp))
#else
// Linux下使用localtime_r(安全版本)
#define LOCAL_TIME_SAFE(timestamp,tm_ptr) localtime_r(&(timestamp),tm_ptr)
#endif // _WIN32
关键说明:
util.hpp是自定义工具类,包含文件目录创建、时间戳获取等通用功能(下文会用到);LOCAL_TIME_SAFE宏解决了localtime的线程安全问题:Windows 使用localtime_s,Linux 使用localtime_r,保证跨平台兼容性。
2:抽象基类LogSink
cpp
/**
* @brief 日志落地基类,定义日志输出的统一接口
* 设计思路:采用纯虚函数定义接口,所有派生类必须实现log方法,保证接口一致性
*/
class LogSink {
public:
// 虚析构函数:确保派生类析构时正确调用基类析构
virtual ~LogSink() = default;
/**
* @brief 纯虚函数,用于将日志数据写入具体的目的地
* @param data 指向待写入日志数据的指针(格式化后的日志字符串)
* @param len 待写入日志数据的长度(字节数)
*/
virtual void log(const char* data, size_t len) = 0;
// 类型别名:简化shared_ptr的使用,提高代码可读性
using ptr = std::shared_ptr<LogSink>;
};
- 纯虚函数
log强制派生类实现具体的输出逻辑,保证 "开闭原则"(新增落地方向只需新增派生类,无需修改基类); std::shared_ptr类型别名ptr:简化智能指针声明,避免重复书写std::shared_ptr<LogSink>;- 虚析构函数:防止基类指针指向派生类对象时,析构不完整导致内存泄漏。
3:控制台输出Sink:StdoutSink
cpp
/**
* @brief 标准输出日志落地类,将日志打印到控制台
* 适用场景:开发调试阶段,快速查看日志
*/
class StdoutSink :public LogSink {
public:
/**
* @brief 将日志数据写入标准输出流
* @param data 指向待写入日志数据的指针
* @param len 待写入日志数据的长度
*/
void log(const char* data, size_t len) override
{
// std::cout.write:直接写入字节流,比operator<<更高效(避免字符串拷贝)
std::cout.write(data, len);
}
};
- 使用
std::cout.write而非std::cout << data:write直接操作字节流,无需解析字符串格式,性能更高; - 极简实现:控制台输出无需额外资源管理(如文件句柄),逻辑轻量。
4:单个文件输出Sink:FileSink
cpp
/**
* @brief 文件日志落地类,将日志写入指定的单个文件
* 适用场景:小流量日志场景,无需滚动切割
*/
class FileSink :public LogSink {
public:
/**
* @brief 构造函数,初始化文件路径并打开文件句柄
* @param pathname 日志文件的完整路径名(如./logs/app.log)
*/
FileSink(const std::string& pathname) :_pathname(pathname)
{
// 1. 自动创建日志文件所在目录(若不存在)
util::File::createdirectory(util::File::Path(pathname));
// 2. 以二进制+追加模式打开文件:
// - 二进制模式:避免Windows下换行符(\n)被转换为\r\n,保证日志字节数准确
// - 追加模式:每次写入都追加到文件末尾,避免覆盖已有日志
_ofs.open(pathname,std::ios::binary | std::ios::app);
// 断言:文件打开失败时直接终止程序(生产环境可替换为异常处理)
assert(_ofs.is_open());
}
/**
* @brief 将日志消息追加写入指定文件
* @param data 指向待写入日志数据的指针
* @param len 待写入日志数据的长度
*/
void log(const char* data, size_t len) override
{
_ofs.write(data, len);
// 断言:写入失败时终止程序(生产环境可增加重试逻辑)
assert(_ofs.good());
}
private:
std::string _pathname; // 日志文件完整路径
std::ofstream _ofs; // 文件输出流对象
};
- 文件打开模式:
std::ios::binary | std::ios::appbinary:避免跨平台换行符转换,保证日志内容的一致性;app:追加模式,即使多进程写入(非并发)也不会覆盖已有日志;
- 目录自动创建:通过
util::File::createdirectory自动创建日志目录(如./logs),无需手动提前创建; - 断言使用:
assert(_ofs.is_open())/assert(_ofs.good())快速发现文件操作异常,适合调试阶段(生产环境可替换为日志告警 + 异常捕获)。
5:按大小滚动文件Sink:RollSizeSink
cpp
/**
* @brief 滚动文件日志落地类,根据文件大小自动切换日志文件
* 适用场景:高流量日志场景,避免单个日志文件过大(如超过1GB),便于日志归档/检索
*/
class RollSizeSink : public LogSink {
public:
/**
* @brief 构造函数,初始化滚动文件策略
* @param basename 日志文件的基础名称前缀(如./logs/app)
* @param max_size 单个日志文件的最大允许大小(字节),超过此大小将创建新文件
*/
RollSizeSink(const std::string& basename, size_t max_size) :
_basename(basename),
_max_size(max_size),
_cur_size(0),
_name_count(0)
{
// 初始化时创建第一个日志文件
std::string pathname = createNewFile();
// 创建日志目录
util::File::createdirectory(util::File::Path(pathname));
// 打开第一个日志文件
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
/**
* @brief 将日志数据写入当前文件,若超出大小限制则滚动创建新文件
* @param data 指向待写入日志数据的指针
* @param len 待写入日志数据的长度
*/
void log(const char* data, size_t len)override
{
// 检查当前文件大小是否超过阈值,若超过则滚动创建新文件
if (_cur_size >= _max_size)
{
std::string pathname = createNewFile();
_ofs.close();// 关闭旧文件句柄
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_size = 0; // 重置当前文件大小
}
// 写入日志数据
_ofs.write(data, len);
assert(_ofs.good());
// 更新当前文件已写入大小
_cur_size += len;
}
private:
/**
* @brief 生成新的滚动日志文件名,包含时间戳和序列号
* @return 新生成的日志文件完整路径字符串
*/
std::string createNewFile()
{
// 获取当前系统时间戳
time_t t = util::Date::GetTime();
struct tm lt{};
// 线程安全的本地时间转换
LOCAL_TIME_SAFE(t, <);
std::stringstream filename;
// 拼接文件名:基础名 + 时间戳 + 序列号 + .log
// 示例:./logs/app2024.05.20-15-30-20-0.log
filename << _basename;
// setw/setfill:格式化时间字段(补0,如5月→05,9秒→09)
filename << std::setw(4) << std::setfill('0') << lt.tm_year + 1900 << "."; // 年(tm_year是从1900开始的偏移量)
filename << std::setw(2) << std::setfill('0') << lt.tm_mon + 1 << "."; // 月(tm_mon从0开始)
filename << std::setw(2) << std::setfill('0') << lt.tm_mday << "-"; // 日
filename << std::setw(2) << std::setfill('0') << lt.tm_hour << "-"; // 时
filename << std::setw(2) << std::setfill('0') << lt.tm_min << "-"; // 分
filename << std::setw(2) << std::setfill('0') << lt.tm_sec << "-"; // 秒
filename << _name_count++; // 序列号(避免同一秒创建多个文件)
filename << ".log"; // 后缀
return filename.str();
}
private:
size_t _name_count; // 日志文件序列号(同一时间戳下的文件计数)
std::string _basename; // 日志文件基础前缀(如./logs/app)
std::ofstream _ofs; // 当前打开的文件输出流
size_t _max_size; // 单个文件最大大小(字节)
size_t _cur_size; // 当前文件已写入大小(字节)
};
- 滚动触发逻辑 :每次写入日志前检查
_cur_size是否超过_max_size,若超过则关闭旧文件、创建新文件; - 文件名生成规则 :
- 时间戳格式:
年.月.日-时-分-秒,保证日志文件按时间有序; - 序列号:避免同一秒内文件大小超限,生成多个文件时通过序列号区分;
- 时间戳格式:
- 格式化工具 :
std::setw(设置字段宽度)+std::setfill('0')(补 0)保证时间字段格式统一(如2024.05.01而非2024.5.1); - 状态管理 :
_cur_size实时记录当前文件写入大小,_name_count自动递增保证文件名唯一。
6:工厂模式:SinkFactory
cpp
/**
* @brief 日志落地对象工厂类,用于创建不同类型的LogSink实例
* 设计思路:解耦对象创建与使用,用户无需直接new,只需通过工厂创建,降低耦合
*/
class SinkFactory {
public:
/**
* @brief 模板静态方法,创建指定类型的LogSink共享指针
* @tparam SinkType 要创建的LogSink派生类类型(如StdoutSink、FileSink)
* @tparam Args 构造函数参数类型包(支持任意参数个数/类型)
* @param args 传递给SinkType构造函数的参数包
* @return 指向新创建的LogSink对象的std::shared_ptr
*/
template<typename SinkType, typename ...Args>
static LogSink::ptr create(Args &&...args)
{
// 完美转发:保证参数的左值/右值属性不变,提高效率
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
简化对象创建 :用户无需关注std::make_shared的细节,只需调用SinkFactory::create<类型>(参数);
cpp
// 创建控制台Sink
auto stdout_sink = SinkFactory::create<StdoutSink>();
// 创建单个文件Sink
auto file_sink = SinkFactory::create<FileSink>("./logs/app.log");
// 创建按大小滚动Sink(最大1GB)
auto roll_sink = SinkFactory::create<RollSizeSink>("./logs/app", 1024*1024*1024);
- 类型安全:模板参数强制指定 Sink 类型,编译期检查类型正确性;
- 完美转发 :
std::forward<Args>(args)保证构造函数参数的传递效率,避免不必要的拷贝。
3:使用样例
cpp
#include "sink.hpp"
#include <string>
int main() {
// 1. 创建控制台Sink
my_log::LogSink::ptr stdout_sink = my_log::SinkFactory::create<my_log::StdoutSink>();
std::string log1 = "【DEBUG】这是控制台日志\n";
stdout_sink->log(log1.c_str(), log1.size());
// 2. 创建单个文件Sink
my_log::LogSink::ptr file_sink = my_log::SinkFactory::create<my_log::FileSink>("./logs/app.log");
std::string log2 = "【INFO】这是文件日志\n";
file_sink->log(log2.c_str(), log2.size());
// 3. 创建按大小滚动Sink(最大100字节)
my_log::LogSink::ptr roll_sink = my_log::SinkFactory::create<my_log::RollSizeSink>("./logs/roll", 100);
for (int i = 0; i < 5; ++i) {
std::string log3 = "【WARN】这是滚动文件日志,第" + std::to_string(i) + "条\n";
roll_sink->log(log3.c_str(), log3.size());
}
return 0;
}
4:总结
- 高扩展性 :新增落地方向(如网络 Sink、数据库 Sink)只需继承
LogSink并实现log方法; - 高性能:字节流写入、二进制模式、无冗余拷贝,适合高并发日志场景;
- 跨平台:时间处理、文件操作兼容 Windows/Linux;
- 易用性:工厂模式简化对象创建,智能指针自动管理资源。
日志落地模块是日志库的核心组件,本文实现的Sink模块通过抽象基类 + 派生类 + 工厂模式,兼顾了扩展性、性能与易用性。从控制台调试到高流量文件滚动,覆盖了大部分常见的日志落地场景,同时预留了充足的扩展空间,可根据业务需求快速适配新的落地方向。
在实际项目中,可结合日志格式化模块、日志级别过滤模块,构建一套完整的高性能 C++ 日志库,满足从开发调试到生产部署的全场景需求。