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

项目日记 -日志系统 -搭建基础框架

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

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

目录

  • 前言
  • 确定核心功能
  • 基础框架的搭建
    • 数据
      • LogLevel
      • LogMessage
    • 日志输出模块
      • Sink
      • StdoutSink
      • SinkFactory
      • SinkFactory扩展
    • 格式化模块
      • FormatItem
      • FileNameFormatItem
      • LineFormatItem
      • MessageFormatItem
      • OtherFormatItem
      • Formatter
    • 日志器
      • Logger
      • SyncLogger
    • 日志器建造者
      • Builder
      • LocalBuilder
  • 功能测试

前言

在上一篇中,
我们明确了项目目标,
完成了模块的设计,
并画出了项目的结构图。

从项目结构中可以发现,
这个项目具有复杂的继承体系,
并且使用了多种设计模式。

如果直接按照模块一个类一个类的实现,
很有可能会在项目中后期出现混乱,
比如由于接口设计不当导致实现与设计不符。

确定核心功能

橙色:核心模块,需要完整实现,
绿色:基础框架,需要部分实现,
灰色:这次不用管。
在这里插入图片描述

基础框架的搭建

数据

LogLevel

提供 debug, info, warning, error, fatal 五个日志级别

#ifndef __LEVEL_HPP__
#define __LEVEL_HPP__namespace ly
{class LogLevel{public:enum class Level{debug = 0,info,warning,error,fatal};static const char* levelToString(Level level){if(level == Level::debug) return "debug";if(level == Level::info) return "info";if(level == Level::warning) return "warning";if(level == Level::error) return "error";if(level == Level::fatal) return "fatal";return "unknown";}};
} // namespace ly#endif // #define __LEVEL_HPP__

LogMessage

需存储:

  • 日志级别
  • 文件名
  • 文件行号
  • 消息内容
  • 消息创建时间
  • 线程id
  • 日志器名

LogMessage 做为项目中最重要且最基础的数据结构,
只应该存储纯数据,
与上层的类不应该有任何的关系。
所以在设计接口时不能图方便直接传 Logger/Sink 等类。

我最开始想的是消息类存个 Sink::ptr 多好,
自己就能把自己落地了,
后来发现 logger.hpp 包含了 message.hpp,
于是就更正了这个错误的想法。

#ifndef __MESSAGE_HPP__
#define __MESSAGE_HPP__#include "level.hpp"
#include <string>
#include <vector>
#include <thread>
#include <ctime>namespace ly
{struct LogMessage{using Level = LogLevel::Level;LogMessage() = default;// 等级, 行号, 文件名, 信息, 日志器名LogMessage(Level level, size_t line, const std::string &file, const std::string &message, const std::string &logger_name): _level(level), _tid(std::this_thread::get_id()), _line(line), _file(file), _message(message), _logger_name(logger_name){time_t t = time(0);localtime_r(&t, &_time);}LogLevel::Level _level;std::thread::id _tid;tm _time;size_t _line;std::string _file;std::string _message;std::string _logger_name;};
} // namespace ly#endif // #define __MESSAGE_HPP__

线程 id 通过 std::this_thread::get_id() 获取,
时间通过 localtime_r 获取,
这两个不需要通过参数传递。

而其他信息,如行号,文件名必须由外部传入。

日志输出模块

日志输出模块作为一个独立的模块,
只需要负责将数据 按指定方式 输出到目标位置即可。
不应该知道项目中有哪些类,
不需要包含项目的其他头文件。

Sink

作为基类,规范了这个模块的输出接口,
也让用户知道如何扩展日志的输出方式。

class Sink
{
public:using ptr = std::shared_ptr<Sink>;virtual void log(const char* str, size_t len) = 0;virtual ~Sink() = default;
};

StdoutSink

标准输出作为最简单的输出方式,
可以用来测试项目是否能跑。

class StdoutSink : public Sink
{
public:virtual void log(const char* str, size_t len) override{fwrite(str, sizeof(char), len, stdout);}
};

SinkFactory

为了适配不同 sink 的构造,
工厂需要使用可变参数模板,
其第一个参数用于确认 sink 的类型。

class SinkFactory
{
public:template<typename T, typename ...Args>static Sink::ptr create(Args &&...args){return std::make_shared<T>(std::forward<Args>(args)...);}
};

如果单独使用工厂,可能有点麻烦:

auto sink = SinkFactory::create<SizeRollSink>(1024 * 1024 * 1024);

对比直接用 make_shared:

auto sink = std::make_shared<SizeRollSink>(1024 * 1024 * 1024);

但是,在实际使用中 sink 并不需要像这样直接创建,
而是以日志器的建造者创建:

auto logger = ly::LocalBuilder().buildName("test").buildSink<ly::StdoutSink>().build();

如果去掉工厂,就会变成。。。

额。。
废物设计出现了!!
突然发现,如果建造者用模板方案,那工厂毫无价值啊:

// 有工厂:
template<typename SinkType, typename ...Args>
Builder &buildSink(Args &&...args) { auto sink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);_sinks.push_back(sink);return static_cast<T &>(*this);
}
// 无工厂:
template<typename SinkType, typename ...Args>
Builder &buildSink(Args &&...args) { auto sink = std::make_shared<SinkType>(std::forward<Args>(args)...);_sinks.push_back(sink);return static_cast<T &>(*this);
}

