当前位置: 首页 > news >正文

项目日记 -日志系统 -功能完善

博客主页:【夜泉_ly】
本文专栏:【日志系统】
欢迎点赞👍收藏⭐关注❤️

在这里插入图片描述
代码仓库:日志系统

目录

  • 日志输出模块
  • 日志格式化模块
  • 异步输出日志器
  • 异步处理模块
  • 单例管理类
  • 外观模式

日志输出模块

在上一篇进行基础项目搭建时,
我们暂时没有讨论多线程问题,
而在本篇会将这些细节完善。

首先看看日志输出模块,
一个 Sink 可能会被多个线程使用吧 ?
可能是创建一个 Sink,
然后绑定到不同线程的 Logger。
也可能是一个 Logger 被不同的线程使用。

无论如何,Sink 是我们日志输出的最后一步,
必须保证线程安全。

所以我们需要在 Sink 基类里加一个成员变量:

std::mutex _mutex;

然后在派生类实际输出的地方加上锁:

class StdoutSink : public Sink
{
public:virtual void log(const char *str, size_t len) override{std::lock_guard<std::mutex> lock(_mutex);fwrite(str, sizeof(char), len, stdout);}
};

至于 FileSink,
我认为 SizeRollSink 和 TimeRollSink 都是文件输出,
所以我让 SizeRollSink 和 TimeRollSink 继承了 FileSink。

然后我就发现这个加锁变得特别难办:
由于是 Sink 持有的锁,
如果我在 SizeRollSink 和 FileSink 进行 log 时都加锁,
那么毫无疑问就会 死锁。
如果只在 FileSink 里加锁,那 SizeRollSink 又感觉有点奇怪?

所以我把 FileSink 的 log 函数变成了两个:

    virtual void log(const char *str, size_t len) override{std::lock_guard<std::mutex> lock(_mutex);log_unsave(str, len);}protected:void choose_file_unsave(const std::string &filename){if (_ofs.is_open())_ofs.close();_ofs.open(filename, std::ios_base::binary | std::ios_base::app);if (!_ofs)std::cout << filename << " open error" << std::endl;}void log_unsave(const char *str, size_t len){_ofs.write(str, len);if (!_ofs)std::cout << _filename << " write error" << std::endl;}

这样,继承 FileSink 的类就可以用 unsafe 方法,
然后在该加锁的地方加锁:
SIzeRollSInk:

virtual void log(const char *str, size_t len) override
{std::lock_guard<std::mutex> lock(_mutex);FileSink::log_unsave(str, len);_cur_size += len;if (_cur_size >= _limit_size){choose_file_unsave(Util::get_new_name(_org_filename));_cur_size = 0;}
}

TimeRollSink:

virtual void log(const char *str, size_t len) override
{std::lock_guard<std::mutex> lock(_mutex);if (time(0) - _prev_time >= _limit_time){choose_file_unsave(Util::get_new_name(_org_filename));_prev_time = time(0);}FileSink::log_unsave(str, len);
}

日志格式化模块

这部分主要做了一下格式化选项的扩充:

    // 格式化选项:// %Y - 年 (2025)// %m - 月 (01-12)// %d - 日 (01-31)// %H - 时 (00-23)// %M - 分 (00-59)// %S - 秒 (00-59)// %l - 日志级别 (info, debug等)// %t - 线程ID// %n - 日志器名// %s - 源文件名// %# - 行号// %v - 实际的日志消息// %% - '%'
std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> Formatter::_creaters{{'Y', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<YearFormatItem>(); }},{'m', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<MonthFormatItem>(); }},{'d', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<DayFormatItem>(); }},{'H', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<HourFormatItem>(); }},{'M', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<MinuteFormatItem>(); }},{'S', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<SecondFormatItem>(); }},{'l', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<LevelFormatItem>(); }},{'t', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<ThreadIDFormatItem>(); }},{'n', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<LoggerNameFormatItem>(); }},{'s', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<FileNameFormatItem>(); }},{'#', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<LineFormatItem>(); }},{'v', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<MessageFormatItem>(); }},{'O', [](const std::string &s) -> FormatItem::ptr{ return std::make_shared<OtherFormatItem>(s); }},
};

异步输出日志器

