日志系统第三弹:日志消息和格式化模块的实现

news2024/9/21 21:59:53

日志系统第三弹:日志消息和格式化模块的实现

  • 一、日志消息模块的实现
  • 二、日志格式化模块的设计
    • 1.格式化占位符的约定
    • 2.如何打印
      • 1.各种零件类
        • 1.抽象类
        • 2.简单的零件类
        • 3.日期零件类
        • 4.非格式化数据零件类
      • 2.Formatter
    • 3.如何解析
  • 三、日志格式化模块的实现
    • 1.解析函数
    • 2.createComponent
    • 3.其他函数
  • 四、测试

一、日志消息模块的实现

日志往往需要包含以下字段:

时间(年月日时分秒)
日志等级
文件名和行号
线程ID
日志器名称(日志器的唯一标识)
消息主体

日志等级主要分为:

DEBUG(调试信息)
INFO(一般信息)
WARNING(警告信息,表示可能出现问题,但尚未出错)
ERROR(错误信息,表示程序运行出现了问题,但通常不会阻止其继续运行)
FATAL(致命错误,表示程序无法继续运行)

当项目从调试阶段结束之后,准备发布了,一般都会设置一个日志等级,只允许等级>=限制等级的日志进行输出,这样就无需到处屏蔽对应的调试信息了

同样的,当项目出了BUG需要紧急调试修复时,就可以通过降低日志等级来恢复DEBUG级别的日志打印,无需到处取消对代码的屏蔽了

因此我们需要在提供一个最高级别的日志等级OFF,表示:当日志限制等级调整为OFF时,所有的日志都无法打印了

我们要对外提供一个用来将枚举值转为字符串的函数

struct LogLevel
{
    enum class value
    {
        UNKNOWN = 0,
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL,
        OFF
    };
    static std::string LogLevel_Name(value level)
    {
        switch (level)
        {
        case value::DEBUG:
            return "DEBUG";
        case value::INFO:
            return "INFO";
        case value::WARNING:
            return "WARNING";
        case value::ERROR:
            return "ERROR";
        case value::FATAL:
            return "FATAL";
        case value::OFF:
            return "OFF";
        }
        return "UNKNOWN";
    }
};

struct LogMessage
{
    LogMessage(LogLevel::value level, const std::string &file, int line, const std::string &logger_name, const std::string &body)
        : _level(level), _time(ns_helper::DateHelper::now()), _file(file), _line(line), _logger_name(logger_name), _body(body), _thread_id(std::this_thread::get_id()) {}
    LogLevel::value _level;
    time_t _time;
    std::string _file;
    int _line;
    std::thread::id _thread_id;
    std::string _logger_name;
    std::string _body;
};

二、日志格式化模块的设计

日志格式化模块主要负责:

  1. 约定各种格式化占位符
  2. 允许用户传入自定义格式化字符串,在内部解析
  3. 把用户传入的消息主体格式化为日志消息

1.格式化占位符的约定

日志消息只包含这6个字段:

时间(年月日时分秒)
日志等级
文件名和行号
线程ID
日志器名称
消息主体

但是跟printf一样,用户可以在format格式化字符串当中打印纯字符printf("hello %s\n") hello 就是用户打印的纯字符
用户还可以打印\n、\t等等格式字符

因此我们还要对纯字符,\n,\t进行约定:

%d  时间(date)
%r  日志等级(rate)
%f  文件名(file)
%l  行数(line)
%i  线程ID(id)
%b  消息主体(body)
%n  换行符(\n)
%t  水平制表符(\t)
%g  日志器名称(logger_name)

因为时间需要二次格式化(年月日,时分秒的格式化)
所以,我们直接借鉴日期具体的格式化占位符

"%Y-%m-%d %H:%M:%S"
2024-08-18 14:49:42

%Y :%m :%d :%H :%M :%S :

因此作为分割,我们规定{}是二级格式

2.如何打印

直接暴力解析并打印? 可以是可以,但是太不优雅了,而且代码的可扩展性不好,万一哪一天又想加一种格式化占位符呢

而且我们是同一个日志器打印出来的日志格式都是相同的,因此对于日志格式化模块来说,它只负责打印同一类型的日志

所以我们更希望它能够把日志格式给记录下来,到时候打印的时候直接根据记录的格式拼接好字符串即可

因此我们就需要把日志按照最小单位进行拆分

借助类似于建造者模式的思想,我们把组成日志的每个零件拆分一下,各成一类,都继承自同一个抽象类,重写同一个接口

1.各种零件类