那么暂时废除 SinkFactory 吧,
我们不能为了使用设计模式而使用设计模式。

SinkFactory扩展

现在这是个有用的工厂了,
毕竟能运行时动态创建的工厂才算真正的工厂:

static Sink::ptr create(const std::string& type, const std::unordered_map<std::string, std::string>& config)
{if (type == "stdout")   return std::make_shared<StdoutSink>();if (type == "file")     return std::make_shared<FileSink>(config.at("filename"));if (type == "sizeroll") return std::make_shared<SizeRollSink>(config.at("filename"), std::stoi(config.at("limit_size")));if (type == "timeroll") return std::make_shared<TimeRollSink>(config.at("filename"), std::stoi(config.at("limit_time")));return nullptr;
}

由于用了哈希,
甚至还可以添加 json 格式的配置文件,
为复杂的 Sink 提供配置信息。

格式化模块

用于测试的格式化选项:

%s - 源文件名
%# - 行号
%v - 实际的日志消息
%% - '%'

FormatItem

格式化子项基类
每个格式化选项对应一个格式化子项派生类,
然后在格式化器中存储格式化子项指针数组,
格式化的过程就是遍历这个子项数组,
然后每个子项从 LogMessage 中提取出对应的信息,
最后组成一个完整的消息。

因此格式化子项基类提供一个纯虚函数 format,
作用是把 message 里的信息拼到 std::stringstream 里去。

class FormatItem
{
public:using ptr = std::shared_ptr<FormatItem>;virtual ~FormatItem() = default;virtual void format(std::stringstream &out, const LogMessage &message) = 0;
};

FileNameFormatItem

%s - 源文件名

class FileNameFormatItem : public FormatItem
{
public:virtual void format(std::stringstream &out, const LogMessage &message) override { out << message._file; }
};

LineFormatItem

%# - 行号

class LineFormatItem : public FormatItem
{
public:virtual void format(std::stringstream &out, const LogMessage &message) override { out << message._line; }
};

MessageFormatItem

%v - 实际的日志消息

class MessageFormatItem : public FormatItem
{
public:virtual void format(std::stringstream &out, const LogMessage &message) override { out << message._message; }
};

OtherFormatItem

解析不出来的信息直接按原样输出

class OtherFormatItem : public FormatItem
{
public:OtherFormatItem(const std::string &str = "") : other_message(str) {}virtual void format(std::stringstream &out, const LogMessage &message) override { out << other_message; }private:std::string other_message;
};

Formatter

格式化器,
储存:

  • 格式化字符串
  • 格式化子项指针数组
  • 格式化子项创建者

前两个不必多说,而第三个:
static std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> _creaters;
嘿嘿嘿,虽然类型很抽象,但特别好用。
只要看看初始化就明白了:

std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> Formatter::_creaters{{'v', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<MessageFormatItem>(); }},{'s', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<FileNameFormatItem>(); }},{'#', [](const std::string &) -> FormatItem::ptr{ return std::make_shared<LineFormatItem>(); }},{'O', [](const std::string &s) -> FormatItem::ptr{ return std::make_shared<OtherFormatItem>(s); }},
};