我想的是所有异步输出日志器共用一个写缓冲区,
然后有单独的一个工作线程对读缓冲区进行处理,
大概是这样:
在这里插入图片描述
所以我们的异步日志器需要做什么?
什么都不需要做,
把处理完的数据放入写缓冲区即可:

class AsyncLogger : public Logger
{
public:AsyncLogger(const std::string &name, LogLevel::Level limit_level, Formatter::ptr formatter, std::shared_ptr<std::vector<Sink::ptr>> sink): Logger(name, limit_level, formatter, sink) {}protected:virtual void log(LogLevel::Level level, const char *file, size_t line, const char *fmt, va_list ap) override{char *str = nullptr;if (-1 == vasprintf(&str, fmt, ap))return;LogMessage log_message(level, line, file, str, _name);free(str);auto message(std::make_shared<std::string>(_formatter->format(log_message)));AsyncLooper::getInstance().add_message({message, _sinks});}
};

异步处理模块

这里采用的是双缓冲区,简单高效,而且方便扩容。

缓冲区里的数据类型我定为了这样:

std::vector<std::pair<std::shared_ptr<std::string>, std::shared_ptr<std::vector<Sink::ptr>>>>

有点抽象。

我想的是所有的异步Logger都会往这个缓冲区里写数据,
但是,我的异步处理线程提取出数据后,
还需要用这条消息对应的 Sink 进行落地,
那么怎么找到一条消息对应的 Sink 呢?
简单,我们把 std::string 和 std::vector<Sink::ptr> 绑定在一起就行,
但直接存又有点大了,
所以都给它搞成指针:
std::shared_ptr<std::string>
std::shared_ptr<std::vector<Sink::ptr>>
最后在放进pair、存进数组就行。

而缓冲区实际很好写,这里就不多讲了:

class Buffer
{
public:Buffer() : _pos_read(0), _pos_write(0) { _buffer.reserve(1024); }void add_message(const std::pair<std::shared_ptr<std::string>, std::shared_ptr<std::vector<Sink::ptr>>> &message){if(_pos_write == _buffer.size()) _buffer.emplace_back(message);else _buffer[_pos_write] = message;++_pos_write;}std::pair<std::shared_ptr<std::string>, std::shared_ptr<std::vector<Sink::ptr>>> get_message(){assert(!empty());return _buffer[_pos_read++];}bool empty() { return _pos_read == _pos_write; }void clear(){_pos_read = _pos_write = 0;}int size() { return _pos_write - _pos_read; }private:size_t _pos_read, _pos_write;std::vector<std::pair<std::shared_ptr<std::string>, std::shared_ptr<std::vector<Sink::ptr>>>> _buffer;
};

AsyncLooper的基本工作原理:
消费线程一直取 读缓冲区 的数据进行实际落地,
直到 读缓冲区 的数据被取完。
此时,判断写缓冲区是否有数据,
如果有就交换读写缓冲区,
没有就等待。
同时,为了保证程序退出时我们把数据写完,
以及我们或许不会使用异步日志器,
那么我们可以提供一个 stop 标志,
在 stop 标志变为 true 时,
把所有数据处理完后退出线程。

代码如下:

class AsyncLooper
{
public:static AsyncLooper &getInstance(){static AsyncLooper looper;return looper;}void add_message(const std::pair<std::shared_ptr<std::string>, std::shared_ptr<std::vector<Sink::ptr>>> &message){{std::lock_guard<std::mutex> lock(_mutex);_buffer[_write].add_message(message);}_con.notify_one();}void stop(){_stop = true;_con.notify_one();}~AsyncLooper(){stop();if (_consume.joinable())_consume.join();}private:AsyncLooper() : _stop(false), _buffer(2), _read(0), _write(1){_consume = std::thread(&AsyncLooper::consume, this);}AsyncLooper(const AsyncLooper&) = delete;void consume(){while (true){while (_buffer[_read].empty() == false){auto message = _buffer[_read].get_message();for (auto &sink : *message.second)sink->log(message.first->c_str(), message.first->size());}_buffer[_read].clear();std::unique_lock<std::mutex> lock(_mutex);_con.wait(lock, [this] { return _buffer[_write].size() || _stop; });if (_buffer[_write].empty() && _stop)break;std::swap(_read, _write);}}private:std::atomic<bool> _stop;std::mutex _mutex;std::thread _consume;std::condition_variable _con;std::vector<Buffer> _buffer;size_t _read, _write;
};