1.抽象类
class FormatComponent
{
public:
    using ptr=std::shared_ptr<FormatComponent>;

    virtual ~FormatComponent(){}
    // 将消息内容流入out流当中
    virtual void format(std::ostream &out, const LogMessage &message) = 0;
};

因为流更好用,所以我们才不用字符串,而且字符串可以通过ostringstream的str函数拿到,且ostream是ostringstream的父类

2.简单的零件类
class LevelComponent : public FormatComponent
{
public:
    virtual void format(std::ostream &out, const LogMessage &message)
    {
        out << LogLevel::LogLevel_Name(message._level);
    }
};

class BodyComponent : public FormatComponent
{
public:
    virtual void format(std::ostream &out, const LogMessage &message)
    {
        out<<message._body;
    }
};

class FileComponent : public FormatComponent
{
public:
    virtual void format(std::ostream &out, const LogMessage &message)
    {
        out<<message._file;
    }
};

class LineComponent : public FormatComponent
{
public:
    virtual void format(std::ostream &out, const LogMessage &message)
    {
        out<<message._line;
    }
};

class NameComponent : public FormatComponent
{
public:
    virtual void format(std::ostream &out, const LogMessage &message)
    {
        out<<message._logger_name;
    }
};

class LineBreakComponent:public FormatComponent
{
public:
    virtual void format(std::ostream& out,const LogMessage& message)
    {
        out<<"\n";
    }
};

class TabComponent:public FormatComponent
{
public:
    virtual void format(std::ostream& out,const LogMessage& message)
    {
        out<<"\t";
    }
};

class ThreadIdComponent : public FormatComponent
{
public:
    virtual void format(std::ostream &out, const LogMessage &message)
    {
        out << message._thread_id;
    }
};
3.日期零件类

因为日期零件类有自己的二级格式,所以我们构造日期零件类的时候需要把二级格式传给他,由他进行保存

因为日期归他管

而解析日期格式有专门的系统调用函数strftime

size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

根据format格式化字符串和tm日期结构体进行解析,将解析结果放到字符串s当中,字符串最大程度为max(防止缓冲区溢出的风险)

class DateComponent : public FormatComponent
{
public:
    DateComponent(const std::string &format)
        : _format(format) {}

   	virtual void format(std::ostream &out, const LogMessage &message)
    {
        const int max_size = _format.size() + 32;
        char buf[max_size] = {0};
        time_t now = message._time;
        struct tm *tm = localtime(&now);
        strftime(buf, max_size - 1, _format.c_str(), tm);
        out << buf;
    }

private:
    std::string _format;
};
4.非格式化数据零件类

对于非格式化数据的打印,直接打印原本的字符即可,因此非格式化数据零件类无需LogMessage而是需要一个string,所以他在构造的时候,也是需要给一个string的

class OtherComponent : public FormatComponent
{
public:
    OtherComponent(const std::string &body)
        : _body(body) {}

    virtual void format(std::ostream &out, const LogMessage &message)
    {
        out << _body;
    }

private:
    std::string _body;
};

2.Formatter

要求构造时就要传入pattern日志模式,然后我们进行解析,将对应的零件类保存在vector当中

然后提供两个消息的格式化接口,一个是可以直接打印日志,另一个是返回string类型的日志

默认格式是这样的:

"[%d{%Y-%m-%d %H:%M:%S}] [%r] [%f:%l] [%g] [%i] %b%n"
日期[年月日 时分秒] [日志等级] [文件名:行号] [日志器名称] [线程ID] 消息主体 换行
class Formatter
{
public:
    // 日期[年月日 时分秒] [日志等级] [文件名:行号] [日志器名称] [线程ID] 消息主体 换行
    Formatter(const std::string &pattern="[%d{%Y-%m-%d %H:%M:%S}] [%r] [%f:%l] [%g] [%i] %b%n") {}

    void format(std::ostream &out, const LogMessage &message) {}

    std::string format(const LogMessage &message) {}
private:
    bool parse() {}
	FormatComponent::ptr createComponent(const std::string& symbol,const std::string& val){}
    // 格式
    std::string _pattern;
    std::vector<FormatComponent::ptr> _components;
};

注意: %%%的转义,也就是打印%自己。相当于\\就是\

3.如何解析

维护一个parse_ret用来存放解析结果,里面的类型是pair<int,int>,first是d、r、f、l、i、b、n、t、g、空串,second是OtherComponent 要的body或者DateComponent 要的format

如果first为空,则代表该解析结果是OtherComponent

