日志库实现

该日志库是我借鉴sylar和muduo网络库所研究出来的具有高扩展性和高性能的跨平台的日志库。该日志库使用流式输出,只支持日志输出到文件,原因是网络io很不稳定,基本没有见过利用网络传输进行日志的打印。输出日志的文件名是根据程序运行的路径来自动取名的。release模式下平均每一条1.3us~1.4us之间,基本不会影响主程序的运行的性能。

github仓库地址!!

项目使用

  1. 下载源码将其添加为子目录
  2. 在cmake项目中添加以下命令:
include(FetchContent)FetchContent_Declare(WLogGIT_REPOSITORY https://github.com/wrxhardworking/WLog.gitGIT_TAG masterGIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(WLog)target_link_libraries(${PROJECT_NAME} PRIVATE WLog)

这种方式是在在项目中将编译源码,链接库。

项目构建

mkdir build && cd build
cmake ..

项目测试

#include "../Logger.h"#include <iostream>
#include <chrono>
#include <thread>
#include <memory>class Timer {public:Timer() {m_curTime = std::chrono::high_resolution_clock::now();}~Timer() {auto end = std::chrono::high_resolution_clock::now();auto startTime = std::chrono::time_point_cast<std::chrono::microseconds>(m_curTime).time_since_epoch().count();auto endTime = std::chrono::time_point_cast<std::chrono::microseconds>(end).time_since_epoch().count();auto duration = endTime - startTime;double ms = duration * 0.001;//得到毫秒printf("%lld us (%lf ms)\n", duration, ms);fflush(stdout);}private:std::chrono::time_point<std::chrono::high_resolution_clock> m_curTime;
};int main() {/** part I:* 测试性能* part II:* 自定义日志名称,日志格式* part III:* 使用方式* part IV:* 线程安全的测试*/
#if 1{Timer timer;{for (int i = 0; i < 10000; ++i) {LOG_INFO<<"hello world";}}}#endif
#if 0//设置日志等级GET_LOGGER->setLevel(LogLever::ALERT);//设置日志名称GET_LOGGER->setName("service");
//    设置日志格式GET_LOGGER->setFormatter(std::make_shared<LogFormatter>("....."));//......敬请期待(指定日志输出地 默认名字是target_name 文件的默认路径是../build(构建目录))
#endif
#if 0/** 1.支持普通字符串类型* 2.支持std::string类型* 3.支持std::vector<int>,std::vector<std::string>等类型*/std::string log = "hello world ";std::vector<const char *> s{"hello"};LOG_INFO<<log<<s;LOG_INFO<<"HELLO WORLD";LOG_INFO<<std::vector<int>{1,2,3,4,5};LOG_INFO<<1.1111111;LOG_INFO<<111111111;
#endif
#if 0std::vector<std::thread> v;v.reserve(8);
for(int i = 0;i<100;i++){v.emplace_back([](){LOG_INFO<<"hello world";});}
for(auto &x :v){x.join();
}
#endif
}

这里我的电脑在release模式下测出打印100万条日志所需的的时间为1300ms,平均每一条1.3us~1.4us之间不同机子下应该有不同的性能。

项目总览

重点部分详解

1.init()

​ 该方法是为了解析用户定义的格式,该格式包含模板字符和常规字符。模板字符:是指那些已经确定好了的意义的字符,它们分别代表一个属性,若是模板字符,则前面有我们规定好的转义字符%,例如:%c %d{%Y-%m-%d %H:%M:%S} %p 代表的则是 日志名 日期时间 日志等级 ,这里我们对d字符做了特别的处理,它代表日期时间,它的后面必须带有 {日期时间的格式化样式} 。常规字符:常规字符是真正会输出到日志文件中的实际内容。例如:%c %d{%Y-%m-%d %H:%M:%S} %p 线程id:%t %f %l %m %n 线程二字就会被实际输出到文件中。当然,还也可以在本库的基础上继续拓展,而且可以很规范的扩展,具体看源代码。

​ 该方法是为了解析用户规定的输出日志的格式,采取的状态机的方法提取模板字符,将每个模板字符字符与FormatItem进行一个映射,进而得到具有正确顺序的m_items,重点在于利用状态机提取模板字符的算法。

代码如下(详细看源码):

