文章目录
- 专栏导读
- 日志格式化类成员介绍
- patrern
- items
- 格式化子项类的设计
- 抽象格式化子项基类
- 日志主体消息子项
- 日志等级子项
- 时间子项
- localtime_r介绍
- strftime介绍
- 源码文件名子项
- 源码文件行号子项
- 线程ID子项
- 日志器名称子项
- 制表符子项
- 换行符子项
- 原始字符串子项
- 日志格式化类的设计
- 设计思想
- 接口实现
- Formatter
- format
- parsePattern
- createItem
- 日志输出格式化类整理
专栏导读
🌸作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
🌸专栏简介:本文收录于 C++项目——基于多设计模式下的同步与异步日志系统
🌸相关专栏推荐:C语言初阶系列、C语言进阶系列 、C++系列、数据结构与算法、Linux
日志格式化类成员介绍
日志消息格式化类主要负责将日志消息进行格式化
。类中包含以下成员:
patrern
- 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、文件名等等,我们针对不同的元素设计不同的格式化子项。
换句话说,不同的格式化子项从日志消息对象中提取出指定元素,转换为规则字符串并按顺序保存在一块内存空间中。
/*
%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等)。我们通过设计不同的格式化子项来取出指定的元素,并将它们追加到一块内存空间中。
但是由于不同的格式化子项类对象类型也各不相同
,我们就采用多态
的思想,抽象出一个格式化子项基类,基于基类派生出不同的格式化子项类
。这样就可以定义父类指针的数组,指向不同的格式化子项子类对象。
抽象格式化子项基类
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream &out, const LogMsg &msg) = 0;
};
- 在基类中定义一个
智能指针对象
,方便管理; format
的参数为一个IO流对象,一个LogMsg对象。作用为提取LogMsg对象中的指定元素追加到流对象中。
日志主体消息子项
class MsgFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._payload;
}
};
日志等级子项
class MsgFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._payload;
}
};
时间子项
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
结构中。
函数原型如下:
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
结构包含以下成员变量:
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语言中非常常用,特别是在需要将日期和时间以不同的格式输出到屏幕、文件或其他输出设备时。
函数原型如下:
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,表示字符串无法完全存储在给定的缓冲区中。
源码文件名子项
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;
}
};
线程ID子项
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][%c][%f:%l][%p]%T%m%n
,其中[
、]
、:
等符号是不属于上述任何子项的,这些符号不需要被解析,它们会作为日志内容被直接输出。
日志格式化类的设计
设计思想
日志格式化Formatter
类中提供四个接口:
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
// 时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行
Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
: _pattern(pattern)
{
assert(parsePattern()); // 确保格式化字符串有效
}
format
// 对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
为该格式化字符
,val
为null
;若为原始字符串
则key
为null
,val
为原始字符串内容
。
得到数组之后,根据数组内容,调用createItem
函数创建对应的格式化子项对象
,添加到items
成员中。
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
- 根据不同的格式化字符创建不同得格式化子项对象;
// 根据不同的格式化字符创建不同得格式化子项对象
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();
}
至此,日志消息格式化类已经全部实现完毕。
日志输出格式化类整理
#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
⭐本期赠书⭐
《数据结构与算法分析》 《计算机网络:自顶向下法》 《现代操作系统》 《深入理解计算机系统》
参与方式:评论区回复“C永不落幕”