C++项目 —— 基于多设计模式下的同步&异步日志系统(2)(工厂模式)
我们在之前把日志消息的主体已经组织好了,而且我们创建的适当的类控制日志格式,并且按照我们的格式组织日志消息。如果还没有看过的小伙伴可以点击这里:
我们这次的任务是要完成日志器中的一个小功能实现:日志输出的方向,我们日志输出的方向有:控制台,固定文件,滚动文件。这个模块的实现还会涉及到工厂模式的使用。
基类实现
首先设计思想还是比较清晰的,设计一个基类,派生出三个不同的方向,我们先实现两个方向比较简单的:
namespace logs
{
//基类实现
class BaseSink
{
public:
using ptr = std::shared_ptr<BaseSink>;
BaseSink()
{
}
virtual ~BaseSink() {}
virtual void log(const char *data, size_t len) = 0; //要继承实现的接口
};
class StdoutSink : public BaseSink
{
public:
//将日志消息写到标准输出
void log(const char *data, size_t len)
{
std::cout.write(data, len);
}
};
class FixFileSink : public BaseSink
{
public:
FixFileSink(const std::string& pathname)
:_pathname(pathname)
{
//1.创建文件所在路径
logs::utils::File::createDiretory(logs::utils::File::path(_pathname));
//2.创建文件并打开
_ofs.open(_pathname,std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
//将日志消息写到固定文件中
void log(const char *data, size_t len)
{
_ofs.write(data,len);
assert(_ofs.good());
}
private:
std::string _pathname; //创建文件时的文件路径
std::ofstream _ofs; //流式文件操作
};
}
我们也可以顺便测试一下:
#include"utils.hpp"
#include"level.hpp"
#include"message.hpp"
#include"fometter.hpp"
#include "sink.hpp"
int main()
{
// std::cout << logs::utils::File::path("./abc/def");
// logs::utils::File::createDiretory("./abc/def");
//std::cout <<logs::Loglevel::toString(logs::Loglevel::value::DEBUG);
logs::logMsg msg(logs::Loglevel::value::DEBUG,"main.cc",53,"root","格式化功能测试....");
logs::Formetter fmt("abc[%d{%H:%M:%S}][%c]%T%m%n");
std::string str = fmt.format(msg);
//标准输入测试
size_t len = str.size();
logs:: StdoutSink st;
st.log(str.c_str(),len);
//文件测试
logs::FixFileSink fx("../test/test.txt");
fx.log(str.c_str(),len);
}
滚动文件
滚动文件意思是,如果这个文件日志已经被写满了,会自动换到一个新的文件写日志,写满了的话又继续换。
文件名问题
既然会不停的创建文件,我们就不得不考虑一个问题,文件名。文件名是不能重复的,那么有没有一种方式,能保证我们的文件名是绝对不会重复的呢?有的,时间戳,时间只要在流逝,时间戳就会变。我们可以让时间戳作为我们文件名的一部分,这样就可以保证文件绝对不会重复了。
class RollFileSink : public BaseSink
{
public:
private:
std::string createNewFile()
{
//1.获取当前时间戳
time_t time = logs::utils::Date::get_time();
struct tm lt;
localtime_r(&time,<);
std::stringstream filename;
filename << _basename;
filename << lt.tm_year + 1900;
filename << lt.tm_mon + 1;
filename << lt.tm_mday;
filename << lt.tm_hour;
filename << lt.tm_min;
filename << lt.tm_sec;
filename << "-";
filename << _name_count++;
filename << ".log";
return filename.str();
}
// 基础文件名 + 扩展文件名(以时间生成)组成一个实际的当前输出文件名
size_t _name_count;
std::string _basename; //基础文件名
std::ofstream _ofs; //流式文件
size_t _max_size; //最大文件大小
size_t _cur_size: //当前文件大小
};
class RollFileSink : public BaseSink
{
public:
RollFileSink(const std::string& basename,size_t max_size)
:_basename(basename)
,_name_count(0)
,_max_size(max_size)
,_cur_size(0)
{
std::string pathname = createNewFile();
//1、创建文件的所在路径
logs::utils::File::createDiretory(logs::utils::File::path(pathname));
// 2.创建并打开日志文件
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
// 将日志消息写到固定文件中
void log(const char *data, size_t len)
{
if(_cur_size > _max_size)
{
_ofs.close();
std::string pathname = createNewFile();
//1、创建文件的所在路径
logs::utils::File::createDiretory(logs::utils::File::path(pathname));
// 2.创建并打开日志文件
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
_ofs.write(data, len);
assert(_ofs.good());
_cur_size += len;
}
private:
std::string createNewFile()
{
//1.获取当前时间戳
time_t time = logs::utils::Date::get_time();
struct tm lt;
localtime_r(&time,<);
std::stringstream filename;
filename << _basename;
filename << lt.tm_year + 1900;
filename << lt.tm_mon + 1;
filename << lt.tm_mday;
filename << lt.tm_hour;
filename << lt.tm_min;
filename << lt.tm_sec;
filename << "-";
filename << _name_count++;
filename << ".log";
return filename.str();
}
// 基础文件名 + 扩展文件名(以时间生成)组成一个实际的当前输出文件名
size_t _name_count;
std::string _basename; //基础文件名
std::ofstream _ofs; //流式文件
size_t _max_size; //最大文件大小
size_t _cur_size; //当前文件大小
};
我们可以测试一下:
logs::RollFileSink roll("../test/mytest",1024);
size_t cur = 0;
while(cur < 1024 * 2)
{
roll.log(str.c_str(),len);
cur+=len;
}
工厂模式
代码定义了一个名为 SinkFactory 的工厂类,用于创建日志输出器(Sink)的智能指针实例。它使用了现代 C++ 的模板和可变参数特性,是一个非常灵活通用的工厂实现:
class SinkFactory
{
public:
template<typename SinkType,typename... Args>
static BaseSink::ptr create(Args && ...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
auto st1 = logs::SinkFactory::create<logs::StdoutSink>();
st1->log(str.c_str(),len);
这样我们不用用户直接接触接口,而是通过工厂,这样更具灵活性。
一些扩展点
struct tm
是 C/C++ 标准库中用于表示日历时间的结构体,定义在 <ctime>
头文件中。以下是其成员变量及详细说明:
struct tm
成员列表
成员 | 类型 | 说明 | 取值范围 | 注意事项 |
---|---|---|---|---|
tm_sec |
int |
秒 | 0-61 (通常 0-59) | 允许闰秒 |
tm_min |
int |
分钟 | 0-59 | |
tm_hour |
int |
小时(24小时制) | 0-23 | |
tm_mday |
int |
月中的第几天(Day of month) | 1-31 | |
tm_mon |
int |
月份(从0开始) | 0-11 (0=1月) | 使用时需 +1 |
tm_year |
int |
年份(从1900开始) | 0+=1900年 | 使用时需 +1900 |
tm_wday |
int |
星期几(从0开始,0=周日) | 0-6 (0=周日) | |
tm_yday |
int |
年中的第几天(从0开始) | 0-365 | |
tm_isdst |
int |
夏令时标志: 正数=启用 0=禁用 负数=信息不可用 |
-1, 0, 1 |
关键注意事项
特殊计数规则:
- 月份:
tm_mon
从 0 开始(0=1月,11=12月),显示时需要+1
- 年份:
tm_year
是 1900 年起的偏移量,真实年份 =tm_year + 1900
- 星期:
tm_wday
中 0 表示周日
- 月份:
夏令时处理:
if (timeinfo.tm_isdst > 0) { std::cout << "夏令时生效"; }
有效范围扩展:
tm_sec
允许 60-61 以兼容闰秒- 其他字段超出范围时,
mktime()
会自动标准化
使用示例
1. 获取当前时间并打印
#include <ctime>
#include <iostream>
int main() {
time_t now = time(nullptr);
struct tm timeinfo;
localtime_r(&now, &timeinfo); // 线程安全版本
std::cout << "当前时间: "
<< 1900 + timeinfo.tm_year << "-"
<< 1 + timeinfo.tm_mon << "-"
<< timeinfo.tm_mday << " "
<< timeinfo.tm_hour << ":"
<< timeinfo.tm_min << ":"
<< timeinfo.tm_sec;
}
2. 与 strftime
配合使用
char buf[64];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
std::cout << "格式化时间: " << buf;
3. 构造自定义时间
struct tm custom_time = {0};
custom_time.tm_year = 2023 - 1900; // 2023年
custom_time.tm_mon = 6 - 1; // 6月
custom_time.tm_mday = 15; // 15日
time_t t = mktime(&custom_time); // 转换为time_t
常见问题
为什么年份从1900开始?
历史原因,早期系统用2位数存储年份,1900为基准年。如何获取时区信息?
通过tm_gmtoff
和tm_zone
(非标准扩展,需检查平台支持):#ifdef __linux__ std::cout << "UTC偏移: " << timeinfo.tm_gmtoff / 3600 << "小时"; #endif
线程安全注意
- 优先使用
localtime_r()
(POSIX)或localtime_s()
(Windows) - 避免使用非线程安全的
localtime()
- 优先使用
可视化记忆表
struct tm {
int tm_sec; // 秒 [0,61]
int tm_min; // 分 [0,59]
int tm_hour; // 时 [0,23]
int tm_mday; // 日 [1,31]
int tm_mon; // 月 [0,11] ← 注意+1
int tm_year; // 年-1900 ← 注意+1900
int tm_wday; // 周几 [0,6] (0=周日)
int tm_yday; // 年内天数 [0,365]
int tm_isdst; // 夏令时标志
};
掌握这些成员的含义和特性,可以高效处理各种时间操作需求。