文章目录
- 专栏导读
- 抽象基类
- StdoutSink类设计
- FileSink类设计
- RollBySizeSink类设计
- 日志落地工厂类设计
- 日志落地类整理
- 日志落地拓展测试
- RollByTimeSink类设计
- 测试代码
- 测试完整代码
专栏导读
🌸作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
🌸专栏简介:本文收录于 C++项目——基于多设计模式下的同步与异步日志系统
🌸相关专栏推荐:C语言初阶系列、C语言进阶系列 、C++系列、数据结构与算法、Linux
日志落地类主要负责将日志消息输出到指定的位置。目前实现了三个日志落地方向:
标准输出
:StdoutSink;固定文件
:FileSink;滚动文件
(文件按照时间/大小
进行滚动切换):RollSink;
同时,日志落地类还应提供可扩展落地方向的功能。用户可以自己编写一个新的落地模块,将日志进行其他方向的落地。
- 实现思想:
抽象出落地模块类
;不同落地方向从基类进行派生
;使用工厂模式进行创建与表示分离
。
抽象基类
- 提供一个智能指针对象方便管理;
- 将日志输函数
log
作与析构函数
设置为虚函数
;
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
LogSink() {}
virtual ~LogSink() {}
virtual void log(const char *data, size_t len) = 0;
};
StdoutSink类设计
// 落地方向:标准输出
class StdOutSink : public LogSink
{
public:
void log(const char *data, size_t len)
{
std::cout.write(data, len);
}
};
FileSink类设计
类中包含两个成员:
pathname
:文件名,用来指定日志消息输出到哪个文件;ofs
:文件输出类对象,进行输出操作;
在C++中,ofstream
是用于文件输出
的类,它是 C++ 标准库中的一部分,通常与 ifstream
(用于文件输入
)一起使用。ofstream
类允许你创建、打开、写入和关闭
文本文件。你可以使用它来将数据写入文件,例如文本、数字或二进制数据。
// 落地方向:指定文件
class FileSink : public LogSink
{
public:
FileSink(const std::string &pathname)
:_pathname(pathname)
{
// 1.创建日志文件所在目录
util::File::createDirectory(util::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;
};
RollBySizeSink类设计
日志文件滚动的条件有两个:文件大小和时间。我们可以选择:
- 日志文件在大于1GB的时候会更换新的文件;
- 每天定点滚动一个日志文件。
本项目基于文件大小的判断滚动生成新的文件。
滚动文件输出的必要性
:
- 由于磁盘空间有限,我们不可能一直无限的向一个文件中增加数据;
- 如果一个日志文件的体积太大,一方面是不好打开,另一方面是即使打开了,由于包含数据巨大,也不利于查找我们需要的信息;
- 所以实际开发中会对单个日志文件的大小也会做一些限制,即当大小超过了某个大小时(如1GB),我们就重新创建一个新的日志文件来滚动写日志。对于那些过期的文件,大部分企业内都有专门的运维人员去定时清理过期的日志文件,或者设置定时任务,定时清理过期日志。
// 落地方向:滚动文件
class RollBySizeSink : public LogSink
{
public:
RollBySizeSink(const std::string &basename, size_t max_size)
: _basename(basename),
_max_fsize(max_size),
_cur_fsize(0),
_name_count(0)
{
std::string pathname = createNewFile();
// 1.创建日志文件所在的目录
util::File::createDirectory(util::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_fsize >= _max_fsize)
{
_ofs.close(); // 关闭原来已经打开的文件
std::string pathname = createNewFile();
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_fsize = 0;
}
_ofs.write(data, len);
assert(_ofs.good());
_cur_fsize += len;
}
private:
// 切换文件后,以时间格式创建新的文件名
std::string createNewFile()
{
time_t t = util::Date::getTime();
struct tm lt;
localtime_r(&t, <);
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();
}
private:
size_t _name_count;
std::string _basename;
std::ofstream _ofs;
size_t _max_fsize; // 日志文件最大大小
size_t _cur_fsize; // 已经写入的文件大小
};
日志落地工厂类设计
- 为了避免用户将来实现自己的落地方向时需要修改源代码,这违背了开闭原则,所以我们采用工厂类的设计;
- 由于不同的落地方向如
StdoutSink
、FileSink
、RollBySizeSink
,它们各自的构造函数所需参数并不相同,无法统一的管理,所以我们采用参数包的方式来解决。
class SinkFactory
{
public:
template <typename SinkType, typename... Args>
static LogSink::ptr create(Args &&...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
日志落地类整理
#ifndef __M_SINK_H__
#define __M_SINK_H__
#include "util.hpp"
#include <memory>
#include <sstream>
#include <fstream>
#include <cassert>
namespace LOG
{
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
LogSink() {}
virtual ~LogSink() {}
virtual void log(const char *data, size_t len) = 0;
};
// 落地方向:标准输出
class StdOutSink : public LogSink
{
public:
void log(const char *data, size_t len)
{
std::cout.write(data, len);
}
};
// 落地方向:指定文件
class FileSink : public LogSink
{
public:
FileSink(const std::string &pathname)
:_pathname(pathname)
{
// 1.创建日志文件所在目录
util::File::createDirectory(util::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;
};
// 落地方向:滚动文件
class RollBySizeSink : public LogSink
{
public:
RollBySizeSink(const std::string &basename, size_t max_size)
: _basename(basename),
_max_fsize(max_size),
_cur_fsize(0),
_name_count(0)
{
std::string pathname = createNewFile();
// 1.创建日志文件所在的目录
util::File::createDirectory(util::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_fsize >= _max_fsize)
{
_ofs.close(); // 关闭原来已经打开的文件
std::string pathname = createNewFile();
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_fsize = 0;
}
_ofs.write(data, len);
assert(_ofs.good());
_cur_fsize += len;
}
private:
std::string createNewFile()
{
time_t t = util::Date::getTime();
struct tm lt;
localtime_r(&t, <);
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();
}
private:
size_t _name_count;
std::string _basename;
std::ofstream _ofs;
size_t _max_fsize; // 日志文件最大大小
size_t _cur_fsize; // 已经写入的文件大小
};
class SinkFactory
{
public:
template <typename SinkType, typename... Args>
static LogSink::ptr create(Args &&...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
#endif
日志落地拓展测试
本小节主要内容为测试日志落地类是否支持扩展功能。我们新增一个基于时间的滚动文件类RollByTimeSink
。
RollByTimeSink类设计
#include <unistd.h>
enum class TimeGap
{
GAP_SECOND,
GAP_MINUTE,
GAP_HOUR,
GAP_DAY
};
class RollByTimeSink : public LOG::LogSink
{
public:
// 构造时传入文件名,并打开文件,将操作句柄管理起来
RollByTimeSink(const std::string &basename, TimeGap gap_type) : _basename(basename)
{
switch(gap_type)
{
case TimeGap::GAP_SECOND: _gap_size = 1; break;
case TimeGap::GAP_MINUTE: _gap_size = 60; break;
case TimeGap::GAP_HOUR: _gap_size = 3600; break;
case TimeGap::GAP_DAY: _gap_size = 3600 * 24; break;
}
_cur_gap = _gap_size == 1? LOG::util::Date::getTime() : LOG::util::Date::getTime() % _gap_size; // 获取当前是第几个时间段
std::string filename = createNewFile();
LOG::util::File::createDirectory(LOG::util::File::path(filename));
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
// 将日志消息写入到标准输出,判断当前时间是否是当前文件的时间段,不是则切换文件
void log(const char* data, size_t len)
{
time_t cur = LOG::util::Date::getTime();
if((cur % _gap_size) != _cur_gap)
{
_ofs.close();
std::string filename = createNewFile();
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
_ofs.write(data, len);
assert(_ofs.good());
}
private:
std::string createNewFile()
{
time_t t = LOG::util::Date::getTime();
struct tm lt;
localtime_r(&t, <);
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 << ".log";
return filename.str();
}
private:
std::string _basename;
size_t _gap_size; // 时间段的大小
int _cur_gap; // 当前是第几个时间段
std::ofstream _ofs;
};
测试代码
int main()
{
LOG::LogMsg msg(LOG::LogLevel::value::INFO, 53, "main.cc", "root", "格式化功能测试...");
LOG::Formatter fmt;
std::string str = fmt.format(msg);
LOG::LogSink::ptr time_lsp = LOG::SinkFactory::create<RollByTimeSink>("./logfile/roll-", TimeGap::GAP_SECOND);
time_t old = LOG::util::Date::getTime();
while(LOG::util::Date::getTime() < old + 5)
{
time_lsp->log(str.c_str(), str.size());
sleep(1);
}
}
测试结果
测试完整代码
test.cc
#include "LogSink.hpp"
#include "util.hpp"
#include <unistd.h>
enum class TimeGap
{
GAP_SECOND,
GAP_MINUTE,
GAP_HOUR,
GAP_DAY
};
class RollByTimeSink : public LOG::LogSink
{
public:
// 构造时传入文件名,并打开文件,将操作句柄管理起来
RollByTimeSink(const std::string &basename, TimeGap gap_type) : _basename(basename)
{
switch(gap_type)
{
case TimeGap::GAP_SECOND: _gap_size = 1; break;
case TimeGap::GAP_MINUTE: _gap_size = 60; break;
case TimeGap::GAP_HOUR: _gap_size = 3600; break;
case TimeGap::GAP_DAY: _gap_size = 3600 * 24; break;
}
_cur_gap = _gap_size == 1? LOG::util::Date::getTime() : LOG::util::Date::getTime() % _gap_size; // 获取当前是第几个时间段
std::string filename = createNewFile();
LOG::util::File::createDirectory(LOG::util::File::path(filename));
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
// 将日志消息写入到标准输出,判断当前时间是否是当前文件的时间段,不是则切换文件
void log(const char* data, size_t len)
{
time_t cur = LOG::util::Date::getTime();
if((cur % _gap_size) != _cur_gap)
{
_ofs.close();
std::string filename = createNewFile();
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
_ofs.write(data, len);
assert(_ofs.good());
}
private:
std::string createNewFile()
{
time_t t = LOG::util::Date::getTime();
struct tm lt;
localtime_r(&t, <);
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 << ".log";
return filename.str();
}
private:
std::string _basename;
size_t _gap_size; // 时间段的大小
int _cur_gap; // 当前是第几个时间段
std::ofstream _ofs;
};
int main()
{
LOG::LogMsg msg(LOG::LogLevel::value::INFO, 53, "main.cc", "root", "格式化功能测试...");
LOG::Formatter fmt;
std::string str = fmt.format(msg);
LOG::LogSink::ptr time_lsp = LOG::SinkFactory::create<RollByTimeSink>("./logfile/roll-", TimeGap::GAP_SECOND);
time_t old = LOG::util::Date::getTime();
while(LOG::util::Date::getTime() < old + 5)
{
time_lsp->log(str.c_str(), str.size());
sleep(1);
}
}