void LogFormatter::init(){// 按顺序存储解析到的pattern项// 每个pattern包括一个整数类型和一个字符串,类型为0表示该pattern是常规字符串,为1表示该pattern需要转义// 日期格式单独用下面的dataformat存储std::vector<std::pair<int,std::string>> patterns;// 临时存储常规字符串std::string tmp;//存储时间日期的格式std::string dateFormat;//是否解析出错bool error = false;//设置初始状态 默认为解析模板LogFormatter::Parse status = LogFormatter::Parse::NORMAL;size_t i = 0;while(i<m_pattern_.size()){//逐个字符进行解析std::string c(1,m_pattern_[i]);if(c=="%"){switch (status) {case LogFormatter::Parse::NORMAL:{if(!tmp.empty()){//0表示常规字符patterns.emplace_back(0,tmp);}tmp.clear();//因为解析常规字符的时候碰到了% 所以要解析模板字符了status = LogFormatter::Parse::TEMPLATE;break;}case LogFormatter::Parse::TEMPLATE:{//1表示模板字符//在解析模板字符的时候碰到了%说明%事项表示常规字符说明要解析常规字符了 要将其转为常规字符的状态patterns.emplace_back(1,c);status = LogFormatter::Parse::NORMAL;break;}}++i;continue;}//碰到的不是%else{switch (status) {case LogFormatter::Parse::NORMAL:{//持续解析常规字符串 直到遇见% 把解析后的常规的字符串放入patterns中tmp += c;++i;break;}case LogFormatter::Parse::TEMPLATE:{//若是解析模板字符patterns.emplace_back(1,c);//插入完后 将其切回常规字符status = LogFormatter::Parse::NORMAL;++i;//此处又要对时间有特殊的处理 将%d后大括号中的内容放入dataformatif(c=="d"){if(i<m_pattern_.size()&&m_pattern_[i]=='{'){++i;while(i<m_pattern_.size()&&m_pattern_[i]!='}'){dateFormat.push_back(m_pattern_[i]);++i;}//说明其中的大括号没有闭合if(m_pattern_[i]!='}'){std::cerr<<"parse dateFormat error"<<std::endl;error = true;}//没有出错的此时i的位置是'}'的位置 所以要加一++i;}}break;}}if(error) break;continue;}}static std::map<std::string,std::function<FormatterItem::ptr(const std::string &str)> > s_formatters = {//注意下面没有datetime的宏定义 它会在后面做一个特殊处理//这个宏处理直呼较好#define xx(str,func) \{#str,[](const std::string &fmt){return FormatterItem::ptr(new func(fmt));}}xx(m, MessageFormatItem),xx(p, LeverFormatItem),xx(c, NameFormatItem),xx(r, ElapseFormatItem),xx(f, FilenameFormatItem),xx(t, ThreadIdFormatItem),xx(F, FiberIdFormatItem),xx(n, NewLineFormatItem), xx(l, LineFormatItem),#undef xx};for(auto&v : patterns){if(v.first==0){//是常规字符m_items_.emplace_back(FormatterItem::ptr(new StringFormaItem(v.second)));}// 是模板(转义字符)字符// 日期做特殊的处理else if(v.second == "d"){m_items_.emplace_back(FormatterItem::ptr(new DateTimeFormatItem(dateFormat)));}//以下是模板字符的处理else{auto it = s_formatters.find(v.second);if(it == s_formatters.end()){std::cerr<<"error happend is "<<v.second<< std::endl;error = true;break;}else{//map中存在该字符对应的Itemm_items_.emplace_back(it->second(v.second));}}}
}

2.双缓冲技术

​ 基本思路是准备两个buffer:A与B,前端负责将A填数据,后端则负责B中的数据写入文件。当A满了则交换A,B,如此往复。这样做的好处:1不用直接向磁盘写消息;2也避免每条日志都触发后端日志线程,减少了线程唤醒的频率降低开销。为了及时将日志文件写入消息,即便A没有满,日志库也每隔3s执行一次交换操作。在实际实现中我们采取了四个缓冲区,这样可以进一步避免日志前端的等待。每个容器(缓冲区)大小为4MB,至少可以放1000条日志消息,std::unique_ptr具备移动语义,而且可以自己管理生命周期。

​ 前端来一条日志如果当前缓冲区大小足够则直接append,这里拷贝一条日志消息不会造成多大开销,因为其他部分都是对指针的操作。如果当前缓冲区大小都不够了,则将当前缓冲区移入数组,并将另外一个块准备好的缓冲区作为当前缓冲区,然后追加日志消息后端开始写日志。如果前端写日志很快一下子把两块缓冲区都使用完了,那么只好重新分配一块缓冲区,这种情况很少见。双缓冲技术精妙之处是临界区的缩小。具体看源代码

代码如下图:

//后台线程所作的事情
void AsyncLogging::backendThread() {//以追加的方式打开一个文件 base为文件路径FILE* fp = fopen(base,"a+");//以下两块buffer是为后面的更替做准备BufferPtr replaceBuffer1(new Buffer);BufferPtr replaceBuffer2(new Buffer);//初始化replaceBuffer1->bezero();replaceBuffer2->bezero();//输出到文件中buffer 用一个容器装起Buffers BufferToWrite;//预留空间 省去打日志时申请时间BufferToWrite.reserve(16);while(running){//以下是临界区处{std::unique_lock<std::mutex> locker(Mutex);//fixme 会不会存在虚假唤醒? buffers不为空或者等待事件超过三秒if (buffers.empty()) {condition.wait_for(locker, std::chrono::seconds(flushTimeInterval));}//超时阶段buffers.push_back(std::move(currentBuffer));currentBuffer = std::move(replaceBuffer1);BufferToWrite.swap(buffers);//如果预备buffer被占用if (!nextBuffer) {nextBuffer = std::move(replaceBuffer2);}}for(const auto & buffer : BufferToWrite){fwrite(buffer->data(),1,buffer->length(),fp);}//一般只有一个if(BufferToWrite.size()>2){BufferToWrite.resize(2);}if(!replaceBuffer1){replaceBuffer1 = std::move(BufferToWrite.back());BufferToWrite.pop_back();}if(!replaceBuffer2){replaceBuffer2 = std::move(BufferToWrite.back());BufferToWrite.pop_back();}//将写缓冲区置空BufferToWrite.clear();// fixme flushfflush(fp);}//fixme flushfflush(fp);if(fp){fclose(fp);}
}

