项目日记 -日志系统 -搭建基础框架

发布于:2025-09-10 ⋅ 阅读:(19) ⋅ 点赞:(0)

博客主页:【夜泉_ly
本文专栏:【日志系统
欢迎点赞👍收藏⭐关注❤️

在这里插入图片描述
代码仓库:日志系统

前言

在上一篇中,
我们明确了项目目标,
完成了模块的设计,
并画出了项目的结构图。

从项目结构中可以发现,
这个项目具有复杂的继承体系,
并且使用了多种设计模式。

如果直接按照模块一个类一个类的实现,
很有可能会在项目中后期出现混乱,
比如由于接口设计不当导致实现与设计不符。

确定核心功能

橙色:核心模块,需要完整实现,
绿色:基础框架,需要部分实现,
灰色:这次不用管。
在这里插入图片描述

基础框架的搭建

数据

LogLevel

提供 debug, info, warning, error, fatal 五个日志级别

#ifndef __LEVEL_HPP__
#define __LEVEL_HPP__

namespace ly
{
    class LogLevel
    {
    public:
        enum class Level
        {
            debug = 0,
            info,
            warning,
            error,
            fatal
        };

        static const char* levelToString(Level level)
        {
            if(level == Level::debug) return "debug";
            if(level == Level::info) return "info";
            if(level == Level::warning) return "warning";
            if(level == Level::error) return "error";
            if(level == Level::fatal) return "fatal";
            return "unknown";
        }
    };
} // namespace ly

#endif // #define __LEVEL_HPP__

LogMessage

需存储:

  • 日志级别
  • 文件名
  • 文件行号
  • 消息内容
  • 消息创建时间
  • 线程id
  • 日志器名

LogMessage 做为项目中最重要且最基础的数据结构,
只应该存储纯数据,
与上层的类不应该有任何的关系。
所以在设计接口时不能图方便直接传 Logger/Sink 等类。

我最开始想的是消息类存个 Sink::ptr 多好,
自己就能把自己落地了,
后来发现 logger.hpp 包含了 message.hpp,
于是就更正了这个错误的想法。

#ifndef __MESSAGE_HPP__
#define __MESSAGE_HPP__

#include "level.hpp"
#include <string>
#include <vector>
#include <thread>
#include <ctime>

namespace ly
{
    struct LogMessage
    {
        using Level = LogLevel::Level;
        LogMessage() = default;
        // 等级, 行号, 文件名, 信息, 日志器名
        LogMessage(Level level, size_t line, const std::string &file, const std::string &message, const std::string &logger_name)
            : _level(level), _tid(std::this_thread::get_id()), _line(line), _file(file), _message(message), _logger_name(logger_name)
        {
            time_t t = time(0);
            localtime_r(&t, &_time);
        }
        LogLevel::Level _level;
        std::thread::id _tid;
        tm _time;
        size_t _line;
        std::string _file;
        std::string _message;
        std::string _logger_name;
    };
} // namespace ly

#endif // #define __MESSAGE_HPP__

线程 id 通过 std::this_thread::get_id() 获取,
时间通过 localtime_r 获取,
这两个不需要通过参数传递。

而其他信息,如行号,文件名必须由外部传入。

日志输出模块

日志输出模块作为一个独立的模块,
只需要负责将数据 按指定方式 输出到目标位置即可。
不应该知道项目中有哪些类,
不需要包含项目的其他头文件。

Sink

作为基类,规范了这个模块的输出接口,
也让用户知道如何扩展日志的输出方式。

class Sink
{
public:
    using ptr = std::shared_ptr<Sink>;
    virtual void log(const char* str, size_t len) = 0;
    virtual ~Sink() = default;
};

StdoutSink

标准输出作为最简单的输出方式,
可以用来测试项目是否能跑。

class StdoutSink : public Sink
{
public:
    virtual void log(const char* str, size_t len) override
    {
    	fwrite(str, sizeof(char), len, stdout);
    }
};

SinkFactory

为了适配不同 sink 的构造,
工厂需要使用可变参数模板,
其第一个参数用于确认 sink 的类型。

class SinkFactory
{
public:
    template<typename T, typename ...Args>
    static Sink::ptr create(Args &&...args)
    {
        return std::make_shared<T>(std::forward<Args>(args)...);
    }
};

如果单独使用工厂,可能有点麻烦:

auto sink = SinkFactory::create<SizeRollSink>(1024 * 1024 * 1024);

对比直接用 make_shared:

auto sink = std::make_shared<SizeRollSink>(1024 * 1024 * 1024);

但是,在实际使用中 sink 并不需要像这样直接创建,
而是以日志器的建造者创建:

auto logger = ly::LocalBuilder()
	.buildName("test")
	.buildSink<ly::StdoutSink>()
	.build();

如果去掉工厂,就会变成。。。

额。。
废物设计出现了!!
突然发现,如果建造者用模板方案,那工厂毫无价值啊:

// 有工厂:
template<typename SinkType, typename ...Args>
Builder &buildSink(Args &&...args) { 
    auto sink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
    _sinks.push_back(sink);
    return static_cast<T &>(*this);
}
// 无工厂:
template<typename SinkType, typename ...Args>
Builder &buildSink(Args &&...args) { 
    auto sink = std::make_shared<SinkType>(std::forward<Args>(args)...);
    _sinks.push_back(sink);
    return static_cast<T &>(*this);
}

那么暂时废除 SinkFactory 吧,
我们不能为了使用设计模式而使用设计模式。

SinkFactory扩展

现在这是个有用的工厂了,
毕竟能运行时动态创建的工厂才算真正的工厂:

static Sink::ptr create(const std::string& type, const std::unordered_map<std::string, std::string>& config)
{
    if (type == "stdout")   return std::make_shared<StdoutSink>();
    if (type == "file")     return std::make_shared<FileSink>(config.at("filename"));
    if (type == "sizeroll") return std::make_shared<SizeRollSink>(config.at("filename"), std::stoi(config.at("limit_size")));
    if (type == "timeroll") return std::make_shared<TimeRollSink>(config.at("filename"), std::stoi(config.at("limit_time")));
    return nullptr;
}

由于用了哈希,
甚至还可以添加 json 格式的配置文件,
为复杂的 Sink 提供配置信息。

格式化模块

用于测试的格式化选项:

%s - 源文件名
%# - 行号
%v - 实际的日志消息
%% - '%'

FormatItem

格式化子项基类
每个格式化选项对应一个格式化子项派生类,
然后在格式化器中存储格式化子项指针数组,
格式化的过程就是遍历这个子项数组,
然后每个子项从 LogMessage 中提取出对应的信息,
最后组成一个完整的消息。

因此格式化子项基类提供一个纯虚函数 format,
作用是把 message 里的信息拼到 std::stringstream 里去。

class FormatItem
{
public:
    using ptr = std::shared_ptr<FormatItem>;
    virtual ~FormatItem() = default;
    virtual void format(std::stringstream &out, const LogMessage &message) = 0;
};

FileNameFormatItem

%s - 源文件名

class FileNameFormatItem : public FormatItem
{
public:
    virtual void format(std::stringstream &out, const LogMessage &message) override { out << message._file; }
};

LineFormatItem

%# - 行号

class LineFormatItem : public FormatItem
{
public:
    virtual void format(std::stringstream &out, const LogMessage &message) override { out << message._line; }
};

MessageFormatItem

%v - 实际的日志消息

class MessageFormatItem : public FormatItem
{
public:
    virtual void format(std::stringstream &out, const LogMessage &message) override { out << message._message; }
};

OtherFormatItem

解析不出来的信息直接按原样输出

class OtherFormatItem : public FormatItem
{
public:
    OtherFormatItem(const std::string &str = "") : other_message(str) {}
    virtual void format(std::stringstream &out, const LogMessage &message) override { out << other_message; }

private:
    std::string other_message;
};

Formatter

格式化器,
储存:

  • 格式化字符串
  • 格式化子项指针数组
  • 格式化子项创建者

前两个不必多说,而第三个:
static std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> _creaters;
嘿嘿嘿,虽然类型很抽象,但特别好用。
只要看看初始化就明白了:

std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> Formatter::_creaters{
    {'v', [](const std::string &) -> FormatItem::ptr
        { return std::make_shared<MessageFormatItem>(); }},
    {'s', [](const std::string &) -> FormatItem::ptr
        { return std::make_shared<FileNameFormatItem>(); }},
    {'#', [](const std::string &) -> FormatItem::ptr
        { return std::make_shared<LineFormatItem>(); }},
    {'O', [](const std::string &s) -> FormatItem::ptr
        { return std::make_shared<OtherFormatItem>(s); }},
};

之后添加其他格式化子项时,
只要改改这里就行。

下面是格式化器的实现:

// 格式化器
class Formatter
{
public:
    using ptr = std::shared_ptr<Formatter>;

public:
    Formatter(const std::string &pattern = "[%s:%#] %v") { analysis(pattern); }
    // 消息对象 -> 格式化后的消息字符串
    std::string format(const LogMessage &message)
    {
        std::stringstream ss;
        for (auto &e : _formatter_items)
            e->format(ss, message);
        return ss.str();
    }

private:
    void analysis(const std::string &pattern)
    {
        std::string other_message;
        for(int i = 0, size = pattern.size(); i < size; ++i)
        {
            char c = pattern[i];
            if(c != '%' || i + 1 == size || _creaters.count(pattern[i + 1]) == 0)
                other_message.append(1, c);
            else
            {
                c = pattern[++i]; // 将 i 更新至格式化选项处, 将 c 更新为格式化选项
                if(other_message.size()) _formatter_items.push_back(_creaters['O'](other_message));
                other_message.clear();
                _formatter_items.push_back(_creaters[c](""));
            }
        }
    }
    
private:
    std::string _pattern;                          // 格式化字符串
    std::vector<FormatItem::ptr> _formatter_items; // 解析_pattern得到_formatter_items
    static std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> _creaters;
};

