📟作者主页:慢热的陕西人
🌴专栏链接:C++基于多设计模式下的同步&异步日志系统
📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言
主要内容日志项目的输出格式化类的设计和日志落地(LogSink)类设计(简单工厂模式)
⽇志格式化(Formatter)类主要负责格式化⽇志消息。其主要包含以下内容
pattern成员:保存⽇志输出的格式字符串。
◦ %d⽇期
◦ %T缩进
◦ %t线程id
◦ %p⽇志级别
◦ %c⽇志器名称
◦ %f⽂件名
◦ %l⾏号
◦ %m⽇志消息
◦ %n换⾏
std::vectorFormatItem::ptritems成员:⽤于按序保存格式化字符串对应的⼦格式化对象
FormatItem
类主要负责⽇志消息⼦项的获取及格式化。其包含以下⼦类
MsgFormatItem:
表⽰要从LogMsg
中取出有效⽇志数据LevelFormatItem:
表⽰要从LogMsg
中取出⽇志等级NameFormatItem:
表⽰要从LogMsg
中取出⽇志器名称ThreadFormatItem:
表⽰要从LogMsg
中取出线程IDTimeFormatItem:
表⽰要从LogMsg
中取出时间戳并按照指定格式进⾏格式化 CFileFormatItem:
表⽰要从LogMsg
中取出源码所在⽂件名CLineFormatItem:
表⽰要从LogMsg
中取出源码所在⾏号TabFormatItem:
表⽰⼀个制表符缩进NLineFormatItem:
表⽰⼀个换⾏OtherFormatItem:
表⽰⾮格式化的原始字符串⽰例:“[%d{%H:%M:%S}]%m%n”
pattern = "[%d{%H:%M:%S}] %m%n"
items = {
{OtherFormatItem(), "["},
{TimeFormatItem(), "%H:%M:%S"},
{OtherFormatItem(), "]"},
{MsgFormatItem (), ""},
{NLineFormatItem (), ""}
}
LogMsg msg = {
size_t _line = 22;
size_t _ctime = 12345678;
std::thread::id _tid = 0x12345678;
std::string _name = "logger";
std::string _file = "main.cpp";
std::string _payload = "创建套接字失败";
LogLevel::value _level = ERROR;
};
格式化的过程其实就是按次序从Msg中取出需要的数据进⾏字符串的连接的过程。
最终组织出来的格式化消息:“[22:32:54]创建套接字失败\n”
代码实现:
#ifndef __M_FMT_H__
#define __M_FMT_H__
#include"message.hpp"
#include
#include
#include
#include
namespace xupt
{
//抽象格式化子类基类
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream& out, const LogMsg &msg) = 0;
};
//派生格式化子类项---消息,等级,时间,文件名,行号,线程ID,日志器名,制表符,换行,其他
class MsgFormatItem : public FormatItem
{
public:
virtual void format(std:: ostream& out, const LogMsg &msg)
{
out << msg._payload;
}
};
class LevelFormatItem : public FormatItem
{
public:
virtual void format(std:: ostream& out, const LogMsg &msg)
{
out << LogLevel::toString(msg._level);
}
};
class TimeFormatItem : public FormatItem
{
public:
TimeFormatItem(const std::string &fmt = "%H:%M:%S"):_time_fmt(fmt){}
virtual void format(std:: ostream& out, const LogMsg &msg)
{
struct tm t;
localtime_r(&msg._ctime, &t);
char tmp[128];
strftime(tmp, 127, _time_fmt.c_str(), &t);
out << tmp;
}
private:
std::string _time_fmt;
};
class FileFormatItem : public FormatItem
{
public:
virtual void format(std:: ostream& out, const LogMsg &msg)
{
out << msg._file;
}
};
class LineFormatItem : public FormatItem
{
public:
virtual void format(std:: ostream& out, const LogMsg &msg)
{
out << msg._line;
}
};
class ThreadFormatItem : public FormatItem
{
public:
virtual void format(std:: ostream& out, const LogMsg &msg)
{
out << msg._tid;
}
};
class LoggerFormatItem : public FormatItem
{
public:
virtual void format(std:: ostream& out, const LogMsg &msg)
{
out << msg._logger;
}
};
class TabFormatItem : public FormatItem
{
public:
virtual void format(std:: ostream& out, const LogMsg &msg)
{
out << "\t";
}
};
class NewLineFormatItem : public FormatItem
{
public:
virtual void format(std:: ostream& out, const LogMsg &msg)
{
out << "\n";
}
};
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string str):_str(str){}
virtual void format(std:: ostream& out, const LogMsg &msg)
{
out << _str;
}
private:
std::string _str;
};
class Formatter
{
public:
Formatter(const std::string & pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
:_pattern(pattern)
{
assert(parsePattern()); //如果解析失败直接跳断言
}
//对msg进行格式化
void format(std::ostream& out ,const LogMsg & Msg)
{
for(auto& item : _items)
{
item->format(out, Msg);
}
}
std::string format(const LogMsg & Msg)
{
std::stringstream ss;
format(ss, Msg);
return ss.str();
}
private:
//对格式化规则字符串进行解析
bool parsePattern()
{
//1.对格式化规则字符串进行解析
//abcd[%d{%H:%M:%S}][%p]%T%m%n
std::vector<std::pair<std::string, std::string>> fmt_order;
size_t pos = 0;
std::string key, val;
while(pos < _pattern.size())
{
//1.处理原始字符串,如果不是%,那么就是原始字符
if(_pattern[pos] != '%')
{
val.push_back(_pattern[pos++]);
continue;
}
//%的情况
//1.双%的情况
if(pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
{
val.push_back('%'); pos += 2; continue;
}
//能走到这里,代表%后是一个格式化字符串,这时候代表原始字符串处理完毕
if(val.empty() == false)
{
fmt_order.push_back(std::make_pair("", val));
val.clear();
}
//2.%后是格式化字符的情况
pos += 1;//这一步之后,pos指向格式化字符位置
if(pos == _pattern.size())
{
std::cout << "%后没有对应的格式化字符" << std::endl;
return false;
}
key = _pattern[pos];
//2.{}的情况
pos += 1;
if(_pattern[pos] == '{')
{
pos += 1; //这时候pos指向子规则的起始位置
while(pos < _pattern.size() && _pattern[pos] != '}')
{
val.push_back(_pattern[pos++]);
}
//走到了末尾跳出了循环,则代表没有遇到},代表格式是错误的
if(pos == _pattern.size()) //没有找到}
{
std::cout << "子规则{}匹配失败" << std::endl;
return false;
}
pos += 1;
}
fmt_order.push_back(std::make_pair(key, val));
key.clear();
val.clear();
}
//2.根据解析得到的数据初始化子项数组成员
for(auto &it : fmt_order)
{
_items.push_back(createItem(it.first,it.second));
}
return true;
}
//根据不同的格式化字符创建不同的格式化子项对象
FormatItem::ptr createItem(const std::string & key, const std::string &val)
{
if(key == "d") return std::make_shared<TimeFormatItem>(val);
if(key == "t") return std::make_shared<ThreadFormatItem>();
if(key == "c") return std::make_shared<LoggerFormatItem>();
if(key == "f") return std::make_shared<FileFormatItem>();
if(key == "l") return std::make_shared<LineFormatItem>();
if(key == "p") return std::make_shared<LevelFormatItem>();
if(key == "T") return std::make_shared<TabFormatItem>();
if(key == "m") return std::make_shared<MsgFormatItem>();
if(key == "n") return std::make_shared<NewLineFormatItem>();
if(key.empty()) return std::make_shared<OtherFormatItem>(val);
std::cout << "没有对应的格式化字符串:%" << key << std::endl;
abort();
return FormatItem::ptr();
}
private:
std::string _pattern; //格式化规则字符串
std::vector<FormatItem::ptr> _items;
};
}
#endif
⽇志落地类主要负责落地⽇志消息到⽬的地
它主要包括以下内容:
目前实现了三个不同方向上的日志落地:
标准输出:StdoutSink
固定文件:FileSink
滚动文件:RollSinkBySize
◦ 滚动⽇志⽂件输出的必要性:
• 由于机器磁盘空间有限,我们不可能⼀直⽆限地向⼀个⽂件中增加数据
• 如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨⼤,也不利于查找我们需要的信息
• 所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如1GB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。对于那些过期的⽇志,⼤部分企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过期⽇志
◦ ⽇志⽂件的滚动思想:
⽇志⽂件滚动的条件有两个:⽂件⼤⼩和时间。我们可以选择:
本项⽬基于⽂件⼤⼩的判断滚动⽣成新的⽂件
/*
日志落地模块的实现
1.抽象落地基类
2.派生落地子类(根据不同的落地方向)
3.使用工厂模式进行使用和生产的分离
*/
#ifndef __M_SIK_H__
#define __M_SIK_H__
#include"util.hpp"
#include
#include
#include
#include
namespace xupt
{
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:
virtual 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());
}
//写入到文件
virtual void log(const char* data, size_t len)
{
_ofs.write(data, len);
assert(_ofs.good());
}
private:
std::string _pathname;
std::ofstream _ofs;
};
//落地方向:滚动文件(以大小进行滚动)
class RollSinkBySize : public LogSink
{
public:
//构造函数,传入文件名,文件大小,并管理文件输出句柄
RollSinkBySize(const std::string &basename, size_t max_size):
_basename(basename), _max_size(max_size),_cur_size(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());
}
//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件
virtual void log(const char* data, size_t len)
{
if(_cur_size >= _max_size)
{
//先关闭之前的文件
_ofs.close();
//创建新的文件
std::string pathname = createNewFile();
_ofs.open(pathname,std::ios::binary | std::ios::app);
//断言以下是否打开成功
assert(_ofs.is_open());
//置零当前文件大小
_cur_size = 0;
}
//写入?没有更改_cur_size
_ofs.write(data, len);
//断言以下是否成功写入
assert(_ofs.good());
_cur_size += len;
}
private:
//当文件大小达到限制的时候,用于切换文件使用(创建新的文件名,并不能创建文件)
std::string createNewFile()
{
//获取系统时间
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:
size_t _name_count ; //用于区别文件名的
std::string _basename; //存储日志信息的文件, 命名规则我们一般是加一个固定的头,尾部用时间来区分
std::ofstream _ofs; //维护一个我们的文件输出句柄
size_t _max_size; //单个文件的最大大小
size_t _cur_size; //当前文件的大小,算是一个优化,这样我们就不用去频繁查看文件属性,从而降低效率
};
/*
拓展一个以时间作为文件滚动切换类型的日志落地模块
*/
class RollSinkByTime : public LogSink
{
public:
enum class TimeGap
{
GAP_SECOND,
GAP_MINUTE,
GAP_HOUR,
GAP_DAY,
};
public:
RollSinkByTime(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 ? util::Date::now() : util::Date::now() % _gap_size; //初始化当前的时间片
util::File::CreateDirectory(util::File::path(_basename)); //1.创建日志文件所在的目录
std::string filename = createNewFile(); //2.创建文件名,并且打开文件
_ofs.open(filename,std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
//写入到文件,判断当前的时间段,是否是当前文件的时间段,不是则切换文件
virtual void log(const char* data, size_t len)
{
//获取系统时间
time_t cur = util::Date::now();
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 = 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 << ".log";
return filename.str();
}
private:
std::string _basename;
std::ofstream _ofs;
size_t _cur_gap;
size_t _gap_size;
};
class SinkFactory
{
public:
template<typename SinkType, typename ...Args>
static LogSink::ptr create(Args && ...args) //右值引用,保持原来类型的属性,用于后来的完美转发
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
#endif
到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正