C++项目实战——基于多设计模式下的同步&异步日志系统-⑦-日志输出格式化类设计

文章目录

专栏导读

🌸作者简介:花想云,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人...致力于 C/C++、Linux 学习。

🌸专栏简介:本文收录于 C++项目------基于多设计模式下的同步与异步日志系统

🌸相关专栏推荐:C语言初阶系列C语言进阶系列C++系列数据结构与算法Linux

日志格式化类成员介绍

日志消息格式化类主要负责将日志消息进行格式化。类中包含以下成员:

pattern

  • pattern:保存格式化规则字符串
    • 默认日志输出格式[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n
    • %d 表示日期,包含子格式 {%H:%M:%S}
    • %T 表示缩进;
    • %t 表示线程ID;
    • %c 表示日志器名称;
    • %f 表示源码文件名;
    • %l 表示源码行号;
    • %p 表示日志级别;
    • %m 表示主体消息;
    • %n 表示换行;

格式化规则字符串控制了日志的输出格式。定义格式化字符,就是为了方便让用户自己决定以何种形式将日志消息进行输出。

例如,在默认输出格式下,输出的日志消息为:

items

  • std::vector< FormatItem::ptr > items:用于按序保存格式化字符串对应的格式化子项对象。
    • MsgFormatItem:表示要从LogMsg中取出有效载荷;
    • LevelFormatItem:表示要从LogMsg中取出日志等级;
    • LoggerFormatItem:表示要从LogMsg中取出日志器名称;
    • ThreadFormatItem:表示要从LogMsg中取出线程ID;
    • TimeFormatItem:表示要从LogMsg中取出时间戳并按照指定格式进行格式化;
    • FileFormatItem:表示要从LogMsg中取出源码所在文件名;
    • LineFormatItem:表示要从LogMsg中取出源码所在行号;
    • TabFormatItem:表示一个制表符缩进;
    • NLineFormatItem:表示一个换行;
    • OtherFormatItem:表示非格式化的原始字符串;

一个日志消息对象包含许多元素,如时间、线程ID、文件名等等,我们针对不同的元素设计不同的格式化子项。

换句话说,不同的格式化子项从日志消息对象中提取出指定元素,转换为规则字符串并按顺序保存在一块内存空间中。

cpp 复制代码
/*
    %d 表示日期,包含子格式 {%H:%M:%S}
    %t 表示线程ID
    %c 表示日志器名称
    %f 表示源码文件名
    %l 表示源码行号
    %p 表示日志级别
    %m 表示主体消息
    %n 表示换行
*/
class Formatter
{
public:
	// ...
private:
    std::string _pattern; // 格式化规则字符串
    std::vector<FormatItem::ptr> _items;
};

格式化子项类的设计

刚才提到对于一条日志消息message,其中包含很多元素(时间、线程ID等)。我们通过设计不同的格式化子项来取出指定的元素,并将它们追加到一块内存空间中。

但是由于不同的格式化子项类对象类型也各不相同,我们就采用多态的思想,抽象出一个格式化子项基类,基于基类派生出不同的格式化子项类。这样就可以定义父类指针的数组,指向不同的格式化子项子类对象。

抽象格式化子项基类

cpp 复制代码
class FormatItem
{
public:
    using ptr = std::shared_ptr<FormatItem>;
    virtual void format(std::ostream &out, const LogMsg &msg) = 0;
};
  • 在基类中定义一个智能指针对象,方便管理;
  • format的参数为一个IO流对象,一个LogMsg对象。作用为提取LogMsg对象中的指定元素追加到流对象中。

日志主体消息子项

cpp 复制代码
class MsgFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._payload;
    }
};

日志等级子项

cpp 复制代码
class MsgFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._payload;
    }
};

时间子项

cpp 复制代码
class TimeFormatItem : public FormatItem
{
public:
    TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {}

    void format(std::ostream &out, const LogMsg &msg) override
    {
        struct tm t;
        localtime_r(&msg._ctime, &t);
        char tmp[32] = {0};
        strftime(tmp, 31, _time_fmt.c_str(), &t);
        out << tmp;
    }

private:
    std::string _time_fmt; // %H:%M:%S
};
  • 时间子项可以设置子格式,在构造函数中需要传递一个子格式字符串来控制时间子格式;

localtime_r介绍

在LogMsg对象中,时间元素是一个时间戳数字,不方便观察时间信息。我们需要将该时间戳转化为易于观察的时分秒的格式。

localtime_r函数是C标准库中的一个函数,用于将时间戳(表示自1970年1月1日以来的秒数)转换为本地时间的表示。这个函数是线程安全的版本,它接受两个参数:一个指向时间戳的指针和一个指向struct tm类型的指针,它会将转换后的本地时间信息存储在struct tm结构中。