日志器

Logger

由于目前不涉及多线程,
因此相关的成员暂时不添加。

目前我们留三个最主要的成员:

  • 日志名 std::string _name;
  • 格式化器 Formatter::ptr _formatter;
  • 输出方式 std::shared_ptr<std::vector<Sink::ptr>> _sinks;
class Logger
{
public:
    using ptr = std::shared_ptr<Logger>;
    enum class Type
    {
        sync,
        async
    };

public:
    Logger(const std::string &name, Formatter::ptr formater, std::shared_ptr<std::vector<Sink::ptr>> sinks)
        : _name(name), _formatter(formater), _sinks(sinks) {}
    std::shared_ptr<std::vector<Sink::ptr>> sinks() const { return _sinks; }
    const std::string &name() const { return _name; }
    void debug(const char *file, size_t line, const char *fmt, ...)
    {
        va_list ap;
        va_start(ap, fmt);
        log(LogLevel::Level::debug, file, line, fmt, ap);
        va_end(ap);
    }
    virtual ~Logger() = default;

protected:
    virtual void log(LogLevel::Level level, const char *file, size_t line, const char *fmt, va_list ap) = 0;

protected:
    std::string _name;
    Formatter::ptr _formatter;
    std::shared_ptr<std::vector<Sink::ptr>> _sinks;
};

核心功能就是 log 函数,
由 debug, info, warning, error, fatel函数调用,
但同步日志器和异步日志器的输出方式不同,
因此这里声明为纯虚函数。

SyncLogger

class SyncLogger : public Logger
{
public:
    SyncLogger(const std::string &name, Formatter::ptr formater, std::shared_ptr<std::vector<Sink::ptr>> sink) : Logger(name, formater, sink) {}

protected:
    virtual void log(LogLevel::Level level, const char *file, size_t line, const char *fmt, va_list ap) override
    {
        char* str;
        if(-1 == vasprintf(&str, fmt, ap))
        {
            free(str);
            return;
        }
        LogMessage log_message(level, line, file, str, _name);
        std::string message = _formatter->format(log_message);
        for (auto &e : *_sinks)
            e->log(message.c_str(), message.size());
    }
};

这个项目的核心逻辑就是这一段了:
logger 拿到用户的输入
-> 通过 vasprintf 转换为字符串
-> 构建 LogMessage 对象
-> 用格式化器格式化 LogMessage 对象,返回格式化后的字符串
-> 用日志落地器将消息落地

这就是为什么我画的结构图长这样:

在这里插入图片描述

日志器建造者

目前只是基础框架搭建阶段,
Logger 的构造就有三个参数了,
之后随着项目的完善还会增多。
因此建造者是必须的。

Builder

为了能够链式调用,
这里使用奇异递归模板模式。

template <typename T>
class Builder
{
public:
    Builder &buildName(const std::string &name)
    {
        _name = name;
        return static_cast<T &>(*this);
    }
    Builder &buildType(Logger::Type type)
    {
        _type = type;
        return static_cast<T &>(*this);
    }
    template<typename SinkType, typename ...Args>
    Builder &buildSink(Args &&...args) { 
        auto sink = std::make_shared<SinkType>(std::forward<Args>(args)...);
        _sinks.push_back(sink);
        return static_cast<T &>(*this);
    }
    Builder &buildFormatPattern(const std::string& format_pattern)
    {
        _format_pattern = format_pattern;
        return static_cast<T &>(*this);
    }
    virtual Logger::ptr build() = 0;
    virtual ~Builder() = default;

protected:
    std::string _name;
    std::string _format_pattern;
    Logger::Type _type = Logger::Type::sync;
    std::vector<Sink::ptr> _sinks;
};

LocalBuilder

class LocalBuilder : public Builder<LocalBuilder>
{
public:
    virtual Logger::ptr build()
    {
        assert(_name.size());
        
        Formatter::ptr formater;
        if(_format_pattern.size()) formater = std::make_shared<Formatter>(_format_pattern);
        else formater = std::make_shared<Formatter>();

        std::shared_ptr<std::vector<Sink::ptr>> sinks = std::make_shared<std::vector<Sink::ptr>>(_sinks);

        if (Logger::Type::sync == _type)
            return std::make_shared<SyncLogger>(_name, formater, sinks);
        else
            return std::make_shared<AsyncLogger>(_name, formater, sinks);
    }
    virtual ~LocalBuilder() override {}
};

功能测试

为了让我们不用手动传 __FILE__ 和 __LINE__,
我们还得再加一句:

#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)

然后就能开始测试了:

#include "builder.hpp"

int main()
{
    auto logger = ly::LocalBuilder()
        .buildName("test_logger")
        .buildFormatPattern("[filename : %s] [line : %#] message : %v")
        .buildSink<ly::StdoutSink>()
        .build();
    logger->debug("%s\n", "基础框架功能测试");
    return 0;
}

在这里插入图片描述

在这里插入图片描述


希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!


网站公告

今日签到

点亮在社区的每一天
去签到