具体步骤:

遍历_pattern,维护一个非结构化字符串unformat_val ,如果当前字符不是%,则放入unformat_val的尾部,往后走一步

否则,分为三种情况:
(1)下一个字符是%,则将%放入unformat_val的尾部
(2)下一个字符是d,则往后找} , 将{}里面的格式提出来,存为val, 后续要传递给createComponent作为参数
(3)否则,就按普通的格式化字符来匹配

一旦遇到格式化字符,最需要先把unformat_val字符串先抛入vec当中保存下来
在这里插入图片描述
大家画个图,一下子就出来

三、日志格式化模块的实现

1.解析函数

其实就是个分类讨论而已,顺着思路写很简单

bool parse()
{
    // 1. 解析字符串
    std::string unformat_val;
    size_t index = 0;
    size_t start = 0;
    std::vector<std::pair<std::string, std::string>> parse_ret;
    while (index < _pattern.size())
    {
        if (_pattern[index] != '%')
        {
            unformat_val += _pattern[index++];
        }
        else if (index + 1 >= _pattern.size())
        {
            std::cout << "解析失败,因为%后面没有字符,pattern:" << _pattern << "\n";
            return false;
        }
        else if (_pattern[index + 1] == '%')
        {
            unformat_val += '%';
            index += 2;
        }
        else if (_pattern[index + 1] == 'd')
        {
            if (!unformat_val.empty())
            {
                // 首先先将unformat_val放进去
                parse_ret.push_back({"", unformat_val});
                unformat_val.clear();
            }
            // 取出{}之内的二级格式
            size_t start = _pattern.find('{', index + 2), end = _pattern.find('}', index + 2);
            if (start == std::string::npos || end == std::string::npos)
            {
                std::cout << "解析失败,因为%d后面未找到{},pattern:" << _pattern << "\n";
                return false;
            }
            parse_ret.push_back({"d", _pattern.substr(start + 1, (end - 1) - (start + 1) + 1)});
            index = end + 1;
        }
        else
        {
            if (!unformat_val.empty())
            {
                // 首先先将unformat_val放进去
                parse_ret.push_back({"", unformat_val});
                unformat_val.clear();
            }
            parse_ret.push_back({_pattern.substr(index + 1, 1), ""});
            index += 2;
        }
    }
    // 注意:非格式化字符是可以充底的,所以这里需要再次搞一下
    if (!unformat_val.empty())
    {
        // 首先先将unformat_val放进去
        parse_ret.push_back({"", unformat_val});
        unformat_val.clear();
    }
    // 2. 构造组成成分
    for (auto &elem : parse_ret)
    {
        _components.push_back(createComponent(elem.first, elem.second));
    }
    return true;
}

2.createComponent

FormatComponent::ptr createComponent(const std::string &symbol, const std::string &val)
{
    if (symbol == "d")
        return std::make_shared<DateComponent>(val);
    else if (symbol == "r")
        return std::make_shared<LevelComponent>();
    else if (symbol == "f")
        return std::make_shared<FileComponent>();
    else if (symbol == "l")
        return std::make_shared<LineComponent>();
    else if (symbol == "g")
        return std::make_shared<NameComponent>();
    else if (symbol == "i")
        return std::make_shared<ThreadIdComponent>();
    else if (symbol == "b")
        return std::make_shared<BodyComponent>();
    else if (symbol == "n")
        return std::make_shared<LineBreakComponent>();
    else if (symbol == "t")
        return std::make_shared<TabComponent>();
    else if (symbol == "")
        return std::make_shared<OtherComponent>(val);
    else
    {
        std::cout << "不存在该格式化字符: %" << symbol << "\n";
        abort();
        return FormatComponent::ptr(); // 返回一个空指针
    }
}

3.其他函数

// 日期[年月日 时分秒] [日志等级] [文件名:行号] [日志器名称] [线程ID] 消息主体 换行
Formatter(const std::string &pattern = "[%d{%Y-%m-%d %H:%M:%S}] [%r] [%f:%l] [%g] [%i] %b%n")
    : _pattern(pattern)
{
    if (!parse())
    {
        std::cout << "解析pattern失败!, pattern: " << _pattern << "\n";
        abort();
    }
}

void format(std::ostream &out, const LogMessage &message)
{
    for (auto &elem : _components)
    {
        elem->format(out, message);
    }
}

std::string format(const LogMessage &message)
{
    std::ostringstream oss;
    format(oss, message);
    return oss.str();
}

四、测试

g++ -o mytest test.cc -std=c++11 -pthread