之后添加其他格式化子项时,
只要改改这里就行。

下面是格式化器的实现:

// 格式化器
class Formatter
{
public:using ptr = std::shared_ptr<Formatter>;public:Formatter(const std::string &pattern = "[%s:%#] %v") { analysis(pattern); }// 消息对象 -> 格式化后的消息字符串std::string format(const LogMessage &message){std::stringstream ss;for (auto &e : _formatter_items)e->format(ss, message);return ss.str();}private:void analysis(const std::string &pattern){std::string other_message;for(int i = 0, size = pattern.size(); i < size; ++i){char c = pattern[i];if(c != '%' || i + 1 == size || _creaters.count(pattern[i + 1]) == 0)other_message.append(1, c);else{c = pattern[++i]; // 将 i 更新至格式化选项处, 将 c 更新为格式化选项if(other_message.size()) _formatter_items.push_back(_creaters['O'](other_message));other_message.clear();_formatter_items.push_back(_creaters[c](""));}}}private:std::string _pattern;                          // 格式化字符串std::vector<FormatItem::ptr> _formatter_items; // 解析_pattern得到_formatter_itemsstatic std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> _creaters;
};

日志器

Logger

由于目前不涉及多线程,
因此相关的成员暂时不添加。

目前我们留三个最主要的成员:

  • 日志名 std::string _name;
  • 格式化器 Formatter::ptr _formatter;
  • 输出方式 std::shared_ptr<std::vector<Sink::ptr>> _sinks;
class Logger
{
public:using ptr = std::shared_ptr<Logger>;enum class Type{sync,async};public:Logger(const std::string &name, Formatter::ptr formater, std::shared_ptr<std::vector<Sink::ptr>> sinks): _name(name), _formatter(formater), _sinks(sinks) {}std::shared_ptr<std::vector<Sink::ptr>> sinks() const { return _sinks; }const std::string &name() const { return _name; }void debug(const char *file, size_t line, const char *fmt, ...){va_list ap;va_start(ap, fmt);log(LogLevel::Level::debug, file, line, fmt, ap);va_end(ap);}virtual ~Logger() = default;protected:virtual void log(LogLevel::Level level, const char *file, size_t line, const char *fmt, va_list ap) = 0;protected:std::string _name;Formatter::ptr _formatter;std::shared_ptr<std::vector<Sink::ptr>> _sinks;
};

核心功能就是 log 函数,
由 debug, info, warning, error, fatel函数调用,
但同步日志器和异步日志器的输出方式不同,
因此这里声明为纯虚函数。

SyncLogger

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

这个项目的核心逻辑就是这一段了:
logger 拿到用户的输入
-> 通过 vasprintf 转换为字符串
-> 构建 LogMessage 对象
-> 用格式化器格式化 LogMessage 对象,返回格式化后的字符串
-> 用日志落地器将消息落地

这就是为什么我画的结构图长这样:

在这里插入图片描述

日志器建造者

目前只是基础框架搭建阶段,
Logger 的构造就有三个参数了,
之后随着项目的完善还会增多。
因此建造者是必须的。

Builder

为了能够链式调用,
这里使用奇异递归模板模式。

template <typename T>
class Builder
{
public:Builder &buildName(const std::string &name){_name = name;return static_cast<T &>(*this);}Builder &buildType(Logger::Type type){_type = type;return static_cast<T &>(*this);}template<typename SinkType, typename ...Args>Builder &buildSink(Args &&...args) { auto sink = std::make_shared<SinkType>(std::forward<Args>(args)...);_sinks.push_back(sink);return static_cast<T &>(*this);}Builder &buildFormatPattern(const std::string& format_pattern){_format_pattern = format_pattern;return static_cast<T &>(*this);}virtual Logger::ptr build() = 0;virtual ~Builder() = default;protected:std::string _name;std::string _format_pattern;Logger::Type _type = Logger::Type::sync;std::vector<Sink::ptr> _sinks;
};

LocalBuilder