函数原型如下:

cpp 复制代码
struct tm *localtime_r(const time_t *timep, struct tm *result);
  • timep参数是指向时间戳的指针;
  • result参数是指向struct tm类型的指针,用于存储转换后的本地时间信息;
  • localtime_r函数返回一个指向struct tm结构的指针,同时也将结果存储在result参数中;
  • struct tm结构包含了年、月、日、时、分、秒等时间信息的成员变量,可用于格式化和输出时间;

struct tm类型
struct tm是C语言中的一个结构体类型,用于表示日期和时间的各个组成部分。

struct tm结构包含以下成员变量:

cpp 复制代码
struct tm
{
	int tm_sec; // 秒(0-59)
	int tm_min; // 分钟(0-59)
	int tm_hour; // 小时(0-23)
	int tm_mday; // 一个月中的日期(1-31)
	int tm_mon; // 月份(0-11,0代表1月)
	int tm_year; // 年份(从1900年起的年数,例如,121表示2021年)
	int tm_wday; // 一周中的星期几(0-6,0代表星期日)
	int tm_yday; // 一年中的第几天(0-365)
	int tm_isdst; // 是否为夏令时(正数表示是夏令时,0表示不是,负数表示夏令时信息不可用)
}

strftime介绍

strftime函数是C标准库中的一个函数,用于将日期和时间按照指定的格式进行格式化,并将结果存储到一个字符数组中。这个函数在C语言中非常常用,特别是在需要将日期和时间以不同的格式输出到屏幕、文件或其他输出设备时。

函数原型如下:

cpp 复制代码
size_t strftime(char *s, size_t maxsize, const char *format, const struct tm *timeptr);
  • s:一个指向字符数组的指针,用于存储格式化后的日期和时间字符串;
  • maxsize:指定了字符数组 s 的最大容量,以防止缓冲区溢出;
  • format:一个字符串,用于指定日期和时间的输出格式。该字符串可以包含- 各种格式化控制符,例如%Y表示年份,%m表示月份等等;
  • timeptr:一个指向struct tm 结构的指针,表示待格式化的日期和时间;

返回值:

  • strftime函数返回生成的字符数(不包括空终止符\0),如果生成的字符数大于 maxsize,则返回0,表示字符串无法完全存储在给定的缓冲区中。

源码文件名子项

cpp 复制代码
class FileFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._file;
    }
};

源码文件行号子项

cpp 复制代码
class LineFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._line;
    }
};

线程ID子项

cpp 复制代码
class ThreadFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._tid;
    }
};

日志器名称子项

cpp 复制代码
class LoggerFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._logger;
    }
};

制表符子项

cpp 复制代码
class TabFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << "\t";
    }
};

换行符子项

cpp 复制代码
class NLineFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << "\n";
    }
};

原始字符串子项

cpp 复制代码
class OtherFormatItem : public FormatItem
{
public:
    OtherFormatItem(const std::string &str) : _str(str) {}
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << _str;
    }

private:
    std::string _str;
};