WLog日志库:c++ 高拓展、高性能日志库相关推荐

  1. php日志分析,PHP SeasLog实现高性能日志记录

    简介 为什么使用SeasLog log日志,通常是系统或软件.应用的运行记录.通过log的分析,可以方便用户了解系统或软件.应用的运行情况:如果你的应用log足够丰富,也可以分析以往用户的操作行为.类 ...

  2. 深度|从Go高性能日志库zap看如何实现高性能Go组件

    导语:zap是uber开源的Go高性能日志库.本文作者深入分析了zap的架构设计和具体实现,揭示了zap高效的原因.并且对如何构建高性能Go语言库给出自己的建议. 作者简介:李子昂,美图公司架构平台系 ...

  3. 深度 | 从Go高性能日志库zap看如何实现高性能Go组件

    导语:zap是uber开源的Go高性能日志库.本文作者深入分析了zap的架构设计和具体实现,揭示了zap高效的原因.并且对如何构建高性能Go语言库给出自己的建议. 作者简介:李子昂,美图公司架构平台系 ...

  4. muduo学习笔记:base部分之高性能日志库

    服务端编程,日志必不可少,生产环境中应做到"Log Everything All The Time". 一个日志库答题分为前端(Logging.{h,cc})和后端(LogFile ...

  5. golang高性能日志库zap的使用

    本文作者:陈进坚 个人博客:https://jian1098.github.io CSDN博客:https://blog.csdn.net/c_jian 简书:https://www.jianshu. ...

  6. 号称C/C++高性能日志库

    ** 以下都是号称高性能日志库 ** 腾讯 C++日志库:Mars XLOG https://github.com/Tencent/mars 美团 C/C++日志库 https://github.co ...

  7. Golang高性能日志库zap + lumberjack 日志切割组件详解

    文章篇幅较长,可以先收藏防止迷路~ 目录 zap日志库 1. why zap? 2. 简单使用 3. 自定义logger例子 4. Gin项目使用zap 6. lumberjack 日志切割组件 za ...

  8. muduo库的高性能日志库(三)——Logging文件

    目录 SourceFile内部类 Impl内部类 内部实现细节 Logger类 1. 规定日志的几个等级 2. 两个内部类 3. 设置日志属性 4. 构造函数 5. 析构函数 日志宏 接下看一下Log ...

  9. 微信终端跨平台组件 mars 系列(一) - 高性能日志模块xlog

    前言 mars 是微信官方的终端基础组件,是一个使用 C++ 编写的业务性无关,平台性无关的基础组件.目前已接入微信 Android.iOS.Mac.Windows.WP 等客户端.现正在筹备开源中, ...

最新文章

  1. Windows核心编程 第十七章 -内存映射文件(上)
  2. Linux的shell脚本实战之检查主机IP是否存在
  3. 当Android工程中提示你找不到头文件,但你已经设置头文件路径了
  4. Numpy 排序 -- sort()、argsort()
  5. SAP Spartacus的发布方式以及语义化版本管理机制
  6. HDU 4267 A Simple Problem with Integers [树状数组]
  7. 【PAT - 甲级 - 1018】Public Bike Management (带权最短路,多条最短路中加条件,DFS)
  8. 自定义View控件(2—手写实例代码)
  9. javascript 动态修改css样式方法汇总(四种方法)
  10. webapp开发学习--Ionic+Cordova 环境搭建
  11. Redis介绍及实践分享
  12. LivePlayer.js免费直播、点播播放器如何自适应div宽高集成播放视频
  13. 日积月累系列之分页控件(js源码)
  14. Python3.4下使用sqlalchemy
  15. Touch Panel 调试技巧 01
  16. basic4android 开发 使用类库方法
  17. 我国计算机操作系统开发历史及现状(软件学报格式的本文WORD文档在作者主页)
  18. 网站搭建:从零搭建个人网站教程(1)
  19. keil警告提示: last line of file ends without a newline
  20. 第11节:Docker基本操做

热门文章

  1. vimdiff 命令使用介绍
  2. linux lseek 指定 文件大小,linux下通过lseek()实现文件大小设置
  3. 短视频剪辑技巧分享,先做排序不能忘,观看效果会更佳
  4. 导出word文档生成docx格式 添加水印
  5. Could not find a declaration file for module
  6. 印度软件水平为什么世界第一_第1部分:为什么现实世界中的软件需求很难
  7. 同花顺_代码解析_技术指标_EJK
  8. Linux已挂载的硬盘无法访问
  9. Matlab 科研绘图汇总
  10. uni-app开发 常见异常和解决办法