class LocalBuilder : public Builder<LocalBuilder>
{
public:virtual Logger::ptr build(){assert(_name.size());Formatter::ptr formater;if(_format_pattern.size()) formater = std::make_shared<Formatter>(_format_pattern);else formater = std::make_shared<Formatter>();std::shared_ptr<std::vector<Sink::ptr>> sinks = std::make_shared<std::vector<Sink::ptr>>(_sinks);if (Logger::Type::sync == _type)return std::make_shared<SyncLogger>(_name, formater, sinks);elsereturn std::make_shared<AsyncLogger>(_name, formater, sinks);}virtual ~LocalBuilder() override {}
};

功能测试

为了让我们不用手动传 __FILE__ 和 __LINE__,
我们还得再加一句:

#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)

然后就能开始测试了:

#include "builder.hpp"int main()
{auto logger = ly::LocalBuilder().buildName("test_logger").buildFormatPattern("[filename : %s] [line : %#] message : %v").buildSink<ly::StdoutSink>().build();logger->debug("%s\n", "基础框架功能测试");return 0;
}

在这里插入图片描述

在这里插入图片描述


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


文章转载自:

http://YnzrabVv.jppzj.cn
http://pqEUYwGB.jppzj.cn
http://dpUO7tmV.jppzj.cn
http://yWR2KZBm.jppzj.cn
http://8Mtb80R6.jppzj.cn
http://mwQ0poGD.jppzj.cn
http://LuCPWLb7.jppzj.cn
http://O3hcArWU.jppzj.cn
http://mHdPCUfa.jppzj.cn
http://FR4E4nde.jppzj.cn
http://XLys2XY3.jppzj.cn
http://bfL1O3r9.jppzj.cn
http://dz7o1gZr.jppzj.cn
http://OtP287rd.jppzj.cn
http://D6NFF0j5.jppzj.cn
http://aiGcjKLg.jppzj.cn
http://SHtiidOY.jppzj.cn
http://ULc67Q93.jppzj.cn
http://RI5EK5T7.jppzj.cn
http://LkrQEQ2B.jppzj.cn
http://1LN0iNZY.jppzj.cn
http://KTbFzMeB.jppzj.cn
http://9qXNcpF8.jppzj.cn
http://WtrLLnxo.jppzj.cn
http://C6PRXQle.jppzj.cn
http://yi7ZvQfW.jppzj.cn
http://C7hpXwAG.jppzj.cn
http://y60sH1pw.jppzj.cn
http://vwd0XsQC.jppzj.cn
http://2tbIj88K.jppzj.cn
http://www.dtcms.com/a/374385.html

相关文章:

  • 计算机网络第四章(4)——网络层《ARP协议》
  • 探迹SalesGPT
  • 带有 Attention 机制的 Encoder-Decoder 架构模型分析
  • 利用易语言编写,逻辑为按照数字越大抽取率越前
  • leetcode 219 存在重复元素II
  • Redis(缓存)
  • ARP 协议
  • 169.在Vue3中使用OpenLayers + D3实现地图区块呈现不同颜色的效果
  • 【C++】递归与迭代:两种编程范式的对比与实践
  • 【Java】设计模式——单例、工厂、代理模式
  • C++ ——一文读懂:Valgrind 检测内存泄漏
  • 代码随想录算法训练营第三十一天 | 合并区间、单调递增的数字
  • Redis核心通用命令深度解析:结合C++ redis-plus-plus 实战指南
  • 三防手机的三防是指什么?推荐一款实用机型
  • 请求库-axios
  • Python 2025:AI工程化与智能代理开发实战
  • 聚铭网络入选数世咨询《中国数字安全价值图谱》“日志审计”推荐企业
  • 【56页PPT】数字化智能工厂总体设计SRMWCSWMSMESEMS系统建设方案(附下载方式)
  • 高性价比云手机挑选指南
  • 分布式IP代理集群架构与智能调度系统
  • 构造函数和析构函数中的多态陷阱:C++的隐秘角落
  • 使用 Altair RapidMiner 将机器学习引入您的 Mendix 应用程序
  • 从IFA再出发:中国制造与海信三筒洗衣机的“答案”
  • SQLite 数据库核心知识与 C 语言编程
  • unity中通过拖拽,自定义scroll view中子物体顺序
  • 最长上升子序列的长度最短连续字段和(动态规划)
  • 2025年最新AI大模型原理和应用面试题
  • Docker 轻量级管理Portainer
  • Aider AI Coding 智能上下文管理深度分析
  • 【Vue3】02-Vue3工程目录分析