项目日记 -日志系统 -功能完善
博客主页:【夜泉_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语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!