前言
最近,在使用spdlog日志库,但是不如自己使用std::cout 输出的方便,想要调整spdlog的格式化输出,但是网上缺少这块比较完整的资料,现在将这部分说明。
本章节主要说明创建日志后怎样格式化输出,以及可以的格式化输出有哪些。
- 痛点:软件上线后跟踪主要依靠日志,但是不能具体到代码,达不到类似断点调试的效果。
- 效果展示:
[Tue Apr 23 00:00:17 2024|04/23/24|ms:981] [debug] [P:22588|t:22600] [Code:/home/zry/src/modules/Connect/OpConnectMD.cpp:415|UploadStatusData(...)] - : device_type = YWIND00
- 重点: 在日志中可以显示:在什么路径下的什么文件的什么函数的第几行打印的日志,从而达到类似断点调试的需求。示例如下:
[Code:/home/zry/src/modules/Connect/OpConnectMD.cpp:415|UploadStatusData(...)]
准备
#define LOG_CHARCK_HOUR 0 // 每日创建日志的小时值(24h)
#define LOG_CHARCK_MINE 0 // 每日创建日志的分钟值(24h)
std::shared_ptr<spdlog::logger> my_logger = spdlog::daily_logger_mt(FileName, logFilePath, LOG_CHARCK_HOUR, LOG_CHARCK_MINE,true);
spdlog::set_default_logger(my_logger);
my_logger->flush_on(spdlog::level::info); // Flush logs immediately
my_logger->set_pattern("%^[%c|%x|ms:%e] [%l] [P:%P|t:%t] [Code:%@|%!(...)] - %v%$"); // Set log pattern
进过上述代码,可以创建一个每天零点零分切换的日志文件,路径是FileName字符串指定的内容,文件名称是logFilePath,创建的日志会自动在文件名后追加日期。
格式化
目标
我们的程序假设是一个包含多进程、多线程、多文件、函数众多,调用复杂的程序。
所以为了方便调试,我们期望能够知道 进程号 、 线程号、文件名称、行号、函数名称、等调试信息
又因为我们的程序是长时间运行的,可能是多时区运行的,所以我们最好知道 日期、时间、时区
另外我们还想知道程序性能和调用频率。
基于以上内容我们可以如下设计我们的调试信息。
my_logger->set_pattern("%^[%c|%x|ms:%e] [%l] [P:%P|t:%t] [Code:|%@|%s|%g|%#|%!|] - %v%$"); // Set log pattern
他的打印输出如下:
[Tue Apr 23 00:00:17 2024|04/23/24|ms:981] [info] [P:22588|t:22600] [Code:/home/zry/src/modules/Connect/OpConnectMD.cpp:397|UploadStatusData(...)] - UploadStatusData start
[Tue Apr 23 00:00:17 2024|04/23/24|ms:981] [debug] [P:22588|t:22600] [Code:/home/zry/src/modules/Connect/OpConnectMD.cpp:415|UploadStatusData(...)] - : device_type = YWIND00
[Tue Apr 23 00:00:17 2024|04/23/24|ms:981] [debug] [P:22588|t:22600] [Code:/home/zry/src/modules/Connect/OpConnectMD.cpp:416|UploadStatusData(...)] - : device_nid = N01
[Tue Apr 23 00:00:17 2024|04/23/24|ms:981] [debug] [P:22588|t:22600] [Code:/home/zry/src/modules/Connect/OpConnectMD.cpp:417|UploadStatusData(...)] - : data_period = 20240423%0200
[Tue Apr 23 00:00:17 2024|04/23/24|ms:981] [debug] [P:22588|t:22600] [Code:/home/zry/src/modules/Connect/OpConnectMD.cpp:418|UploadStatusData(...)] - : data_len = 200
[Tue Apr 23 00:00:17 2024|04/23/24|ms:981] [debug] [P:22588|t:22600] [Code:/home/zry/src/modules/Connect/OpConnectMD.cpp:419|UploadStatusData(...)] - : data_content = DATADICK,V202201,00000,YWIND00,N01,ST,20240423000000,z,1,rAA,0,rAB,0,qAA,0,qAB,0,qCA,0,qDA,0,qEB,0,tF,0,tFA,-93,tFB,0,tFC,0,wA,25.7,wAA,0,xA,DC,xE,12.2,xEA,0,xF,16,xFA,0,xG,/,xGA,4,vAA,1,vAB,1,1929,ED
[Tue Apr 23 00:00:17 2024|04/23/24|ms:981] [debug] [P:22588|t:22600] [Code:/home/zry/src/modules/Connect/OpConnectMD.cpp:420|UploadStatusData(...)] - : is_sup = false
介绍
本节分为 四个小节,
- 从使用者直接使用的
set_pattern()
函数入手, - 先介绍使用者最关心的格式化函数,
- 再介绍上一层级的
pattern_formatter
类, - 再回到直接使用的
set_pattern()
函数进行详细介绍,
目的是:可以完整的带读者从调用开始逐层的深入底层,再回归到使用,授人以鱼不如授人以渔。让读者彻底明白spdlog库是如何设定格式化输出的,从而可以有把握的DIY自己的格式化输出格式。
1. 使用者调用的set_pattern()
函数
spdlog 日志序列化基于 set_pattern()
函数,这个函数负责处理日志格式字符串中的各种标志。根据不同的标志,它会创建不同的格式化对象,并将这些对象添加到formatters_
向量中。这些格式化对象会在日志消息被记录时用于格式化消息。
例如,如果标志是'%'
,则函数会创建一个percent_formatter
对象;如果标志是'n'
,则函数会创建一个logger_name_formatter
对象;如果标志是'd'
,则函数会创建一个date_formatter
对象等等。这些格式化对象都是formatter
类的派生类,它们实现了format
函数,该函数用于将日志消息的特定部分格式化为字符串。
此外,函数还处理一些其他标志,例如'+'
,它会设置默认的格式化器;'-'
,它会取消设置默认的格式化器;'#'
,它会在日志消息中包含源代码文件的行号;'&'
,它会在日志消息中包含当前运行的线程ID;'@'
,它会在日志消息中包含源代码文件的名称。
总之,这个函数负责根据日志格式字符串中的标志创建适当的格式化对象,并将这些对象添加到formatters_
向量中,以便在记录日志消息时使用。
2. 格式化日志消息的pattern_formatter
类的成员函数handle_flag_
这段代码是SPDLOG日志库的源代码的一部分,具体是格式化日志消息的pattern_formatter
类的成员函数handle_flag_
。
具体的源码如下:路径为:usr->local->include->spdlog->pattern_formatter-inl.h->{}spdlog->handle_flag_(char,detailspadding info)
template<typename Padder>
SPDLOG_INLINE void pattern_formatter::handle_flag_(char flag, details::padding_info padding)
{
// process custom flags
auto it = custom_handlers_.find(flag);
if (it != custom_handlers_.end())
{
auto custom_handler = it->second->clone();
custom_handler->set_padding_info(padding);
formatters_.push_back(std::move(custom_handler));
return;
}
// process built-in flags
switch (flag)
{
case ('+'): // default formatter
formatters_.push_back(details::make_unique<details::full_formatter>(padding));
need_localtime_ = true;
break;
case 'n': // logger name
formatters_.push_back(details::make_unique<details::name_formatter<Padder>>(padding));
break;
case 'l': // level
formatters_.push_back(details::make_unique<details::level_formatter<Padder>>(padding));
break;
case 'L': // short level
formatters_.push_back(details::make_unique<details::short_level_formatter<Padder>>(padding));
break;
case ('t'): // thread id
formatters_.push_back(details::make_unique<details::t_formatter<Padder>>(padding));
break;
case ('v'): // the message text
formatters_.push_back(details::make_unique<details::v_formatter<Padder>>(padding));
break;
case ('a'): // weekday
formatters_.push_back(details::make_unique<details::a_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('A'): // short weekday
formatters_.push_back(details::make_unique<details::A_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('b'):
case ('h'): // month
formatters_.push_back(details::make_unique<details::b_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('B'): // short month
formatters_.push_back(details::make_unique<details::B_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('c'): // datetime
formatters_.push_back(details::make_unique<details::c_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('C'): // year 2 digits
formatters_.push_back(details::make_unique<details::C_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('Y'): // year 4 digits
formatters_.push_back(details::make_unique<details::Y_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('D'):
case ('x'): // datetime MM/DD/YY
formatters_.push_back(details::make_unique<details::D_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('m'): // month 1-12
formatters_.push_back(details::make_unique<details::m_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('d'): // day of month 1-31
formatters_.push_back(details::make_unique<details::d_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('H'): // hours 24
formatters_.push_back(details::make_unique<details::H_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('I'): // hours 12
formatters_.push_back(details::make_unique<details::I_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('M'): // minutes
formatters_.push_back(details::make_unique<details::M_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('S'): // seconds
formatters_.push_back(details::make_unique<details::S_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('e'): // milliseconds
formatters_.push_back(details::make_unique<details::e_formatter<Padder>>(padding));
break;
case ('f'): // microseconds
formatters_.push_back(details::make_unique<details::f_formatter<Padder>>(padding));
break;
case ('F'): // nanoseconds
formatters_.push_back(details::make_unique<details::F_formatter<Padder>>(padding));
break;
case ('E'): // seconds since epoch
formatters_.push_back(details::make_unique<details::E_formatter<Padder>>(padding));
break;
case ('p'): // am/pm
formatters_.push_back(details::make_unique<details::p_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('r'): // 12 hour clock 02:55:02 pm
formatters_.push_back(details::make_unique<details::r_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('R'): // 24-hour HH:MM time
formatters_.push_back(details::make_unique<details::R_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('T'):
case ('X'): // ISO 8601 time format (HH:MM:SS)
formatters_.push_back(details::make_unique<details::T_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('z'): // timezone
formatters_.push_back(details::make_unique<details::z_formatter<Padder>>(padding));
need_localtime_ = true;
break;
case ('P'): // pid
formatters_.push_back(details::make_unique<details::pid_formatter<Padder>>(padding));
break;
case ('^'): // color range start
formatters_.push_back(details::make_unique<details::color_start_formatter>(padding));
break;
case ('$'): // color range end
formatters_.push_back(details::make_unique<details::color_stop_formatter>(padding));
break;
case ('@'): // source location (filename:filenumber)
formatters_.push_back(details::make_unique<details::source_location_formatter<Padder>>(padding));
break;
case ('s'): // short source filename - without directory name
formatters_.push_back(details::make_unique<details::short_filename_formatter<Padder>>(padding));
break;
case ('g'): // full source filename
formatters_.push_back(details::make_unique<details::source_filename_formatter<Padder>>(padding));
break;
case ('#'): // source line number
formatters_.push_back(details::make_unique<details::source_linenum_formatter<Padder>>(padding));
break;
case ('!'): // source funcname
formatters_.push_back(details::make_unique<details::source_funcname_formatter<Padder>>(padding));
break;
case ('%'): // % char
formatters_.push_back(details::make_unique<details::ch_formatter>('%'));
break;
case ('u'): // elapsed time since last log message in nanos
formatters_.push_back(details::make_unique<details::elapsed_formatter<Padder, std::chrono::nanoseconds>>(padding));
break;
case ('i'): // elapsed time since last log message in micros
formatters_.push_back(details::make_unique<details::elapsed_formatter<Padder, std::chrono::microseconds>>(padding));
break;
case ('o'): // elapsed time since last log message in millis
formatters_.push_back(details::make_unique<details::elapsed_formatter<Padder, std::chrono::milliseconds>>(padding));
break;
case ('O'): // elapsed time since last log message in seconds
formatters_.push_back(details::make_unique<details::elapsed_formatter<Padder, std::chrono::seconds>>(padding));
break;
default: // Unknown flag appears as is
auto unknown_flag = details::make_unique<details::aggregate_formatter>();
if (!padding.truncate_)
{
unknown_flag->add_ch('%');
unknown_flag->add_ch(flag);
formatters_.push_back((std::move(unknown_flag)));
}
// fix issue #1617 (prev char was '!' and should have been treated as funcname flag instead of truncating flag)
// spdlog::set_pattern("[%10!] %v") => "[ main] some message"
// spdlog::set_pattern("[%3!!] %v") => "[mai] some message"
else
{
padding.truncate_ = false;
formatters_.push_back(details::make_unique<details::source_funcname_formatter<Padder>>(padding));
unknown_flag->add_ch(flag);
formatters_.push_back((std::move(unknown_flag)));
}
break;
}
}
这段代码是SPDLOG日志库的源代码的一部分,具体是格式化日志消息的pattern_formatter
类的成员函数handle_flag_
。
这个函数负责处理日志格式字符串中的各种标志。根据不同的标志,它会创建不同的格式化对象,并将这些对象添加到formatters_
向量中。这些格式化对象会在日志消息被记录时用于格式化消息。
例如,如果标志是'%'
,则函数会创建一个percent_formatter
对象;如果标志是'n'
,则函数会创建一个logger_name_formatter
对象;如果标志是'd'
,则函数会创建一个date_formatter
对象等等。这些格式化对象都是formatter
类的派生类,它们实现了format
函数,该函数用于将日志消息的特定部分格式化为字符串。
此外,函数还处理一些其他标志,例如'+'
,它会设置默认的格式化器;'-'
,它会取消设置默认的格式化器;'#'
,它会在日志消息中包含源代码文件的行号;'&'
,它会在日志消息中包含当前运行的线程ID;'@'
,它会在日志消息中包含源代码文件的名称。
总之,这个函数负责根据日志格式字符串中的标志创建适当的格式化对象,并将这些对象添加到formatters_
向量中,以便在记录日志消息时使用。
3. 格式化函数所属类pattern_formatter
类的定义
而在 同级目录下的头文件定义中有 pattern_formatter
的定义,路径为:/usr/local/include/spdlog/pattern_formatter.h
具体源码如下:
class SPDLOG_API pattern_formatter final : public formatter
{
public:
using custom_flags = std::unordered_map<char, std::unique_ptr<custom_flag_formatter>>;
explicit pattern_formatter(std::string pattern, pattern_time_type time_type = pattern_time_type::local,
std::string eol = spdlog::details::os::default_eol, custom_flags custom_user_flags = custom_flags());
// use default pattern is not given
explicit pattern_formatter(pattern_time_type time_type = pattern_time_type::local, std::string eol = spdlog::details::os::default_eol);
pattern_formatter(const pattern_formatter &other) = delete;
pattern_formatter &operator=(const pattern_formatter &other) = delete;
std::unique_ptr<formatter> clone() const override;
void format(const details::log_msg &msg, memory_buf_t &dest) override;
template<typename T, typename... Args>
pattern_formatter &add_flag(char flag, Args &&...args)
{
custom_handlers_[flag] = details::make_unique<T>(std::forward<Args>(args)...);
return *this;
}
void set_pattern(std::string pattern);
void need_localtime(bool need = true);
private:
std::string pattern_;
std::string eol_;
pattern_time_type pattern_time_type_;
bool need_localtime_;
std::tm cached_tm_;
std::chrono::seconds last_log_secs_;
std::vector<std::unique_ptr<details::flag_formatter>> formatters_;
custom_flags custom_handlers_;
std::tm get_time_(const details::log_msg &msg);
template<typename Padder>
void handle_flag_(char flag, details::padding_info padding);
// Extract given pad spec (e.g. %8X)
// Advance the given it pass the end of the padding spec found (if any)
// Return padding.
static details::padding_info handle_padspec_(std::string::const_iterator &it, std::string::const_iterator end);
void compile_pattern_(const std::string &pattern);
};
而这个类对象被作为智能指针所指向的对象在 我们调用 set_pattern
函数时,被创建。
4. 再回到 set_pattern
函数的详细说明
具体的源码如下:路径为:/usr/local/include/spdlog/logger-inl.h
源码为:
SPDLOG_INLINE void logger::set_pattern(std::string pattern, pattern_time_type time_type)
{
auto new_formatter = details::make_unique<pattern_formatter>(std::move(pattern), time_type);
set_formatter(std::move(new_formatter));
}
这段代码定义了一个名为 set_pattern
的成员函数,用于设置日志记录器的格式化模式。函数接受两个参数:一个字符串 pattern
和一个枚举类型 pattern_time_type
。
auto new_formatter = details::make_unique<pattern_formatter>(std::move(pattern), time_type);
这行代码创建了一个名为new_formatter
的智能指针,指向一个新创建的pattern_formatter
对象。pattern_formatter
是spdlog
中用于格式化日志信息的类。std::move(pattern)
用于将pattern
字符串移动到新创建的pattern_formatter
对象中,time_type
参数指定了时间格式的类型。set_formatter(std::move(new_formatter));
这行代码调用了set_formatter
成员函数,将新创建的格式化器new_formatter
设置为当前日志记录器的格式化器。
总的来说,set_pattern
函数用于设置日志记录器的格式化模式,可以通过传入一个特定的模式字符串和一个时间格式来定制日志输出的格式。
补充
另外,也附赠 创建日志的源码路径,有兴趣的朋友可以看一下都有哪些创建特定日志风格的方式。
具体路径为:/usr/local/include/spdlog/sinks/daily_file_sink.h
其中我使用的可以定点日切的源码为:
template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> daily_logger_format_mt(const std::string &logger_name, const filename_t &filename, int hour = 0,
int minute = 0, bool truncate = false, uint16_t max_files = 0, const file_event_handlers &event_handlers = {})
{
return Factory::template create<sinks::daily_file_format_sink_mt>(
logger_name, filename, hour, minute, truncate, max_files, event_handlers);
}
而我们整个程序的生命周期中操作的日志对象,其实是一个名为logger
的类对象,它的定义路径为/usr/local/include/spdlog/logger.h
源码由于太多,这里不放了,有兴趣的同学可以收藏,在空闲时间自己查找路径查看源码。
这里只列出对logger
类的解释,解释如下:
这段代码是C++实现的SPDLOG日志类。SPDLOG是一个快速、头文件只读、C++日志库,旨在易于使用、高效,同时仍然提供必要的灵活性来满足各种应用程序的需求。
logger
类是SPDLOG库的核心组件。它提供了在不同严重级别记录消息的方法(例如,跟踪、调试、信息、警告、错误和严重),并支持将日志记录到多个接收器(例如,控制台、文件和网络)。
logger
类有几种构造函数,包括:
explicit logger(std::string name)
: 使用指定的名称和空接收器列表构造一个日志记录器。logger(std::string name, sink_ptr single_sink)
: 使用指定的名称和一个接收器构造一个日志记录器。logger(std::string name, sinks_init_list sinks)
: 使用指定的名称和初始接收器列表构造一个日志记录器。
logger
类还有几种用于记录消息的方法,包括:
log(level::level_enum lvl, const std::string& msg)
: 在指定的严重级别记录一条消息。log(level::level_enum lvl, const char* msg)
: 在指定的严重级别记录一条消息。log(level::level_enum lvl, const wchar_t* msg)
: 在指定的严重级别记录一条消息。log(level::level_enum lvl, const void* msg)
: 在指定的严重级别记录一条消息。log(level::level_enum lvl, std::string&& msg)
: 在指定的严重级别记录一条消息。log(level::level_enum lvl, const std::string& fmt, ...)
: 在指定的严重级别记录一条格式化的消息。log(level::level_enum lvl, const char* fmt, ...)
: 在指定的严重级别记录一条格式化的消息。log(level::level_enum lvl, const wchar_t* fmt, ...)
: 在指定的严重级别记录一条格式化的消息。
logger
类还有设置记录级别、启用和禁用回溯记录、将缓冲记录刷新到接收器的方法。
spdlog日志格式化可以使用符号查找表
标志 | 描述 |
---|---|
+ | 默认格式化器 |
n | 记录器名称 |
l | 级别 |
L | 短级别 |
t | 线程ID |
v | 消息 |
a | 星期 |
A | 短星期 |
b | 月份 |
B | 短月份 |
c | 日期 |
C | 年份(2位数字) |
Y | 年份(4位数字) |
D | 日期(MM/DD/YY) |
d | 月份中的天数 |
H | 小时(24小时制) |
I | 小时(12小时制) |
M | 分钟 |
S | 秒 |
e | 毫秒 |
f | 微秒 |
F | 纳秒 |
p | 上午/下午 |
r | 时间(12小时制) |
R | 时间(24小时制) |
T | ISO 8601时间 |
z | 时区 |
P | 进程ID |
^ | 颜色范围开始 |
$ | 颜色范围结束 |
@ | 源位置 |
s | 短源文件名 |
g | 完整源文件名 |
# | 源行号 |
! | 源函数名 |
% | 转义字符 |
u | 上次记录消息以来的经过时间(纳秒) |
i | 上次记录消息以来的经过时间(微秒) |
o | 上次记录消息以来的经过时间(毫秒) |
O | 上次记录消息以来的经过时间(秒) |
后记
这里我故意留了一个小尾巴,考虑到篇幅长度,这里只对源码做解释,后续我会更新一篇实操代码中遇到的问题记录,并在其中附赠一份可用的源码框架。
分享一个有趣的 学习链接:https://xxetb.xet.tech/s/HY8za