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

一、日志消息模块的实现

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

时间(年月日时分秒)
日志等级
文件名和行号
线程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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

program-learner

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值