利用RAII机制进行日志的采集

引言

对于日志器的组装我们已经完成了一大半了,接下来就是一些比较细节的实现。我们先回顾一下子之前的组成部分,首先是我们自定义了一个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));
}

总结

本篇文章到这里就结束了!!!希望可以帮助大家理解~~~

相关推荐
0+1111 小时前
Linux --线程概念与控制
linux·运维·服务器·jvm
nLif1 小时前
基于GTK的简易MFC对话框兼容层 -- MfcToGTK
linux·c++·mfc·gtk
fengyehongWorld2 小时前
Linux 安装frp实现内网穿透
linux·运维·服务器
一_个前端2 小时前
[TailScale]-打通你的局域网服务器到公网电脑
linux
DO_Community3 小时前
Omarchy 将研发基础设施迁移至 DigitalOcean 云平台
linux·人工智能·llm·agent·omarchy
米糕闯编程3 小时前
鱼香ros2(三)Linux操作总结
linux·机器人·ros2
一技安身3 小时前
【信创】银河麒麟V10服务器使用安装光盘Packages自建本地yum源
linux·运维·服务器
小小、码农3 小时前
C++11右值引用与移动语义 —— 从拷贝资源到转移资源
开发语言·网络·c++·网络协议
码农客栈4 小时前
Linux I2C驱动实验
linux·运维·驱动开发