引言
对于日志器的组装我们已经完成了一大半了,接下来就是一些比较细节的实现。我们先回顾一下子之前的组成部分,首先是我们自定义了一个buffer,这个buffer是存储我们输出的日志的,由inline和overflow组成,当日志的内容比较小的时候,我们会把日志输出到inline里面,这个inline在栈上,也就是array<char, 128>,而当日志比较多的时候,我们才在堆上开辟内存,vector。
formatter,我们的日志输出不仅仅由message,为了更好的定位信息的位置,我们还会自己组装log_record,然后用格式串的方式传递给format,format再解析出对应的格式串的信息,最后和message一起打印到buffer里面
我们打印的地方不止有一处,有网络,有终端,有文件,为了让我们的日志可以定向的输出到特定的位置,我们有了appender,我们只需要调用我们提供的接口就可以输出到我们想要输出的位置
然后我们将上面的所有部分全部组在一起,也就是我们的logger。
而这一篇文章的目的是要捕捉系统里面的关键信息,先根据 format 把参数转换成文本,再把文本写入 LogLine 管理的日志,最后把这些数据放到appender里面,打印到特定的地方。
工具类的解释
我们有时候为了防止一个类里面的变量和另一个类冲突的时候,我们可以使用namespace{},在这个作用域里面的变量只会被该文件里面的代码所访问,而且还可以优化编译器的编译。
static的静态变量在第一次被赋值的时候就已经确定了,而之后的任何赋值编译器都不会执行。
cpp
std::chrono::steady_clock::duration GetElapsedTime() noexcept
{
static const auto START_TIME = std::chrono::steady_clock::now();
return std::chrono::steady_clock::now() - START_TIME;
}
LogLine.cc
我们的LogLine类里面有一个strucgt Impl,这个和appender和Logger有很大的区别,这两个类是有存放impl的指针。因为Logger和appender数量比较少,而且对象的生命周期也比较固定,而LogLine是每一条日志都会被使用,每一次使用完之后就会被析构。impl这个对象里面的所有数据都会被放在我们自定义的数组里面,如果使用指针的话,那么我们自定义在栈上的数组就没有任何意义了,因为指针的对象会被放在堆上。
第二点就是我们存放Impl的数组里面是存放的std::byte,目的是强调里面只存放数据,不进行任何的逻辑运算。而正是因为如此,我们需要内存对齐的操作,对于编译器,读取8/16倍数的内存地址命中率更高,我们的Impl里面的数据都是要放到这个这个首地址处。之后我们调用Impl的构造函数,首地址对齐保证构造的合法性。
平时我们用int a\[\]这种数组并不需要内存对齐是因为int本身就自带内存对齐,而std::type这种并不会保证内存对齐
alignas(std::max_align_t) 规范的是数组的首地址(对齐要求决定起始地址,x86-64 上要求是 16 的倍数),对内部布局没有影响------元素仍是连续的 std::byte,每个 1 字节,不插入 padding。因为 Impl 是在缓冲首地址处 placement new 的,首地址对齐即保证构造合法。 因为 m_implStorage 是 LogLine 的成员,为了让它在对象内部落在 16 字节边界上,编译器往往还会顺带把 LogLine 整体的对齐也提到 16 字节------但这是「为了满足成员对齐」的连带结果,本质仍在规范那个首地址
我们整个类的构建思路是在LogLine析构的时候调用submit,打印到特定的位置,这也就符合我们的RAII机制。RAII (Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 管理资源的一种方式:构造对象时获取资源,析构对象时释放资源,我们这里在构造的时候得到系统信息,在析构的时候传输日志信息。
cpp
class PANS_API LogLine final
{
public:
LogLine(Logger& logger, LogLevel::Level level, u32 line, std::string_view file_name);
~LogLine() noexcept;
LogLine(const LogLine&) = delete;
LogLine& operator=(const LogLine&) = delete;
LogLine(LogLine&& ) = delete;
LogLine& operator=(LogLine&&) = delete;
[[nodiscard]] std::ostream& stream() noexcept;
private:
// 这个地方不使用指针是因为每行日志都要构造一个LogLine,构造频率很高
// 这个Impl最后是放在我们自定义的缓冲区里面的,如果都是指针的话,那么我们做栈的优化的努力就白费了
struct Impl;
static constexpr std::size_t LOG_LINE_IMPL_SIZE = 1024;
// 内存起始地址对其,原因就是我们使用的是byte,存的是Impl
alignas(std::max_align_t) std::byte implStorage_[LOG_LINE_IMPL_SIZE];
[[nodiscard]] Impl& getImpl() noexcept;
};
// LogPrintf("....");
PANS_API void LogPrintf(Logger& logger, LogLevel::Level level, u32 line, std::string_view file_name, const char* format, ...);
inlineBuffer是我们自定义的数组,而streamBuffer是我们自定义的streambuf,把inlineBuffer传递到streamBuffer里面我们就可以使用字节流进行输出了。
cpp
struct LogLine::Impl final
{
Impl(Logger& logger, LogLevel::Level level, u32 line, std::string_view filename)
: logger_(logger)
, level_(level)
, line_(line)
, filename_(filename)
, timestamp_(std::chrono::system_clock::now())
, elapsed_(GetElapsedTime())
, threadId_(GetThreadId())
, fiberId_(GetFiberId())
, threadName_(GetThreadName())
, streamBuffer_(inlineBuffer_)
, stream_(&streamBuffer_)
{}
[[nodiscard]] LogRecordView getRecord() const noexcept
{
return {
level_,
logger_.getName(),
inlineBuffer_.view(),
timestamp_,
elapsed_,
threadId_,
fiberId_,
threadName_,
filename_,
line_,
};
}
Logger& logger_;
LogLevel::Level level_;
u32 line_ = 0;
std::string_view filename_;
std::chrono::system_clock::time_point timestamp_;
std::chrono::steady_clock::duration elapsed_;
u64 threadId_ = 0;
u64 fiberId_ = 0;
std::string_view threadName_;
InlineBuffer<LOG_MESSAGE_INLINE_CAPACITY> inlineBuffer_;
SmallStreamBuffer<LOG_MESSAGE_INLINE_CAPACITY> streamBuffer_;
std::ostream stream_;
};
我们创建的结构体Impl直接放到我们已经开辟到的内存地址上,就是implStorage,如果不使用这个,那么我们就会重新开辟一个内存空间去存这个impl,并且我们还需要重新得到这个地址的头指针。
cpp
LogLine::LogLine(Logger& logger, LogLevel::Level level, u32 line, std::string_view file_name)
{
static_assert(sizeof(Impl) <= LOG_LINE_IMPL_SIZE, "LogLine inline implementation storage is too small");
static_assert(alignof(Impl) <= alignof(std::max_align_t), "LogLine implementation requires excessive alignment");
// 按照固定的内存地址创建对象,这里是在Impl的起始地址上创建implStorage,把struct Impl的数据全部放在这个数组里面
std::construct_at(reinterpret_cast<Impl*>(implStorage_), logger, level, line, file_name);
}
std::launder 是 C++17 提供的函数,常用于在一块存储中构造对象后,获取指向该对象的有效指针。 std::launder(...) 返回的是 Impl*,前面的 * 将指针解引用,得到 Impl 对象,从而返回它的引用,不会复制对象。
cpp
LogLine::Impl& LogLine::getImpl() noexcept
{
return *std::launder(reinterpret_cast<Impl*>(implStorage_));
}
这个析构函数就是在Logline析构的时候再把所有的数据submit到Logger里面,logger里面就会找到appenders
cpp
LogLine::~LogLine() noexcept
{
Impl& impl = getImpl();
LoggerAccess::Submit(impl.logger_, impl.getRecord());
std::destroy_at(&impl);
}
方便 字节流 << 输出
cpp
std::ostream& LogLine::stream() noexcept
{
return getImpl().stream_;
}
为了可以兼容LogPrintf("%d", age);这种打印输出方式,我们就需要接受可变参。思路就是我们还是构造LogLine对象,然后创建inline_buffer(栈上),如果大小不够就在堆上创建overflow。我们接受可变参需要用到va_list
va_list是接受format的,也就是%d,%s这种,不是接受...。
这个过程我们在x86体系架构里面一般是6个寄存器传递参数,
arguments_copy通常是用来再次读取同一组可变参数 的,因为va_list在被读取后,不能保证还能从头使用,因为va_list不是一个可以反复遍历的普通容器,更像是一个记录当前读取位置的游标。读取参数时,这个位置会向后移动。我们之所以需要va_list是因为寄存器里面的数据并不安全,容易被修改,所以我们把寄存器里面的数据保存到栈上,也就作为寄存器数据的一个副本。

vsnprintf这个就是把...里面接受的可变参数赋值到占位符上,也就是转变为我们可读的数据。
不过总的来说,这个函数内部调用的还是字节流,不过封装了一层接受可变参。
cpp
void LogPrintf(Logger &logger, LogLevel::Level level, u32 line, std::string_view file_name, const char *format, ...)
{
LogLine log_line(logger, level, line, file_name);
if (format == nullptr)
{
log_line.stream() << "<null-format>";
return;
}
std::array<char, PRINTF_FORMAT_INLINE_CAPACITY> inline_buffer;
va_list arguments;
va_list arguments_copy;
va_start(arguments, format);
va_copy(arguments_copy, arguments);
const int required_size = std::vsnprintf(inline_buffer.data(), inline_buffer.size(), format, arguments);
if (required_size < 0)
{
va_end(arguments_copy);
log_line.stream() << "<format-error>";
return;
}
const std::size_t message_size = static_cast<std::size_t>(required_size);
if (message_size < inline_buffer.size())
{
va_end(arguments_copy);
log_line.stream().write(inline_buffer.data(), static_cast<std::streamsize>(message_size));
return;
}
std::vector<char> overflow_buffer(message_size + 1);
const int second_result = std::vsnprintf(overflow_buffer.data(), overflow_buffer.size(), format, arguments_copy);
va_end(arguments_copy);
if (second_result < 0)
{
log_line.stream() << "<format-error>";
return;
}
log_line.stream().write(overflow_buffer.data(), static_cast<std::streamsize>(message_size));
}
总结
本篇文章到这里就结束了!!!希望可以帮助大家理解~~~