一定要链接pthread库,否则无法拿到正确的thread_id

int main()
{
    LogMessage message(LogLevel::value::WARNING, __FILE__, __LINE__, "default_logger", "I am here, World");

    std::shared_ptr<Formatter> formatter = std::make_shared<Formatter>();
    std::cout << formatter->format(message);

    formatter = std::make_shared<Formatter>("abc%%%%def%t%d{%Y %m %d}%%%r %b%n");
    std::cout << formatter->format(message);
    return 0;
}

wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/test$ ./mytest 
[2024-08-18 17:50:18] [WARNING] [test.cc:6] [default_logger] [140422774794048] I am here, World
abc%%def        2024 08 18%WARNING I am here, World

以上就是日志系统第三弹:日志消息和格式化模块的实现的全部内容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2153411.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一文详解Unity下RTMP推送|轻量级RTSP服务|RTSP|RTMP播放模块说明

技术背景 好多开发者&#xff0c;对Unity下的模块&#xff0c;不甚了解&#xff0c;实际上&#xff0c;除了Windows/Linux/Android/iOS Native SDK&#xff0c;大牛直播SDK发布了Unity环境下的RTMP推流|轻量级RTSP服务&#xff08;Windows平台Linux平台Android平台&#xff09…

Windows安装Oracle11gR2(图文教程)

本章教程&#xff0c;记录在Windows10上安装Oracle11gR2过程。 一、下载安装包 通过网盘分享的文件&#xff1a;oracle11g 链接: https://pan.baidu.com/s/15ilciQ5NlKWtClklmdAH_w?pwds4dd 提取码: s4dd 二、下载并解压文件 将网盘中的安装包文件下载到本地&#xff0c;在此之…

谷歌收录查询工具,好用的谷歌收录查询工具应具备的这5个特性

在探讨如何高效利用谷歌收录查询工具以优化网站可见性和搜索引擎排名时&#xff0c;好用这一标准往往涵盖了工具的准确性、易用性、功能全面性以及对搜索引擎算法变化的适应性等多个方面。 1.准确性 首先&#xff0c;一款好用的谷歌收录查询工具必须能够提供高度准确的数…

C Prime Plus 第6章习题

你该逆袭了 红色标注的是&#xff1a;错误的答案 蓝色标注的是&#xff1a;正确的答案 绿色标注的是&#xff1a;做题时有疑问的地方 橙色标注的是&#xff1a;答案中需要着重注意的地方 练习题 一、复习题1、2、3、4、5、我的答案&#xff1a;错误正确答案&#xff1a; 6、7、…

窥探 引用拷贝、浅拷贝、深拷贝 的那些事 (clone版)

谁家玉笛暗飞声 散入春风满洛城 往期回顾✨内部类 目录✨ 引用拷贝 介绍 总结 浅拷贝 介绍 浅拷贝的步骤 深拷贝 介绍 引用拷贝 介绍 引用拷贝就是我们常用的 “赋值” &#xff0c;只是复制了原对象的引用&#xff0c;即两个对象指向同一块内存堆地址。修改其中的一个对象会影…

【图灵完备 Turing Complete】游戏经验攻略分享 Part.6 处理器架构2 函数

新的架构来了&#xff0c;本游戏的最后一个攻略分享&#xff0c;最后汇编部分无非是对于操作码的熟练&#xff0c;硬件没有问题&#xff0c;那么也就无关痛痒了。 汇编实现&#xff0c;两数相或和两数相与非一起相与即可。 八位异或器&#xff0c;整就完事了。 有手就行。 利…

【梯度下降算法学习笔记】

梯度下降单参数求解 经过之前的学习我们来引入梯度下降的理念 α \alpha α为学习率 w 1 w 初 − α ∂ J ( w ) ∂ w w_1w_初-\alpha\frac{\partial J(w)}{\partial w} w1​w初​−α∂w∂J(w)​ w 2 w 1 − α ∂ J ( w 1 ) ∂ w 1 w_2w_1-\alpha\frac{\partial J(w_1)}…

国庆电影扎堆来袭,AI智能体帮你推荐必看佳片!(附制作教程)

大家好&#xff0c;我是凡人。 今天看到新闻&#xff0c;发现国庆有10部影片要扎堆儿上映&#xff0c;对于选择困难症的我属实有点难选&#xff0c;同时也想避开一些坑省的浪费金钱和时间。 本着不知道就问AI的习惯&#xff0c;想问问大模型怎么看&#xff0c;但做了简单的交…