解释一下原始字符串:

  • 例如一个格式化字符串为[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n,其中[]:等符号是不属于上述任何子项的,这些符号不需要被解析,它们会作为日志内容被直接输出。

日志格式化类的设计

设计思想

日志格式化Formatter类中提供四个接口:

cpp 复制代码
class Formatter
{
public:
    using ptr = std::shared_ptr<Formatter>;
    Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
    void format(std::ostream &out, const LogMsg &msg);
    std::string format(const LogMsg &msg);
private:
    bool parsePattern();
    // 根据不同的格式化字符创建不同得格式化子项对象
    FormatItem::ptr createItem(const std::string &key, const std::string &val);
private:
    std::string _pattern; // 格式化规则字符串
    std::vector<FormatItem::ptr> _items;
};
  • Formatter:构造函数,构造一个formatter对象。函数参数为一个格式化字符串用来初始化成员pattern
  • format:提供两个重载函数,函数作用为将LogMsg中元素提取出来交由对应的格式化子项处理;可以将LogMsg进行格式化,并追加到流对象当中,也可以直接返回格式化后的字符串;
  • parsePattern:用于解析规则字符串_pattern
  • createItem:用于根据不同的格式化字符串创建不同的格式化子项对象

接口实现

Formatter

cpp 复制代码
// 时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行
Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
    : _pattern(pattern)
{
    assert(parsePattern()); // 确保格式化字符串有效
}

format

cpp 复制代码
// 对msg进行格式化
void format(std::ostream &out, const LogMsg &msg)
{
    for (auto &item : _items)
    {
        item->format(out, msg);
    }
}

std::string format(const LogMsg &msg)
{
    std::stringstream ss;
    format(ss, msg);
    return ss.str();
}

parsePattern

函数设计思想

  • 函数的主要逻辑是从前往后的处理格式化字符串。以默认格式化字符串"[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"为例:
    • 从前往后遍历,如果没有遇到%则说明之前的字符都是原始字符串;
    • 遇到%,则看紧随其后的是不是另一个%,如果是,则认为%就是原始字符串;
    • 如果%后面紧挨着的是格式化字符(c、f、l、S等),则进行处理;
    • 紧随格式化字符之后,如果有{,则认为在{之后、}之前都是子格式内容;

在处理过程中,我们需要将得到的结果保存下来,于是我们可以创建一个vector,类型为一个键值对(key,val)。如果是格式化字符,则key为该格式化字符valnull;若为原始字符串keynullval原始字符串内容

得到数组之后,根据数组内容,调用createItem函数创建对应的格式化子项对象,添加到items成员中。

cpp 复制代码
bool parsePattern()
{
    std::vector<std::pair<std::string, std::string>> fmt_order;
    size_t pos = 0;
    std::string key, val;
    while (pos < _pattern.size())
    {
        if (_pattern[pos] != '%')
        {
            val.push_back(_pattern[pos++]);
            continue;
        }

        if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
        {
            val.push_back('%');
            pos += 2;
            continue;
        }

        if (val.empty() == false)
        {
            fmt_order.push_back(std::make_pair("", val));
            val.clear();
        }

        pos += 1;
        if (pos == _pattern.size())
        {
            std::cout << "%之后没有格式化字符\n";
            return false;
        }

        key = _pattern[pos];
        pos += 1;

        if (pos < _pattern.size() && _pattern[pos] == '{')
        {
            pos += 1;
            while (pos < _pattern.size() && _pattern[pos] != '}')
            {
                val.push_back(_pattern[pos++]);
            }

            if (pos == _pattern.size())
            {
                std::cout << "子规则{}匹配出错\n";
                return false;
            }

            pos += 1;
        }

        fmt_order.push_back(std::make_pair(key, val));
        key.clear();
        val.clear();
    }

    for (auto &it : fmt_order)
    {
        _items.push_back(createItem(it.first, it.second));
    }
    return true;
}

createItem

  • 根据不同的格式化字符创建不同得格式化子项对象;
cpp 复制代码
// 根据不同的格式化字符创建不同得格式化子项对象
FormatItem::ptr createItem(const std::string &key, const std::string &val)
{
    if (key == "d")
        return std::make_shared<TimeFormatItem>(val);
    if (key == "t")
        return std::make_shared<ThreadFormatItem>();
    if (key == "c")
        return std::make_shared<LoggerFormatItem>();
    if (key == "f")
        return std::make_shared<FileFormatItem>();
    if (key == "l")
        return std::make_shared<LineFormatItem>();
    if (key == "p")
        return std::make_shared<LevelFormatItem>();
    if (key == "T")
        return std::make_shared<TabFormatItem>();
    if (key == "m")
        return std::make_shared<MsgFormatItem>();
    if (key == "n")
        return std::make_shared<NLineFormatItem>();
    if (key == "")
        return std::make_shared<OtherFormatItem>(val);
    std::cout << "没有对应的格式化字符串:%" << key << std::endl;
    abort();
}

至此,日志消息格式化类已经全部实现完毕。


日志输出格式化类整理

cpp 复制代码
#ifndef __M_FMT_H__
#define __M_FMT_H__

#include "level.hpp"
#include "message.hpp"
#include <vector>
#include <sstream>
#include <ctime>
#include <cassert>

namespace LOG
{
    // 抽象格式化子项基类
    class FormatItem
    {
    public:
        using ptr = std::shared_ptr<FormatItem>;
        virtual void format(std::ostream &out, const LogMsg &msg) = 0;
    };

    class MsgFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._payload;
        }
    };

    class LevelFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << LogLevel::tostring(msg._level);
        }
    };

    class TimeFormatItem : public FormatItem
    {
    public:
        TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {}

        void format(std::ostream &out, const LogMsg &msg) override
        {
            struct tm t;
            localtime_r(&msg._ctime, &t);
            char tmp[32] = {0};
            strftime(tmp, 31, _time_fmt.c_str(), &t);
            out << tmp;
        }

    private:
        std::string _time_fmt; // %H:%M:%S
    };

    class FileFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._file;
        }
    };

    class LineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._line;
        }
    };

    class ThreadFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._tid;
        }
    };

    class LoggerFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._logger;
        }
    };

    class TabFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << "\t";
        }
    };

    class NLineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << "\n";
        }
    };

    class OtherFormatItem : public FormatItem
    {
    public:
        OtherFormatItem(const std::string &str) : _str(str) {}
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << _str;
        }

    private:
        std::string _str;
    };

    /*
        %d 表示日期,包含子格式 {%H:%M:%S}
        %t 表示线程ID
        %c 表示日志器名称
        %f 表示源码文件名
        %l 表示源码行号
        %p 表示日志级别
        %m 表示主体消息
        %n 表示换行
    */
    class Formatter
    {
    public:
        using ptr = std::shared_ptr<Formatter>;
        // 时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行
        Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
            : _pattern(pattern)
        {
            assert(parsePattern());
        }

        // 对msg进行格式化
        void format(std::ostream &out, const LogMsg &msg)
        {
            for (auto &item : _items)
            {
                item->format(out, msg);
            }
        }

        std::string format(const LogMsg &msg)
        {
            std::stringstream ss;
            format(ss, msg);
            return ss.str();
        }

    private:
        // 解析格式化字符串
        bool parsePattern()
        {
            std::vector<std::pair<std::string, std::string>> fmt_order;
            size_t pos = 0;
            std::string key, val;
            while (pos < _pattern.size())
            {
                if (_pattern[pos] != '%')
                {
                    val.push_back(_pattern[pos++]);
                    continue;
                }

                if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
                {
                    val.push_back('%');
                    pos += 2;
                    continue;
                }

                if (val.empty() == false)
                {
                    fmt_order.push_back(std::make_pair("", val));
                    val.clear();
                }

                pos += 1;
                if (pos == _pattern.size())
                {
                    std::cout << "%之后没有格式化字符\n";
                    return false;
                }

                key = _pattern[pos];
                pos += 1;

                if (pos < _pattern.size() && _pattern[pos] == '{')
                {
                    pos += 1;
                    while (pos < _pattern.size() && _pattern[pos] != '}')
                    {
                        val.push_back(_pattern[pos++]);
                    }

                    if (pos == _pattern.size())
                    {
                        std::cout << "子规则{}匹配出错\n";
                        return false;
                    }

                    pos += 1;
                }

                fmt_order.push_back(std::make_pair(key, val));
                key.clear();
                val.clear();
            }

            for (auto &it : fmt_order)
            {
                _items.push_back(createItem(it.first, it.second));
            }
            return true;
        }

        // 根据不同的格式化字符创建不同得格式化子项对象
        FormatItem::ptr createItem(const std::string &key, const std::string &val)
        {
            if (key == "d")
                return std::make_shared<TimeFormatItem>(val);
            if (key == "t")
                return std::make_shared<ThreadFormatItem>();
            if (key == "c")
                return std::make_shared<LoggerFormatItem>();
            if (key == "f")
                return std::make_shared<FileFormatItem>();
            if (key == "l")
                return std::make_shared<LineFormatItem>();
            if (key == "p")
                return std::make_shared<LevelFormatItem>();
            if (key == "T")
                return std::make_shared<TabFormatItem>();
            if (key == "m")
                return std::make_shared<MsgFormatItem>();
            if (key == "n")
                return std::make_shared<NLineFormatItem>();
            if (key == "")
                return std::make_shared<OtherFormatItem>(val);
            std::cout << "没有对应的格式化字符串:%" << key << std::endl;
            abort();
        }

    private:
        std::string _pattern; // 格式化规则字符串
        std::vector<FormatItem::ptr> _items;
    };
}

#endif
相关推荐
迷迭所归处6 分钟前
C++ —— 关于vector
开发语言·c++·算法
CV工程师小林35 分钟前
【算法】BFS 系列之边权为 1 的最短路问题
数据结构·c++·算法·leetcode·宽度优先
white__ice1 小时前
2024.9.19
c++
天玑y2 小时前
算法设计与分析(背包问题
c++·经验分享·笔记·学习·算法·leetcode·蓝桥杯
姜太公钓鲸2332 小时前
c++ static(详解)
开发语言·c++
菜菜想进步2 小时前
内存管理(C++版)
c语言·开发语言·c++
Joker100852 小时前
C++初阶学习——探索STL奥秘——模拟实现list类
c++
科研小白_d.s2 小时前
vscode配置c/c++环境
c语言·c++·vscode
湫兮之风2 小时前
c++:tinyxml2如何存储二叉树
开发语言·数据结构·c++
友友马3 小时前
『 Linux 』HTTP(一)
linux·运维·服务器·网络·c++·tcp/ip·http