一:项目简介
1:需求背景分析
- 生产环境的产品为了保证其可靠性稳定性,不允许开发人员使用附加调试器去排查问题,可以借助打印日志来帮助开发者分析问题
- 如果客户端已经上线,出现bug且无法复现,可以借助客户端上传至服务器的日志帮助开发者人员分析
- 对于高频操作定时器、心跳包)少量调试次数下无法触发我们想要的行为,通过断点暂停的方式,不得不重复操作几十次,甚至几百次,排查效率十分低下,可以借助日志系统来排查问题
- 分布式、多线程/多进程代码,出现bug难以定位(多线程调试过的深有体会),可以借助日志系统定位bug
- 帮助首次接触代码的新开发人员了解代码的运行流程
2:功能简介
本项目实现了一个日志系统,主要支持以下功能:
- 支持多级别的日志消息
- 支持同步&异步兼容
- 支持可靠写入日志到控制台、指定文件、滚动文件中
- 支持多线程并发写日志
- 支持扩展不同日志落地到目标地
3:开发环境
- centos 7
- vscode/vim
- g++/gdb
- Makefile
4:核心技术
- 类层次设计(继承+多态)
- C++11(包括智能指针、锁、右值引用、多线程)
- 双缓冲区
- 生产消费者模型
- 多线程
- 设计模式(单例、工厂、建造者、模板)
5:环境搭建
本项目不依赖第三方库,只需安装好Centos/Ubuntu以及vscode/vim即可开发。
6:技术实现3个方面
日志系统技术实现包括3个方面:
- 使用printf、cout等输出函数打印日志到控制台
- 对于大型商业化项目,为了方便排查问题,我们一般将日志输出到文件或者数据库系统方便分析日志,主要分为同步、异步方式
6.1:同步写日志
指的是在输出日志的时候,必须等待日志输出语句执行完毕,才能执行后面的业务逻辑语句,执行日志输出的线程和业务逻辑线程是同一个。
缺点:附带大量磁盘IO,且影响业务逻辑线程执行性能,效率低。
好处:无需采用多线程
6.2:异步写日志
指的是执行日志输出逻辑语句的线程与业务逻辑处理线程不是同一个,而是有专门的线程负责日志的输出操作,业务线程只需要把日志放入一个缓冲区(日志的生产者),不用等待即可执行后面的业务逻辑。而日志如何从缓冲区进行落地文件、控制台由单独的日志线程来完成(作为日志的消费者)。
二:项目框架设计
1:模块划分
1.1:日志等级模块-level
- UNKNOWN
- DEBUG 调试,关键信息输出
- INFO 普通提示型日志消息
- WARN 警告,不影响运行,但需要注意
- ERROR 错误,程序运行错误的日志
- FATAL 致命,导致程序无法继续推进运行的日志
1.2:消息模块-message
- 时间
- 日志等级
- 行号
- 文件名
- 线程id
- 日志器名称
- 消息有效载荷
1.3:消息格式化模块-format
- 格式字符串 默认:"[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"
- 格式化后子项集合数组std::vector<FormatItem::ptr> _items
1.4:日志消息落地模块
-
标准输出 StdoutSink
-
指定文件输出 FileSink
-
滚动文件输出 RoolBySizeSink
设计logsink基类,通过继承和重写log函数,不同的子类控制不同的落地方向。
1.5:日志器模块
对以上几个模块整合的模块
std::mutex _mutex;
std::string _logger_name;
std::atomic<LogLevel::value> _limit_level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
用户通过日志器来进行日志的输出
1.6:日志器管理模块
std::mutex _mutex;
Logger::ptr _root_logger;
std::unordered_map<std::string, Logger::ptr> _loggers;
包含多日志器,管理模块就是对创建的所有日志器统一管理,并提供一个默认日志器提供日志的输出。
1.7:异步工作线程模块
实现对日志的异步输出功能,业务线程负责将日志输入到缓冲区,异步工作线程负责将缓冲区数据落地和输出,以此提供更高效的非阻塞日志输出。
2:代码设计
2.1:实用类设计-util.hpp
/*
实用工具类的实现:
1:获取系统时间
2:判断文件是否存在
3:获取文件所在路径
4:创建目录
*/
#ifndef __MY_UTIL_H__
#define __MY_UTIL_H__
#include<iostream>
#include<ctime>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
namespace wjwlog
{
namespace util
{
class Date
{
public:
static size_t Now()
{
return (size_t)time(nullptr);
}
};
class File
{
public:
static bool exists(const std::string &pathname)
{
// return access(pathname.c_str(),F_OK)==0;//无法跨平台
struct stat st;
if(stat(pathname.c_str(),&st)<0)return false;
else return true;
}
static std::string path(const std::string &pathname)//a/b/c.txt
{
size_t pos = pathname.find_last_of("/\\");//windows下是\,需要转义
if(pos == std::string::npos)
{
return ".";//没有找到,说明在当前目录
}
else
{
return pathname.substr(0,pos+1);//第二个参数表示长度,需要带上pos,就得+1
//这里带上/的好处是,后面我们在创建父级目录的时候,需要不断的寻找/,因此要加上
//其实不加上也是可以的,因为我们可以直接通过npos位置的mkdir创建目录的。
}
}
static void createDirectory(const std::string &pathname)//这些pathname都代表的是文件+路径名
{
// ./abc/def/test.txt
size_t pos=0,idx=0;
while(idx<pathname.size())
{
pos = pathname.find_first_of("/\\",idx);
if(pathname == ".")
{
break;
}
else if(pos==std::string::npos)
{
mkdir(pathname.c_str(),0777);
break;
}
else
{
//一路建立父级目录
std::string parent_dir = pathname.substr(0,pos+1);
if(exists(parent_dir)==true)
{
//已经存在
idx=pos+1;
continue;
}
else
{
mkdir(parent_dir.c_str(),0777);
idx=pos+1;
}
}
}
}
};
}
}
#endif
pathname是文件路径+文件名,在该类中实现了如下功能:
- exists: 根据pathname判断文件是否存在
- createDirectory: 根据pathname创建父级目录
- path: 根据pathname,提取父级目录。
2.2: 日志等级类设计-level.hpp
//枚举出日志等级
//把对应的日志等级转化为对应的字符串
#ifndef __MY_LEVEL_H__
#define __MY_LEVEL_H__
namespace wjwlog
{
class LogLevel
{
public:
enum class value
{
UNKNOWN=0,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
OFF
};
static const char* toString(LogLevel::value level)//不需要建立对象再来访问这个函数
{
switch(level)
{
//也可以使用预处理的宏来完成这里的过程
#define TOSTRING(name) #name//表示输出name的字符串形式
//例子:case Loglevel::value::DEBUG: return TOSTRING(DEBUG);
//写完case后还需要输入#undef TOSTRING
case LogLevel::value::DEBUG:return "DEBUG";
case LogLevel::value::INFO:return "INFO";
case LogLevel::value::WARN:return "WARN";
case LogLevel::value::ERROR:return "ERROR";
case LogLevel::value::FATAL:return "FATAL";
case LogLevel::value::OFF:return "OFF";
}
return "UNKNOWN";
}
};
}
#endif
2.3:日志消息类设计-message.hpp
#ifndef __MY_MSG_H__
#define __MY_MSG_H__
#include"level.hpp"
#include"util.hpp"
#include<iostream>
#include<thread>
#include<string>
namespace wjwlog
{
struct LogMsg
{
time_t _ctime;//日志产生的时间戳
LogLevel::value _level;//日志等级
size_t _line;//行号
std::thread::id _tid;//线程id
std::string _file;//文件名
std::string _logger;//日志器名称
std::string _payload;//日志消息有效载荷
LogMsg(LogLevel::value level,
size_t line,
const std::string file,
const std::string logger,
const std::string msg)
:_ctime(util::Date::Now())
,_level(level)
,_line(line)
,_tid(std::this_thread::get_id())
,_file(file)
,_logger(logger)
,_payload(msg){}
};
}
#endif
该类主要是封装一条日志消息的各个信息:时间、日志器名称、日志等级、文件名、行号、有效载荷。
2.4:日志输出格式化类设计-format.hpp
#ifndef __M_FMT_H__
#define __M_FMT_H__
#include "level.hpp"
#include "message.hpp"
#include <ctime>
#include <vector>
#include <cassert>
#include <string>
#include <sstream>
#include <utility>
//该类负责把一个msg 对象转化为格式化字符串,通过对pattern([%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n)这种字符串
//进行解析为key,val的pair数组,然后根据key调用createitem这种私有接口,创建出不同的子项派生类指针,然后放到成员vector中的每一个基类指针中。
//然后遍历vector,调用基类指针和多态原理指向的format函数,输出到out中,最后以一个format的重载函数,把out定义为stringstream对象追加进去。
//stringstream的str()就是最初msg对象格式化后的字符串结果。
namespace wjwlog
{
// 抽象格式化子项基类
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream &out, LogMsg &msg) = 0;
};
// 派生格式化子项子类:消息、等级、时间、文件名、行号、线程ID、日志器名称、制表符、换行、其他。
class MsgFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._payload;
}
};
class LevelFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << wjwlog::LogLevel::toString(msg._level);
}
};
class TimeFormatItem : public FormatItem
{
public:
TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt)
{
}
void format(std::ostream &out, LogMsg &msg) override
{
struct tm t;
localtime_r(&msg._ctime, &t);
char tmp[32] = {0};
strftime(tmp, 31, _time_fmt.c_str(), &t);
out << tmp;
}
private:
std::string _time_fmt;
};
class FileFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._file;
}
};
class LineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._line;
}
};
class ThreadFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._tid;
}
};
class LoggerFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._logger;
}
};
class TabFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << "\t";
}
};
class NLineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << "\n";
}
};
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string &str) : _str(str) {}
void format(std::ostream &out, LogMsg &msg) override
{
out << _str;
}
private:
std::string _str;
};
class Formatter
{
public:
/*
%d表示日期,包含子格式{%H:%M:%S}
%t表示线程id
%c表示日志器名称
%f:%l表示文件名:行号
%p表示日志级别
%T表示水平制表符号
%m表示消息主体
%n表示换行
*/
using ptr = std::shared_ptr<Formatter>;
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, LogMsg &msg)
{
for (auto &item : _items)
{
item->format(out, msg);
}
}
std::string format(LogMsg &msg)
{
std::stringstream ss;
format(ss, msg);
return ss.str();
}
// 对格式化规则字符串解析
bool parsePattern()
{
std::vector<std::pair<std::string, std::string>> fmt_order;
size_t pos = 0;
std::string key, val;
while (pos < _pattern.size())
{
if (_pattern[pos] != '%')
{
// 不是'%',就是原始字符
val.push_back(_pattern[pos++]);
continue;
}
// 走下来就是'%'
if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
{
// 如果是转义字符
val.push_back('%');
pos += 2;
continue;
}
// 走到这说明后面是个格式化字符,不过也有可能是pos+1的位置是末尾的下一个字符了。但是我们下面有判断的
if (val.empty() == false)
{
// 如果这前面有内容,也就是非格式化字符串,那就添加一下前面的非格式化字符串,因为有可能是%d这种直接的形式。
fmt_order.push_back(std::make_pair("", val));
val.clear();
}
// 走到这,此时pos还在%的位置
pos += 1;
if (pos == _pattern.size())
{
std::cout << "%之后没有对应的格式化字符" << std::endl;
return false; // 和之前的pos+1呼应了
}
key = _pattern[pos]; // 对应的就是格式化字符串,比如d,m这些
pos += 1;
if (pos < _pattern.size() && _pattern[pos] == '{')
{
// 走到这说明走到了子格式
pos += 1; // 此时是在{的后面,也就是子格式的起始位置
while (pos < _pattern.size() && _pattern[pos] != '}')
{
val.push_back(_pattern[pos++]); // 日期,的val对应的是子格式里面的值
}
if (pos == _pattern.size()) // 如果pos走到了结尾
{
std::cout << "子格式{}匹配出错" << std::endl;
return false;
}
pos += 1; // 此时在},要让他往下一个位置继续判断
}
// 此时我们的一个格式就已经创建好了
fmt_order.push_back(std::make_pair(key, val)); // 对于%g,走到这里是g和空值,但是不影响,我们后面creatitem的时候,会返回一个other对象
key.clear();
val.clear();
}
// 2:根据解析得到的数据初始化数组成员
for (auto &it : fmt_order)
{
_items.push_back(creatItem(it.first, it.second));//通过key的不同创建不同的对象,但是统一父类指针接受
}
return true; // 只有%d和普通字符串需要有val的值,其他的都没有,val都是空的
}
private:
// 根据不同格式化字符创建不同格式化子项对象
FormatItem::ptr creatItem(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 == "p")
return std::make_shared<LevelFormatItem>();
if (key == "l")
return std::make_shared<LineFormatItem>();
if (key == "T")
return std::make_shared<TabFormatItem>();
if (key == "m")
return std::make_shared<MsgFormatItem>();
if (key == "n")
return std::make_shared<NLineFormatItem>();
if (key.empty())
return std::make_shared<OtherFormatItem>(val);
// 例如%g这种没有定义过的就添加不进去
std::cout << "没有对应的格式化字符串"
<< "%" << key << std::endl;
return FormatItem::ptr();
}
private:
std::string _pattern; // 格式化规则字符串
std::vector<FormatItem::ptr> _items;
};
}
#endif
成员包括:
- std::string _pattern; // 格式化规则字符串
- std::vector<FormatItem::ptr> _items;
pattern默认格式为:"[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"
根据不同的规则(%?)把消息对象中的各个部分创建成各个子对象。
具体过程:
- 创建抽象基类,formatitem,提供纯虚函数format(ostream& out,Logmessage& msg)
- 继承抽象基类,定义各个子项派生类,重写format函数
- 定义Formatter类
- 定义parsepattern函数,功能是按照外界上传的pattern格式化字符串进行解析,得到key value的pair,比如adbsad,那么key是空,val是原始字符adbsad;比如%mabcd%k,会创建key是m,val为空,key是空,val是abcd,key是k,val是空的3个pair。
- 定义一个存放pair的数组fmt_order,将上面第四步得到的结果全部添加到该数组中,然后遍历该数组,调用一个createitem的函数,把根据key创建的子项item指针添加到formatitem指针数组中。
- createitem是根据key值,创建不同的子项对象指针(指针也可以进行多态),other对象是key为空,而走完所有的if后,都没有成功创建的就是没有定义的格式,此时会创建formatitem(子项基类)的空指针,等于不添加。
- 定义format函数,参数是(ostream &out,Logmessage &msg),过程是:遍历formatitem数组,用每个formatitem指针调用 不同子类的format接口。
- 定义format函数,参数是(Logmessage &msg),相当于再次封装了一下第7步的format,返回值是msg格式化后的字符串。
总结:基类formatitem,派生不同子类,Formatter工具类负责把外界的pattern格式化规则字符串解析并且创建不同的子类指针,这些指针存放于类型为formatitem指针的数组中,利用多态,遍历数组调用重写的虚函数format,最后封装上面一步步骤,直接返回format后的字符串。
2.5:日志落地类设计-sink.hpp
#ifndef __MY_SINK_H__
#define __MY_SINK_H__
#include "util.hpp"
#include <sstream>
#include <fstream>
#include <memory>
#include <cassert>
namespace wjwlog
{
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); // 这个是写入到cout里面
}
};
class FileSink : public LogSink
{
public:
FileSink(const std::string &pathname)
: _pathname(pathname)
{
/*
如果使用了输出流的 std::ios::out 模式,则会创建一个新的空文件。
如果使用了输出流的 std::ios::app(追加模式)模式,则会尝试在文件末尾写入内容,如果文件不存在则会创建一个新文件。
如果使用了输出流的 std::ios::in 模式,打开一个不存在的文件将导致失败。*/
// 首先创建目录,pathname 是目录+文件名
util::File::createDirectory(util::File::path(pathname));
_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)
{
//第一次要初始化的
std::string pathname = createNewFile();
util::File::createDirectory(util::File::path(pathname));
_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; // 创建好新文件,当前文件大小要置0
}
_ofs.write(data, len);
assert(_ofs.good());
_cur_fsize += len;
}
private:
std::string createNewFile() // 进行大小判断,超过就创建新文件
{
time_t t = util::Date::Now();
struct tm lt;
localtime_r(&t, <);
std::stringstream ss;
ss << _basename;
ss << lt.tm_year + 1900;
ss << lt.tm_mon + 1;
ss << lt.tm_mday;
ss << lt.tm_hour;
ss << lt.tm_min;
ss << lt.tm_sec;
ss << "-";
ss << _name_count++;
ss << ".log";
return ss.str();
}
private:
size_t _name_count;
std::string _basename; // 通过基础文件名+扩展文件名组成一个实际的当前文件名 ./logs/base- > ./logs/base-20240401.txt
std::ofstream _ofs;
size_t _max_fsize; // 记录最大大小,文件大小超过就要切换文件
size_t _cur_fsize; // 记录当前文件已经写入的文件大小
};
class SinkFactory
{
public:
template <class SinkType, class... Args> // 创建实例不需要把不定参数放到<>中
static LogSink::ptr create(Args &&...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
#endif
设计3种落地器:分别落实到控制台、指定文件、滚动文件。每个类维护一个ofstream对象,调用各自的log(char* data,size_t len),把data数据写入到ofstream中。
对于滚动文件落地器:只需要提供一个基础文件名,调用自己私有成员函数createnewfile来创建完整文件名,我们这里的思想是基础文件名+时间信息组成完整文件名。当当前大小超过最大size就创建新文件,所以是滚动日志。
然后定义工厂类,使用函数模板和不定参数,根据sinktype的不同,创建不同的落地器指针。
2.6:日志器类设计-logger.hpp
#ifndef __MY_LOG_H__
#define __MY_LOG_H__
#include "util.hpp"
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "looper.hpp"
#include <cstdarg>
#include <cassert>
#include <thread>
#include <mutex>
#include <atomic>
#include <unordered_map>
namespace wjwlog
{
class Logger
{
public:
using ptr = std::shared_ptr<Logger>;
Logger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formater, std::vector<LogSink::ptr> &sinks)
: _logger_name(logger_name),
_limit_level(level),
_formatter(formater),
_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 << "vasprintf 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 << "vasprintf 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::WARN < _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 << "vasprintf failed\n";
return;
}
va_end(ap);
serialize(LogLevel::value::WARN, 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 << "vasprintf 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 << "vasprintf 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)
// str是不定参的字符串-相当于msg对象的消息主体
{
// 3:用日志消息的字符串构造logmsg对象
LogMsg msg(level, line, file, _logger_name, str); // res:char*->string 隐式类型转换
// 4:通过格式化工具对logmsg格式化,得到格式化后的日志字符串
std::stringstream ss;
_formatter->format(ss, msg);
// format就是把msg对象格式化输出成字符串到ss中
// 还是先根据format对象中的pattern字符串来解析并构造keyvalue的formatitem对象
// 然后调用formatitem对象数组中每个对象的format函数,写msg内容到ss中
// 5:日志落地
log(ss.str().c_str(), ss.str().size()); // stringstream.str返回的是string类型
// 到时候这里调用是根据日志器的类型来调用不同类型日志器的log函数,比如同步就调用syn的log
}
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; // 因为要落地到不同方向,所以用父类指针数组保存就行
/*
debug、warn这些接口,自己已经有了level了,所以参数只需要是line和file,还有一个不定参数,但是不定参数要提取,
所以不定参数前面还需要有个格式化字符串,用来使用vasprintf提取出来不定参数,然后提取到char* res中,然后调用serialize接口
用来构造一个msg对象,参数就是level,line,file,和消息字符串char* str,level是不同级别的接口如debug、warn这些函数内部自己上传参数
loglevel::value::**等,然后str会调用string的单参构造函数进行隐式类型转换,此时msg对象就构造好了。
日志器内部有format对象,到时候我们在外面初始化日志器的时候,可以选择对format进行默认构造还是显示构造,构造就是对format里面的格式字符串
进行初始化,我们有缺省值,所以可以传或者不传,然后format去调用其中的2个参数(ostream&out,logmessage msg);out给stringstream类型的对象ss
然后这个函数会把msg转化到ss里面的字符串str();
然后我们在基类的logger日志器里面有一个虚函数log,在syn同步日志器中重写了这个虚函数(当我们第一次创建不同类型的日志器的时候,
serialize会调用不同日志器的log,因为是虚函数重写的),不同日志器的log会遍历我们的sink落地数组,调用sink里面
每个落地方式中的log方法void log(const char *data, size_t len),每个sink数组到时候自己在外面定义就行,sink对象的log方法会对不同ofstream
对象进行写data数据,而ofstream会打开不同的文件。 这样一来,日志器就调用完成sink中的log函数了,然后free掉不定参数的char* res就行。
总结来说:logger中不同的输出等级接口内部: 构造msg对象,logger成员format调用fmt对msg对象格式化输出到stringstream对象ss中的str()字符串中。
遍历sink数组,调用每个对象的log接口(ss.str().c_str().ss.str().size());负责不同的落地方式。
*/
};
// 同步日志器
class SynLogger : public Logger
{
public:
SynLogger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formater, std::vector<LogSink::ptr> &sinks)
: Logger(logger_name, level, formater, sinks) {}
protected:
void log(const char *data, size_t len)
{
// 在外面构造的时候,确定好有哪些落地方式,构成方法数组sinks,遍历数组调用log,把上面的msg格式化后的结果ss进行落地!
// 同步日志器,需要对写日志过程加锁
std::unique_lock<std::mutex> lock(_mutex);
if (_sinks.empty())
return;
for (auto &sink : _sinks)
{
sink->log(data, len);
}
}
};
class AsyncLogger : public Logger
{
public:
AsyncLogger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formater, std::vector<LogSink::ptr> &sinks,
ASYNC_TYPE looper_type)
: Logger(logger_name, level, formater, sinks),
_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::reallog, this, std::placeholders::_1), looper_type)) {}
void log(const char *data, size_t len) // 将数据写入缓冲区
{
_looper->push(data, len);
}
// 设计一个实际落地函数,也就是消费缓冲区中的callback函数
void reallog(Buffer &buf)
{
if (_sinks.empty())
return;
for (auto &sink : _sinks)
{
sink->log(buf.begin(), buf.readAbleSize());
}
}
private:
AsyncLooper::ptr _looper;
};
// 使用建造者模式构建logger日志器,而不是让用户构造,简化用户使用的复杂度。
// 抽象一个日志器建造者类 1:设置日志器类型 2:不同类型的日志器创建放到一个建造者类中
enum LoggerType
{
LOGGER_SYNC,
LOGGER_ASYNC
};
class LogBuilder
{
public:
LogBuilder() : _logger_type(LoggerType::LOGGER_SYNC),
_limit_level(LogLevel::value::DEBUG),
_looper_type(ASYNC_TYPE::ASYNC_SAFE) {}
void buildLoggerType(LoggerType type)
{
_logger_type = type;
}
void buildEableUnsafeAsync()
{
_looper_type = ASYNC_TYPE::ASYNC_UNSAFE;
}
void buildLoggerName(const std::string &name)
{
_logger_name = name;
}
void buildLevel(LogLevel::value level)
{
_limit_level = level;
}
void buildFormatter(const std::string &pattern)
{
_formatter = std::make_shared<Formatter>(pattern);
}
template <class SinkType, class... Args>
void buildSink(Args &&...args)
{
LogSink::ptr s = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(s);
}
virtual Logger::ptr build() = 0;
protected:
ASYNC_TYPE _looper_type;
LoggerType _logger_type;
std::string _logger_name;
LogLevel::value _limit_level;
Formatter::ptr _formatter; //
std::vector<LogSink::ptr> _sinks; // 因为要落地到不同方向,所以用父类指针数组保存就行
};
// 如果没有loggerbuilder类,那我们就要自己创建loggertype这些参数,然后构造给logger对象
// 所以我们要构造一个类,提供接口构造,logger对象需要的成员变量。
// loggertype默认我们会给同步日志器,等级给debug *****(等级+类型 缺省)
// local这个全局构造器,直接调用build接口,返回一个logger基类指针。
// 总结:创建logbuilder类型的智能指针,用localloggerbuilder对象构造
// 调用指针内继承过来的接口 初始化成员变量
// localbuilder内部对成员变量是否有初始化检查,判断日志器的type,返回不同的(同步异步)日志器。
class LocalLoggerBuilder : public LogBuilder
{
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);
}
return std::make_shared<SynLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
};
class LogManager
{
public:
static LogManager& getInstance()
{
static LogManager eton;
return eton; // c++11后局部静态变量线程安全的,没有构造完成,其他线程会进入阻塞
}
void add(Logger::ptr &logger)
{
if (hasLogger(logger->name()))
return;
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(std::make_pair(logger->name(), logger));
}
bool hasLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
return false;
return true;
}
Logger::ptr getLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
return Logger::ptr();
return it->second;
}
Logger::ptr rootLogger()
{
return _root_logger;
}
private:
LogManager() //默认初始化默认logger也就是root_logger
{
//这里不能用global,否则会死循环,在构造logmanager的时候,builder->build需要使用单例对象,但是此时managaer还在构造,
std::unique_ptr<wjwlog::LogBuilder> builder(new wjwlog::LocalLoggerBuilder());
//指定名字就足够了,new完对象后基类初始化列表和派生类的build都对其他参数进行了一个初始化。
builder->buildLoggerName("root");
_root_logger = builder->build();
_loggers.insert(std::make_pair("root",_root_logger));
}
private:
std::mutex _mutex;
Logger::ptr _root_logger;
std::unordered_map<std::string, Logger::ptr> _loggers;
};
//外界调用全局日志器建造者的话,好处是构造好日志器后,不需要自己手动调用getinstance().add(),进行添加日志器到单例对象了,
//我们只需要在build里面添加一个把日志器添加到单例对象的功能就行。
class GlobalLoggerBuilder : public LogBuilder
{
public:
Logger::ptr build() override
{
// 每个日志器必须有名称
assert(!(_logger_name.empty()));
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
if (_sinks.empty())
{
buildSink<StdoutSink>();
}
Logger::ptr logger;
if (_logger_type == LoggerType::LOGGER_ASYNC)
{
logger = std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
}
logger = std::make_shared<SynLogger>(_logger_name, _limit_level, _formatter, _sinks);
LogManager::getInstance().add(logger);
return logger;
}
};
}
#endif
日志器成员变量:
std::mutex _mutex;
std::string _logger_name;
std::atomic<LogLevel::value> _limit_level;
Formatter::ptr _formatter; //
std::vector<LogSink::ptr> _sinks; // 因为要落地到不同方向,所以用父类指针数组保存就行
注意日志器是有名字的,我们使用什么日志器取决于查找哪个日志器名字。
一个日志器应该负责不同级别的输出,所以需要提供debug、info、warn、error、fatal这些接口
参数都是行号、文件名、格式化规则字符串、不定参数。在serialize函数之前负责把不定参数写入到一个字符串中res中。
日志器定义为一个抽象基类,需要重写log函数。
不同级别的输出函数中共同调用serialize函数,用自身的日志级别、行号、文件名、 前面的res字符串构成一个message对象,然后调用成员formatter格式化字符串到stringstream流中。接着调用重写的函数log。
接下来:设计两个继承logger的派生类----同步日志器和异步日志器
- 同步日志器:log直接遍历自己的sink数组,调用每个sink中的log就行。
- 异步日志器:需要用到双缓冲区,而工作线程处理数据(把数据进行落地)又需要回调函数,所以这个落地函数就由日志器外部提供,也就是reallog,同样的也是遍历sink,调用sink中的log。
接着定义一个建造日志器的类,负责提供接口来初始化日志器中的成员。如果没有这个类,我们就要自己定义日志器类包含的成员,然后再去构造日志器类。
其中buildsink使用模板函数,从create->buildsink
wjwlog::LogSink::ptr File_lsp = wjwlog::SinkFactory::create<wjwlog::FileSink>("./logfile/zdfile.log");
builder->buildSink<wjwlog::FileSink>("./logfile/File.log");
接着定义纯虚函数build,要求必须重写。
接下来定义局部日志器建造者类,继承建造者。
如果我们的formatter没有初始化,就初始化一下(成员pattern就会使用默认的规则字符串,所以默认日志器都是默认规则),如果落地日志器数组是空,那就默认填充输出到控制台的落地器。然后根据日志器的类型是同步还是异步(默认是同步),创建不同的指针。返回的是不同日志器的指针,但都用logger的基类指针接受。
接着创建日志器管理类:
- 成员有默认日志器和以日志器名字和日志器指针构成的哈希表
- 采用单例模式中的简单单例(局部静态变量)
- 提供添加日志器到哈希表
- 提供通过日志器名字判断日志器是否存在函数
- 通过日志器名字返回该日志器
- 返回默认日志器
日志器管理类的构造函数负责初始化默认日志器,首先创建一个局部日志器建造者对象,调用buildname构造root日志器名字,然后build会创建默认值(stdoutsink这些),然后吧root和build返回的指针添加到哈希表中。
如果只提供到这里,外界用户使用日志器管理的时候,创建好日志器后,需要手动调用管理类中的getinstance.add(logger& log),比较麻烦,所以我们再写一个全局建造者类,继承logger。
只需要在build函数中定义一个临时指针logger::ptr,根据loggertype不同创建不同的指针赋值给这个ptr(默认是同步日志器),然后把ptr添加到哈希表中。
2.7:异步日志器双缓冲区类设计-looper.hpp
#ifndef __MY_LOOP_H__
#define __MY_LOOP_H__
#include"buffer.hpp"
#include<condition_variable>
#include<mutex>
#include<thread>
#include<memory>
#include<functional>
#include<atomic>
namespace wjwlog
{
enum class ASYNC_TYPE{
ASYNC_SAFE=0,//缓冲区满了就阻塞
ASYNC_UNSAFE//不考虑资源耗尽,就是扩容,常用性能测试
};
class AsyncLooper
{
public:
using Functor = std::function<void(Buffer&)>;
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor& cb,ASYNC_TYPE looper_type=ASYNC_TYPE::ASYNC_SAFE):
_callBack(cb),
_looper_type(looper_type),
_stop(false),
_thread(std::thread(&AsyncLooper::threadEntry,this))//线程入口函数有隐藏参数this
{}
~AsyncLooper(){stop();}
void stop()
{
_stop = true;
_cond_con.notify_all();//唤醒所有工作线程
_thread.join();
}
void push(const char* data,size_t len)
{
//无限扩容不安全,采用定长
{std::unique_lock<std::mutex> lock(_mutex);
//有数据可以写,生产缓冲区就写,写不了就阻塞
if(_looper_type==ASYNC_TYPE::ASYNC_SAFE)//非阻塞的话不用生产者去阻塞
_cond_pro.wait(lock,[&](){return _pro_buf.writeAbleSize()>=len;});
_pro_buf.push(data,len);//1
}
_cond_con.notify_one();
}
private:
//工作线程,对消费缓冲区数据 进行回调函数处理,初始化缓冲区,交换缓冲区
void threadEntry()
{
while(1)//如果添加了处理残留数据的处理,这里就必须变成死循环了
{
{
//一定要加这个括号,表示一个生命周期范围
//加的原因是,交换完后,如果不放锁,那只能在我处理完数据后写到文件后 ,生产者才能生产。
//不能对数据处理过程也进行保护,不然变成串行化了。
//判断生产缓冲区有没有数据,有就交换,没有阻塞
std::unique_lock<std::mutex> lock(_mutex);
//如果这里没 有stop,我们在工作器停止,也就是析构后,stop变为true了,还会唤醒所有工作线程,那样的话工作线程又继续进行了。
//加了stop工作线程就退出,我们就可以join了
_cond_con.wait(lock,[&](){return _stop || !(_pro_buf.empty());});
//不加这一步会导致,生产者退出后,生产缓冲区还有数据,而消费者进行最后一次的处理就退出了,导致数据残留
if(_stop && _pro_buf.empty())break;
_con_buf.swap(_pro_buf);
if(_looper_type==ASYNC_TYPE::ASYNC_SAFE)//阻塞了才唤醒
_cond_pro.notify_all();//交换完成就可以唤醒生产者了。
}
//唤醒后,对消费缓冲区数据处理
_callBack(_con_buf);//在外界就是遍历sink落地方式,把conbuf数据落地!
//初始化消费缓冲区
_con_buf.reset();
}
}
//线程入口函数
private:
Functor _callBack;//具体对缓冲区处理的函数,由异步工作器使用者传入
private:
bool _stop;//工作器停止标志
Buffer _pro_buf;//生产缓冲区
Buffer _con_buf;//消费缓冲区
std::mutex _mutex;
std::condition_variable _cond_pro;//条件变量生产者
std::condition_variable _cond_con;//条件变量消费者
ASYNC_TYPE _looper_type;
std::thread _thread;//异步工作期工作线程,一定要放在最后,因为如果线程先创建mutex没有先创建,就会造成无人正确唤醒消费者
};
}
#endif
优势:避免了生产者(业务线程)和消费者(工作线程)之间的冲突,提高了效率。之前在学生产消费者模型中,3种关系(生产者和生产者,消费者和消费者,生产者和消费者),任意2个角色都有锁的冲突,所以就算是使用循环队列ringqueue也会出现锁冲突,双缓冲区不一样:
双缓冲区在工作线程处理完任务后,如果有任务就交换一次缓冲区,虽然这是多线程,会造成交换时候的一次锁冲突,但如果不采用这种方案,就会在每一条任务(每一个临界资源)都有锁冲突。且不会涉及到空间的频繁申请带来的消耗。
2.8:日志器全局接口设计(代理模式)-wjwlog.h
把上述类都包含在头文件。
#ifndef __WJW_LOG_H__
#define __WJW_LOG_H__
#include"logger.hpp"
namespace wjwlog
{
Logger::ptr getLogger(const std::string &name)
{
return LogManager::getInstance().getLogger(name);
}
Logger::ptr rootlogger()
{
return LogManager::getInstance().rootLogger();
}
#define debug(fmt,...) debug(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#define Info(fmt,...) Info(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#define Warning(fmt,...) Warning(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#define Error(fmt,...) Error(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#define Fatal(fmt,...) Fatal(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#define DEBUG(fmt,...) wjwlog::rootlogger()->debug(fmt,##__VA_ARGS__)
#define INFO(fmt,...) wjwlog::rootlogger()->Info(fmt,##__VA_ARGS__)
#define WARNING(fmt,...) wjwlog::rootlogger()->Warning(fmt,##__VA_ARGS__)
#define ERROR(fmt,...) wjwlog::rootlogger()->Error(fmt,##__VA_ARGS__)
#define FATAL(fmt,...) wjwlog::rootlogger()->Fatal(fmt,##__VA_ARGS__)
// #define DEBUG(logger,fmt,...) logger->debug(fmt,##__VA_ARGS__)
// #define DLOG(fmt,...) DEBUG(rootlogger(),##__VA_ARGS__)
}
#endif
把日志器的不同级别输出接口封装成宏,且不必用户自己提供行号和文件名,直接用宏就行。
我们知道rootlogger是默认日志器,默认采用的是stdousink,把级别输出接口设置为大写就行。
这里别忘了大写的接口 要加作用域
2.9:使用全局接口的样例代码-test.cc
#include"../logs/wjwlog.h"
#include<unistd.h>
void test_log(const std::string &name)
{
INFO("%s","测试INFO消息");
wjwlog::Logger::ptr logger = wjwlog::getLogger(name);
logger->debug("%s","测试debug消息");
logger->Warning("%s","测试warning消息");
logger->Info("%s","测试Info消息");
logger->Error("%s","测试Error消息");
logger->Fatal("%s","测试Fatal消息");
INFO("%s","测试完毕");
}
int main()
{
std::unique_ptr<wjwlog::LogBuilder> builder(new wjwlog::GlobalLoggerBuilder());
builder->buildLevel(wjwlog::LogLevel::value::WARN);
builder->buildLoggerName("async_logger");
builder->buildFormatter("[%c][%f:%l]%m%n");
builder->buildLoggerType(wjwlog::LoggerType::LOGGER_ASYNC);
builder->buildSink<wjwlog::StdoutSink>();
builder->buildSink<wjwlog::FileSink>("./logfile/file.log");
builder->buildSink<wjwlog::RollBySizeSink>("./logfile/async-by-size",1024*1024);
builder->build();
test_log("async_logger");
}
2.10:各个类关系总结
- format格式化字符串
- sink的log负责落地data到不同的ofstream
- logger包含format和sink和level和loggername
- 同步异步logger继承logger,有自己的log函数,都是遍历自己的sink,调用sink中的log
- logbuild负责初始化logger中的成员,提供纯虚函数build,作用是初始化一个日志器
- localbuild继承logbuild,重写build,对相关成员检查进行默认构造,返回构造好的日志器,以便于loggermanager的getlogger使用
- loggermanager负责管理日志器,是单例,有默认日志器和名称+日志器哈希表
- 包含haslogger,getlogger,rootlogger,add,getinstance
- globalbuild继承logbuild,重写build,添加新语句,负责把构造好的日志器添加到单例日志器管理的哈希表中,也就是add函数。
- 外界使用流程:利用globalbuild,buildname、buildformatter等构造出一个日志器,然后使用globalbuild 的build把日志器自动添加进去,外界只需要包含wjwlog.h,如果想用默认日志器就用大写的接口,如果想用特定日志器,就用wjwlog::getLogger(name)->小写接口。
3:项目性能测试
测试三要素:
- 测试环境
- 测试方法
- 测试结果
#include"../logs/wjwlog.h"
#include<vector>
#include<thread>
#include<chrono>
void bench(const std::string &logname,size_t thr_count,size_t msg_count,size_t msg_len)
{
//1:获取日志器
wjwlog::Logger::ptr ptr = wjwlog::getLogger(logname);
if(ptr.get()==nullptr)return;
std::cout<<"测试日志"<<msg_count<<"条,总大小: "<<(msg_count*msg_len)/1024<<"KB\n";
//2:组织日志消息
std::string msg(msg_len-1,'A');//少1个字节是为了末尾添加换行
//3:创建指定数量线程
std::vector<std::thread> threads;
std::vector<double> cost_array(thr_count);
size_t msg_per_thr = msg_count/thr_count;//每个线程输出的日志数量
//4:线程函数内部计时
//5:开始循环写日志
//6:线程函数内部结束计时
for(int i = 0;i<thr_count;i++)
{
threads.emplace_back([&,i](){
auto start = std::chrono::high_resolution_clock::now();
for(int j = 0;j<msg_per_thr;j++)
{
ptr->Fatal("%s",msg.c_str());
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> cost = end-start;
cost_array[i] = cost.count();
std::cout<<"线程"<<i<<": "<<"\t输出日志数量: "<<msg_per_thr<<", 耗时时间: "<<cost.count()<<"s"<<std::endl;
});
}
for(int i =0;i<thr_count;i++){
threads[i].join();
}
//之所以维护一个costarray,是为了不要算上线程创建的时间
//计算总耗时
//多线程耗时最多的那个线程才是总耗时,因为是并发执行的
double max_cost = cost_array[0];
for(int i = 0;i<cost_array.size();i++)
{
max_cost=max_cost<cost_array[i]?cost_array[i]:max_cost;
}
size_t msg_per_sec = msg_count/max_cost;//每秒输出消息数量=日志总数量/最大耗时
size_t size_per_sec = (msg_count*msg_len) /(max_cost*1024);//每秒输出日志大小=日志数量*日志大小/最大耗时*1024 得到KB
std::cout<<"\t总耗时: "<<max_cost<<"s\n";
std::cout<<"\t每秒输出日志数量: "<<msg_per_sec<<"条\n";
std::cout<<"\t每秒输出日志大小: "<<size_per_sec<<"KB\n";
//7:计算总耗时
//8:输出打印
}
void sync_bench(){
std::unique_ptr<wjwlog::LogBuilder> builder(new wjwlog::GlobalLoggerBuilder());
builder->buildLoggerName("sync_logger");
builder->buildFormatter("%m%n");
builder->buildLoggerType(wjwlog::LoggerType::LOGGER_SYNC);
builder->buildSink<wjwlog::FileSink>("./logfile/sync.log");
builder->build();
bench("sync_logger",1,1000000,100);
}
void async_bench(){}
int main()
{
sync_bench();
return 0;
}
3.1:环境
- OS:2H2GB Centos7.6云服务器
- RAM:1.7GB(free -h的total行下)
- ROM:40GB
- CPU::AMDRyzen75800HwithRadeonGraphics 3.2GHz
3.2:同步日志器
3.2.1:单线程测试
3.2.2:多线程测试
3.3:异步日志器
3.3.1:单线程测试
3.3.2:多线程测试
因为落地文件也是写到文件的缓冲区,所以还是要根据实际生产环境。
实际上
同步日志器:多线程<单线程
异步日志器:多线程>单线程
项目开源代码地址 :gitee源代码,请点击我跳转!https://gitee.com/wenge-c-language/Logs