可以看见,
消费者从读缓冲区里取数据时是不用加锁的,
避免了生产者和消费者之间的竞争,
这就是双缓冲区的好处。

单例管理类

这里主要是提供一个注册中心,
可以管理不同线程的日志器。
并且,提供了一个默认的日志器,简化操作

class LogManager
{
public:static LogManager &getInstance(){static LogManager manager;return manager;}void addLogger(const std::string &name, Logger::ptr logger){std::lock_guard<std::mutex> lock(mutex);_loggers[name] = logger;}bool findLogger(const std::string &name){return _loggers.count(name) != 0;}Logger::ptr getLogger(const std::string &name){std::lock_guard<std::mutex> lock(mutex);return _loggers[name];}private:LogManager(){_loggers["default_logger"] = LocalBuilder().buildName("default_logger").buildSink<ly::StdoutSink>().build();}LogManager(const LogManager &) = delete;private:std::mutex mutex;std::unordered_map<std::string, Logger::ptr> _loggers;
};

外观模式

严格讲,外观模式是有个 Facade 外观类的,
不过 Facade 的作用是简化接口,
封装复杂的子系统。

简单讲,就是让我们程序的使用变得更方便。

所以,我这其实也算一种外观模式:

#ifndef LYLOG_H
#define LYLOG_H#include "logger.hpp"
namespace ly    
{#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 fdebug(fmt, ...) ly::LogManager::getInstance().getLogger("default_logger")->debug(fmt, ##__VA_ARGS__)#define finfo(fmt, ...) ly::LogManager::getInstance().getLogger("default_logger")->info(fmt, ##__VA_ARGS__)#define fwarning(fmt, ...) ly::LogManager::getInstance().getLogger("default_logger")->warning(fmt, ##__VA_ARGS__)#define ferror(fmt, ...) ly::LogManager::getInstance().getLogger("default_logger")->error(fmt, ##__VA_ARGS__)#define ffatal(fmt, ...) ly::LogManager::getInstance().getLogger("default_logger")->fatal(fmt, ##__VA_ARGS__)
}
#endif

使用非常简单:

fdebug("日志器管理者测试\n");

在这里插入图片描述


希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

http://www.dtcms.com/a/395185.html

相关文章:

  • install_docker.sh
  • opencv的DNN模块里
  • FPGA学习笔记——图像处理之对比度调节(线性调节)
  • SkyWalking 核心概念与智能探针工作原理深度揭秘(上)
  • leetcode hot100 简单难度 day02-刷题
  • ARP报文格式
  • 【论文速递】2025年第26周(Jun-22-28)(Robotics/Embodied AI/LLM)
  • 用【PinMe】轻松实现前端部署(文章附有演示案例)
  • 巨坑Spring ai 之spring-ai-starter-vector-store-elasticsearch
  • 【LeetCode 每日一题】2349. 设计数字容器系统
  • i.MX6ULL移植内核6.6(一)修改网络驱动和LCD驱动
  • vue-router(vue 路由)基本使用指南(一)
  • 酒店台账报表:押金原路退回与收支自动化指南-东方仙盟自动化
  • ⸢ 伍-Ⅰ⸥ ⤳ 默认安全治理实践:软件供应链安全治理
  • LeetCode 刷题【88. 合并两个有序数组、89. 格雷编码】
  • 《青花》歌曲,使用3D表现出意境
  • 【Linux篇】- Ext系列文件系统
  • C++符号表
  • Pythoner 的Flask项目实践-带折叠菜单的响应式多页面应用签到墙(源码)
  • 异常:java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
  • Java-131 深入浅出 MySQL MyCat 深入解析 schema.xml 配置详解:逻辑库、逻辑表、数据节点全攻略
  • Vmware CentOS Docker Daemon配置代理
  • 【JAVA】java多态
  • 【AI分析进行时】大模型显存需求估算与国内开源模型实践指南
  • C++基础:(一)C++入门知识介绍(上)
  • Python项目的多语言翻译babel
  • python flask框架详解
  • 基于STM32单片机的家庭医护血氧体温血压吃药监测APP系统
  • 整合亮数据Bright Data与Dify构建自动化分析系统
  • Browser-Use+cpolar:企业网页操作自动化的无界解决方案