MySQL 优化器:理解与探秘

在 MySQL 数据库的世界里&#xff0c;优化器扮演着至关重要的角色。它就像是一位幕后的魔法师&#xff0c;默默地为数据库的高效运行贡献着力量。那么&#xff0c;MySQL 优化器究竟是什么&#xff1f;它又是如何工作的呢&#xff1f;让我们一起来揭开它的神秘面纱。 一、MySQL…

行业人工智能研究-Python自监督方式学习图像表示算法

学术界人工智能研究落后于工业界 摘要 行业或工业界在人工智能研究上超出学术界&#xff0c;并占据着大量的计算力&#xff0c;数据集和人才诱人的薪水和明朗的预期吸引大量人才离开学术界&#xff0c;涌入行业或工业界即使&#xff0c;比如Meta开源其人工智能模型&#xff0…

二分查找算法(1) _二分查找_模板

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 二分查找算法(1) _二分查找模板 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 1. 二…

掌握Android开发新趋势:Jetpack与现代架构技术全解析

随着Android开发技术的不断进步&#xff0c;Jetpack和现代架构技术已成为构建高效、可维护应用的关键。本文将为您介绍一套全面的学习资料&#xff0c;包括大纲、PDF文档、源代码以及学习视频&#xff0c;帮助您深入理解Jetpack核心库、架构组件以及现代开发工具。 内容&#…

linux-基础知识4

网络连接性测试 ping ping可以用来测试本机与目标主机的连通速度网络稳定性 ping -c 5 -s 1024 目标主机ip地址 -c 表示ping包的个数,linux如果缺省-c会一直ping下去&#xff0c;windows平台的选项是-n -s指定ping发送数据的字节数默认是84字节。windows的是-l 没有问题时会之…

如何设计出一个比较全面的测试用例

目录 1. 测试用例的基本要素(不需要执行结果) 2. 测试用例的给我们带来的好处 3. 用例编写步骤 4. 设计测试用例的方法 4.1 基于需求进行测试用例的设计 4.2 具体的设计方法 1.等价类 2.边界值 3.判定表&#xff08;因果图&#xff09; 4.正交表法 5.场景设计法 6.错误猜测…

IO流体系(FiletOutputStream)

书写步骤&#xff1a; 1.创建字节输出流对象 细节1:参数是字符串表示的路径或者是File对象都是可以的 细节2:如果文件不存在会创建一个新的文件&#xff0c;但是要保证父级路径是存在的。 细节3:如果文件已经存在&#xff0c;则会清空文件 2.写数据 细节:write方法的参数…

Python | 绘制核密度图

写在前面 台风天&#xff0c;适合敲代码。前两天正好看到一个有意思的核密度图&#xff0c;使用的是seaborn绘制的。之前了解过这个包&#xff0c;但是一致没有去绘制相关的图&#xff0c;这次正好去学习一下相关的函数。 绘制结果如下所示&#xff1a; 主要两个有意思的地方…

二维光场分析

一、单色光波长的复振幅表示 实波函数 复波函数 复振幅 由于时间因子相同,可以用复振幅来描述 光强 1.1 球面波的复振幅(单色点光源发出的光波) 等相位面是同心球面,波矢处处与等相位面垂直,即 是 r = 1 处的振幅 发散球面波: 会聚球面波: <

828华为云征文 | 华为云X实例监控与告警管理详解

前言 随着云计算的飞速发展&#xff0c;越来越多的企业将业务部署在云平台上&#xff0c;云服务器实例的管理变得尤为重要。云实例的稳定性、性能及安全性&#xff0c;直接影响着业务的连续性与用户体验。为了确保这些目标的实现&#xff0c;监控与告警是关键手段。本文将详细…

2024华为杯研赛C题原创python代码+结果表(1-2问)|全部可运行|精美可视化

2024华为杯研赛C题原创python代码结果表&#xff08;1-2问&#xff09;&#xff5c;全部可运行&#xff5c;精美可视化 以下均为python代码&#xff0c;推荐用anaconda中的notebook当作编译环境 第一问&#xff1a; import pandas as pd import numpy as np import matplotl…

pg入门18—如何使用pg gis

1. 下载postgre gis镜像 2. 运行镜像 docker run -p 15432:5432 -d -e POSTGRES_PASSWORDAb123456! postgis/postgis:12-3.4-alpine 3. 使用gis # 进入容器&#xff0c;登录pgdocker exec -it bash# 登录数据库psql -U postgres# 创建数据库CREATE DATABASE mygeotest;# 使用…