日志落地类设计(简单工厂模式)
日志落地类主要负责落地日志消息到目的地。它主要包括以下内容:
这个类支持可扩展,其成员函数log设置为纯虚函数,当我们需要增加一个log输出目标,可以增加一个类继承自该类并重写log方法实现具体的落地日志逻辑。
目前实现了三个不同方向上的日志落地:
- 标准输出: StdoutSink
- 固定文件: FileSink
- 滚动文件: RollSink
- 滚动日志文件输出的必要性
- 由于机器磁盘空间有限,我们不可能一直无限地向一个文件中增加数据
- 如果一个日志文件体积太大,一方面是不好打开,另一方面是即时打开了由于包含数据巨大,也不利于查找我们需要的信息
- 所以实际开发中会对单个日志文件的大小也会做一些控制,即当大小超过某个大小时〈如1GB),我们就重新创建一个新的日志文件来滚动写日志。对于那些过期的日志,大部分企业内部都有专门的运维人员去定时清理过期的日志,或者设置系统定时任务,定时清理过期日志。
- 日志文件的滚动思想:
日志文件滚动的条件有两个:文件大小和时间。我们可以选择:- 日志文件在大于1GB的时候会更换新的文件
- 每天定点滚动一个日志文件
- 滚动日志文件输出的必要性
本项目基于文件大小的判断滚动生成新的文件
/* 日志落地模块的实现
1. 抽象落地基类
2. 派生子类(根据不同的落地方向进行派生)
3. 使用工厂模式进行创建与表示分离
*/
namespace zyqlog
{
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) override
{
// std::cout << // 无法指定大小
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) override
{
_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 = createNewFileName();
// 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) override
{
if (_cur_fsize >= _max_fsize)
{
_ofs.close(); // 关闭原来已经打开的文件
std::string pathname = createNewFileName();
_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 createNewFileName() // 进行大小判断,超过大小就创建新文件
{
// 获取系统时间,以时间来构造文件名扩展名
time_t t = util::Date::now();
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:
// 通过基础文件名 + 扩展文件名组成一个实际的当前输出文件名
std::string _basename; // ./logs/base- -> ./logs/base-20020809132332.log
std::ofstream _ofs;
size_t _max_fsize; // 记录最大大小,当前文件超过这个大小就要切换文件
size_t _cur_fsize; // 记录当前文件已经写入数据大小
size_t _name_count;
};
class SinkFactory
{
public:
// static LogSink::ptr create(int type)
// {
// switch (type)
// {
// case 1:
// return std::make_shared<StdoutSink>(); // 这样子编写是没有可扩展性的,只能通过修改源代码的方式进行处理
// }
// }
template<typename SinkType, typename ...Args> // 引入了模板参数之后还有问题就是不同落地类型的参数不同,有0个、一个、两个等等,因此还需要引入可变参数包
static LogSink::ptr create(Args &&...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
日志器类(Logger)设计(建造者模式)
日志器主要是用来和前端交互,当我们需要使用日志系统打印log的时候,只需要创建Logger对象调用该对象debug、info、warn、error、fatal等方法输出自己想打印的日志即可,支持解析可变参数列表和输出格式,即可以做到像使用printf函数—样打印日志。
当前日志系统支持同步日志&异步日志两种模式,两个不同的日志器唯一不同的地方在于他们在日志的落地方式上有所不同:
同步日志器:直接对日志消息进行输出。
异步日志器:将日志消息放入缓冲区,由异步线程进行输出。
因此日志器类在设计的时候先设计出一个Logger基类,在Logger基类的基础上,继承出SyncLogger同步日志器和AsyncLogger异步日志器。
且因为日志器模块是对前边多个模块的整合,想要创建一个日志器,需要设置日志器名称,设置日志输出等级,设置日志器类型,设置日志输出格式,设置落地方向,且落地方向有可能存在多个,整个日志器的创建过程较为复杂,为了保持良好的代码风格,编写出优雅的代码,因此日志器的创建这里采用了建造者模式来进行创建。
/*
日志器模块:
功能:对前面所有模块进行整合,向外提供接口完成不同等级日志的输出
管理的成员:
1. 格式化模块对象
2. 落地模块对象
3. 默认的日志器输出限制等级(大于等于限制等级的日志才能输出)
4. 互斥锁(保证日志输出过程是线程安全的,不会出现交叉日志)
5. 日志器名称(日志器的唯一标识,以便于查找)
提供的操作:
debug等级的日志输出操作(分别封装出日志消息LogMsg--各个接口日志等级不同)
info等级的日志输出操作
WARNING等级的日志输出操作
error等级的日志输出操作
fatal等级的日志输出操作
实现:
1. 抽象Logger基类(派生出同步日志器类 & 异步日志器类)
2. 因为两种不同的日志器只有落地方式不同,因此将落地操作给抽象出来
不同的日志器调用各自的落地操作进行日志落地
模块关联中使用基类指针针对子类日志器对象进行日志管理和操作
*/
/*完成日志器模块:
1. 抽象日志器基类
2. 派生出不同的子类(同步日志器类 & 异步日志器类)
*/
namespace zyqlog
{
class Logger
{
public:
using ptr = std::shared_ptr<Logger>;
Logger(const std::string &logger_name, LogLevel::value level, ForMatter::ptr &formatter, std::vector<LogSink::ptr> &sinks)
: _logger_name(logger_name)
, _limit_level(level)
, _formatter(formatter)
, _sinks(sinks.begin(), sinks.end())
{}
const std::string &name()
{
return _logger_name;
}
// 完成构造日志消息对象过程并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出
// 1. 判断当前日志是否达到了输出等级
if (LogLevel::value::DEBUG < _limit_level) {return ;}
// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vsaprintf failed!!\n";
return ;
}
va_end(ap);
serialize(LogLevel::value::DEBUG, file, line, res);
free(res);
}
void info(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出
// 1. 判断当前日志是否达到了输出等级
if (LogLevel::value::INFO < _limit_level) {return ;}
// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vsaprintf failed!!\n";
return ;
}
va_end(ap);
serialize(LogLevel::value::INFO, file, line, res);
free(res);
}
void warning(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出
// 1. 判断当前日志是否达到了输出等级
if (LogLevel::value::WARNING < _limit_level) {return ;}
// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vsaprintf failed!!\n";
return ;
}
va_end(ap);
serialize(LogLevel::value::WARNING, file, line, res);
free(res);
}
void error(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出
// 1. 判断当前日志是否达到了输出等级
if (LogLevel::value::ERROR < _limit_level) {return ;}
// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vsaprintf failed!!\n";
return ;
}
va_end(ap);
serialize(LogLevel::value::ERROR, file, line, res);
free(res);
}
void fatal(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,得到格式化后的日志消息字符串--然后进行落地输出
// 1. 判断当前日志是否达到了输出等级
if (LogLevel::value::FATAL < _limit_level) {return ;}
// 2. 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vsaprintf failed!!\n";
return ;
}
va_end(ap);
serialize(LogLevel::value::FATAL, file, line, res);
free(res);
}
protected:
void serialize(LogLevel::value level, const std::string &file, size_t line, char *str)
{
// 3. 构造LogMsg对象
LogMsg msg(level, line, file, _logger_name, str);
// 4. 通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串
std::stringstream ss;
_formatter->format(ss, msg);
// 5. 进行日志落地
log(ss.str().c_str(), ss.str().size());
}
// 抽象接口完成实际的落地输出--不同的日志器会有不同的实际落地方式
virtual void log(const char *data, size_t len) = 0;
protected:
std::mutex _mutex;
std::string _logger_name;
std::atomic<LogLevel::value> _limit_level;
ForMatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
class SyncLogger : public Logger
{
public:
SyncLogger(const std::string &logger_name, LogLevel::value level, ForMatter::ptr &formatter, std::vector<LogSink::ptr> &sinks)
: Logger(logger_name, level, formatter, sinks)
{}
protected:
void log(const char *data, size_t len) override
{
std::unique_lock<std::mutex> lock(_mutex);
if (_sinks.empty()) return;
for (auto &sink : _sinks)
{
sink->log(data, len);
}
}
};
enum class LoggerType
{
LOGGER_SYNC,
LOGGER_ASYNC,
};
/*
同步日志器输出 test
*/
int main()
{
std::string logger_name = "sync_logger";
zyqlog::LogLevel::value limit = zyqlog::LogLevel::value::WARNING;
zyqlog::Formatter::ptr fmt(new zyqlog::Formatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n"));
zyqlog::LogSink::ptr stdout_lsp = zyqlog::SinkFactory::create<zyqlog::StdoutSink>();
zyqlog::LogSink::ptr file_lsp = zyqlog::SinkFactory::create<zyqlog::FileSink>("./logfile/test.log");
zyqlog::LogSink::ptr roll_lsp = zyqlog::SinkFactory::create<zyqlog::RollBySizeSink>("./logfile/roll-", 1024*1024);
std::vector<zyqlog::LogSink::ptr> sinks = {stdout_lsp, file_lsp, roll_lsp};
// std::vector<zyqlog::LogSink::ptr> sinks = {stdout_lsp};
zyqlog::Logger::ptr logger(new zyqlog::SyncLogger(logger_name, limit, fmt, sinks));
// // 测试
logger->debug(__FILE__, __LINE__, "%s", "测试日志");
logger->info(__FILE__, __LINE__, "%s", "测试日志");
logger->warning(__FILE__, __LINE__, "%s", "测试日志");
logger->error(__FILE__, __LINE__, "%s", "测试日志");
logger->fatal(__FILE__, __LINE__, "%s", "测试日志");
size_t count = 0;
size_t cursize = 0;
while (cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
}
从上面的同步日志器的测试可以得到,我们需要先构建出一个一个的零部件然后在使用日志器进行落地,这样用户使用起来会变得非常的不方便,因此这里使用建造者模式来建造日志器,不要让用户直接去构造日志器。
/*使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用复杂度*/
// 1. 抽象一个日志器建造者类
// 1. 设置日志器类型
// 2. 将不同类型日志器的创建放到同一个日志器建造者类中完成
class LoggerBuilder
{
public:
LoggerBuilder()
: _logger_type(LoggerType::LOGGER_SYNC)
, _limit_level(LogLevel::value::DEBUG)
, _looper_type(AsyncType::ASYNC_SAFE)
{}
void buildLoggerType(LoggerType type)
{
_logger_type = type;
}
void buildEnableUnSafeAsync()
{
_looper_type = AsyncType::ASYNC_UNSAFE;
}
void buildLoggerName(const std::string &name)
{
_logger_name = name;
}
void buildLoggerLevel(LogLevel::value level)
{
_limit_level = level;
}
// 设置输出规则
void buildFormatter(const std::string &pattern)
{
_formatter = std::make_shared<ForMatter>(pattern);
}
template<typename SinkType, typename ...Args> // 可能会存在多个方向的落地因此使用vector来进行记录
void buildSink(Args &&...args)
{
LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(psink);
}
virtual Logger::ptr build() = 0;
protected:
AsyncType _looper_type;
LoggerType _logger_type;
std::string _logger_name;
LogLevel::value _limit_level;
ForMatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
// 2. 派生出具体的建造者类---局部日志器的建造者 & 全局的日志器建造者 (后面添加全局单例管理器之后,将日志器添加全局管理)
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
assert(!_logger_name.empty()); // 必须有日志器名称
if (_formatter.get() == nullptr) // 用户没有传入日志器的输出格式,需要给以一个默认格式
{
_formatter = std::make_shared<ForMatter>();
}
if (_sinks.empty()) // 用户没有指定落地方式,这里有我们自己进行指定
{
buildSink<StdoutSink>();
}
if (_logger_type == LoggerType::LOGGER_ASYNC)
{
return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
}
else
{
return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
}
};
// 日志器建造者类test
int main()
{
std::unique_ptr<zyqlog::LoggerBuilder> builder(new zyqlog::LocalLoggerBuilder());
builder->buildLoggerLevel(zyqlog::LogLevel::value::WARNING); // 这里对零件的顺序没有要求,因此不需要指挥者--指挥零件按照什么样的顺序进行构造
builder->buildLoggerName("sync_logger");
builder->buildLoggerType(zyqlog::LoggerType::LOGGER_SYNC);
builder->buildFormatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n");
builder->buildSink<zyqlog::FileSink>("./logfile/test.log");
builder->buildSink<zyqlog::RollBySizeSink>("./logfile/roll-", 1024*1024);
zyqlog::Logger::ptr logger = builder->build();
// 测试
logger->debug(__FILE__, __LINE__, "%s", "测试日志");
logger->info(__FILE__, __LINE__, "%s", "测试日志");
logger->warning(__FILE__, __LINE__, "%s", "测试日志");
logger->error(__FILE__, __LINE__, "%s", "测试日志");
logger->fatal(__FILE__, __LINE__, "%s", "测试日志");
size_t count = 0;
size_t cursize = 0;
while (cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
}