一款支持多日志器、多级别、多落地方式的同异步日志系统

发布于:2025-06-29 ⋅ 阅读:(18) ⋅ 点赞:(0)

简介

在现代软件开发与系统运维领域,日志系统扮演着不可或缺的关键角色。它如同软件系统的"黑匣子",忠实记录着程序运行过程中的每一个重要时刻,为开发者和运维人员提供了洞察系统内部运行状态的重要窗口。

img

项目特点

  • 多级别日志管理

系统支持DEBUG、INFO、WARN、ERROR、FATAL等多个日志级别,用户可根据不同环境和需求灵活配置输出级别,既保证了开发阶段的详细调试信息,又避免了生产环境的信息冗余。

  • 多日志器架构

采用模块化设计理念,支持同时创建和管理多个独立的日志器实例。不同模块或业务线可以拥有专属的日志器,实现日志的分类管理和独立配置,提高了系统的组织性和可维护性。

  • 多样化落地方式

系统提供了丰富的日志输出目标选择,包括控制台输出、本地文件存储、滚动文件等多种方式。用户可根据实际需求选择合适的落地策略,甚至可以同时启用多种输出方式。如果有特殊的应用场景还可以支持定制专属的落地方式。

  • 灵活的同步异步机制

针对不同的性能要求和应用场景,系统同时支持同步和异步两种输出模式。同步模式确保日志的实时性和一致性,适用于关键业务场景;异步模式则通过缓冲队列机制提升系统吞吐量,避免日志操作对主业务流程的性能影响。

  • 卓越的扩展性设计

系统采用插件化架构和标准化接口设计,为后续功能扩展预留了充足空间。无论是新增输出格式、扩展过滤规则,还是集成第三方日志分析工具,都能够以最小的代码改动实现。

项目实现

系统整体的设计可以分为三大步:基础功能模块实现、异步功能模块实现、核心功能模块实现。

基础功能模块实现

文件操作以及日期时间获取

因为写日志会频繁的涉及到日期时间以及对文件的写入操作,所以可以将这类的操作函数给封装出来,在后续使用时直接调用就可以了。

  • 日期时间类

Date 类中设计实现了两个函数,一个 now() 函数,用于获取当前的时间戳。另一个 dateTime() 函数,用于将对应时间戳转换为特定格式的日期时间字符串。下面是两个函数的实现:

class Date
{
public:
    static time_t now() 
    {
        return time(nullptr);
    }

    static std::string dateTime(const std::string &format = "%Y-%m-%d %H:%M:%S", time_t timestamp = now())
    {
        struct tm lt;
        localtime_r(&timestamp, &lt);
        char tmp[128];
        strftime(tmp, 127, format.c_str(), &lt);
        std::stringstream ss;
        ss << tmp;
        return ss.str();
    }
}; 

now() 函数调用 C 标准库函数 time(nullptr) 直接返回当前时间的时间戳。dateTime 函数接收两个参数,一个 format 表示日期时间的格式,一个 timastamp 表示需要转换的时间戳。

使用线程安全的 localtime_r() 函数将 time_t 时间戳转换为本地时间的 tm 结构体。tm 结构体包含年、月、日、时、分、秒等分解后的时间组件。

image-20250627200407427

使用 strftime() 函数根据指定格式将时间信息格式化为字符串。该函数支持丰富的格式化选项,如 %Y 表示四位年份,%m 表示月份等。

  • 文件操作类

文件操作类包含了检测文件是否存在的的 exist() 函数,对给出的文件绝对或相对路径名进行切分,返回文件路径,例如给出的是 ./LogSystem/code/a.log,这时候返回的就是 ./LogSystem/code/,还有一个用来创建目录的函数 createPath()

class File
{
public:
    // 检查文件是否存在
    static bool exist(const std::string &pathname)
    {
        struct stat st;
        return stat(pathname.c_str(), &st) == 0;
    }

    // 返回文件路径
    static std::string path(const std::string &pathname)
    {
        if(pathname.empty()) return ".";
        int pos = pathname.find_last_of("/\\");
        if(pos == std::string::npos) return ".";
        return pathname.substr(0, pos + 1);
    }

    // 创建目录 
    static bool createPath(const std::string &pathname)
    {
        if(pathname.empty() || exist(pathname)) return true;
        size_t idx = 0;
        while(idx < pathname.size())
        {
            size_t pos = pathname.find_first_of("/\\", idx);
            if(pos == std::string::npos) 
            {
                if(mkdir(pathname.c_str(), 0755))
                {
                    std::cerr << "mkdir failed for path: " << pathname << " errno: " << strerror(errno) << "\n";
                    return false;
                }
                return true;
            }
            std::string substr = pathname.substr(0, pos);
            if(idx == pos || substr == "." || substr == ".." || exist(substr)) 
            {
                idx = pos + 1;
                continue;
            }
            if(mkdir(substr.c_str(), 0755))
            {
                std::cerr << "mkdir failed for path: " << substr << " errno: " << strerror(errno) << "\n";
                return false;
            }
            idx = pos + 1;
        }
        return true;
    }
};

exist() 函数使用 stat(pathname.c_str(), &st) 系统调用尝试获取指定路径文件/目录的信息。利用这个函数的返回值来判断文件是否存在,如果返回值为0,说明文件或目录存在,否则不存在。

path() 函数使用 find_last_of("/\\") 在字符串中查找最后一个正斜杠或反斜杠的位置(兼容UNIX和Windows路径分隔符)。如果没有找到分隔符,说明 pathname 可能只是一个文件名,无路经,返回 "." 表示当前目录,否则,返回从开头到分隔符为止的字符串(包含分隔符)。

createPath() 函数在每次循环中,查找下一个分隔符的位置,获得处理的子路径。如果已经到达最后一级目录,尝试创建完整目录,成功返回 true,失败则输出错误信息并返回 false。

日志等级

对于日志的打印规划了不同的日志等级,在日志打印的时候可以根据需要打印出大于或等于该日志级别的日志信息,对于低于该等级的日志信息进行忽略。

class Level
{
public:
    enum class value
    {
        UNKNOW = 0,
        DEBUG,
        INFO,
        WARN,
        ERROR,
        FATAL,
        OFF
    };

    // 将错误级别转换为字符串
    static const char* toString(const Level::value val)
    {
        switch (val)
        {
        case Level::value::DEBUG:
            return "DEBUG";
        case Level::value::ERROR:
            return "ERROR";
        case Level::value::FATAL:
            return "FATAL";
        case Level::value::INFO:
            return "INFO";
        case Level::value::OFF:
            return "OFF";     
        case Level::value::WARN:
            return "WARN";
        default:
        return "UNKNOW";
        }
    }
};

在定义日志级别时,使用了 enum class,这是C++11引入的强类型枚举,防止与其他枚举名冲突,具有更严格的作用域。包含7个枚举值:

  • UNKNOW(未定义或未知级别,默认是0)
  • DEBUG(调试级)
  • INFO(信息级)
  • WARN(警告级)
  • ERROR(错误级)
  • FATAL(严重错误级,通常不可恢复)
  • OFF(日志关闭,不输出日志)

类中还包含一个静态成员函数 toString,用于获取对应日志级别的字符串流,输入参数是 Level::value 类型。

日志信息描述

项目中对每一条日志信息都是采用 struct Message来描述,代码如下:

/*
日志消息类用来描述一条日志消息 
*/
struct Message
{
    size_t _line;
    time_t _time;
    std::thread::id _tid;
    std::string _name; //日志器名称
    std::string _file;
    std::string _payload;
    Level::value _level;
    
    Message(const size_t line, const std::string &name, const std::string &file, 
        const std::string &payload, const Level::value level)
    : _line(line), _time(Util::Date::now()), _tid(std::this_thread::get_id()), 
      _name(name), _file(file), _payload(payload), _level(level)
    {

    }
};    

Message结构体包含了如下成员:

  • size_t _line:日志消息的代码行数,指日志调用发生的源代码所在行号。
  • time_t _time:日志产生的时间戳,用于记录日志生成的具体时间。
  • std::thread::id _tid:记录日志产生的线程ID,在多线程程序中用于追踪日志的线程来源。
  • std::string _name日志器名称,区分不同模块/不同功能的日志器。
  • std::string _file:日志发生时对应的源文件名
  • std::string _payload日志主体内容,即实际要记录的信息。
  • Level::value _level日志级别,比如INFO、WARN、ERROR等。

Message结构体的构造函数需要由调用者传入linenamefilepayloadlevel参数来初始化对应的成员变量,_time 通过Util::Date::now()获得当前时间,实现了日志时间自动采集。_tid 通过std::this_thread::get_id()获取当前线程ID,自动记录日志所在线程。这样,创建Message对象时即可完整记录下日志产生时的上下文信息。

异步功能模块实现

异步写日志的实现是采用了生产者-消费者模型,先将日志写入到内存缓冲区中,由后台线程自动将缓冲区中的日志信息落地到磁盘文件中,从而提高了日志的写入性能,避免了同步写日志等待写入磁盘的性能瓶颈。

由于常规的生产者-消费者模型会涉及到频繁的锁冲突,例如在多生产者和多消费者模型中,就有生产者和生产者的锁冲突,消费者和消费者的锁冲突以及生产者和消费者的锁冲突,频繁的锁冲突会造成性能的严重损失,所以常规的生产者-消费者模型满足不了项目的需求。

image-20250628114013810

在经过优化测试后,决定在项目中采用双缓冲区加多生产者-单消费者模式,双缓冲区减少了生产者和消费者之间的锁冲突,单消费者减少了消费者和消费者之间的锁冲突,项目中只需要注意生产者和生产者之间的锁冲突即可,单消费者在写磁盘的时候可以批量化的写入,避免了对于每一条或者几条日志信息产生磁盘IO。

image-20250628115231789

缓冲区实现

#define MAX_SIZE (1024 * 1024 * 10)
class Buffer
{
public:
    Buffer() : _buffer(MAX_SIZE), _readerIdx(0), _writeIdx(0)
    {}
    ~Buffer()
    {}

    bool push(const char *data, const size_t len)
    {
        if(writeAbleSize() < len) return false;
        std::copy(data, data + len, &_buffer[_writeIdx]);
        moveWriter(len);
        return true;
    }

    const char *readBegin() { return &_buffer[_readerIdx]; }
    size_t readAbleSize() { return _writeIdx - _readerIdx; }
    size_t writeAbleSize() { return _buffer.size() - _writeIdx; }

    void moveWriter(const size_t len)
    {
        assert((_writeIdx + len) < _buffer.size());
        _writeIdx += len;
    }

    void moveReader(const size_t len)
    {
        assert((_readerIdx + len) <= _writeIdx);
        _readerIdx += len;
    }

    void reset()
    {
        _readerIdx = 0;
        _writeIdx = 0;
    }

    void swap(Buffer &buffer)
    {
        _buffer.swap(buffer._buffer);
        std::swap(_readerIdx, buffer._readerIdx);
        std::swap(_writeIdx, buffer._writeIdx);
    }

    bool empty()
    {
        return _readerIdx == _writeIdx;
    }
private:
    std::vector<char> _buffer;
    size_t _readerIdx;
    size_t _writeIdx;
};

代码中定义了一个 Buffer 类,其中包含如下几个成员:

  • std::vector<char> _buffer:实际的数据存储区,初始化分配 MAX_SIZE 大小空间(10MB)。
  • size_t _readerIdx:读指针,指向下一个可读取数据的位置。
  • size_t _writeIdx:写指针,指向下一个可写入数据的位置。

成员函数主要包含:

  • bool push(const char *data, const size_t len):把外部数据追加到 buffer 里。
  • const char * readBegin(): 返回可读区域的起始指针。
  • size_t readAbleSize():返回可读区间的大小。
  • size_t writeAbleSize():返回写入区间的剩余空间。
  • void moveWriter(const size_t len):更新写指针。
  • void moveReader(const size_t len):更新读指针。
  • reset():读写指针都重置为0,清空 buffer 内容。
  • swap(Buffer &buffer):与另一个 Buffer 交换所有成员内容,包括内部 vector。
  • empty():判断 buffer 当前是否为空。

异步线程实现

using Function = std::function<void(Buffer&)>;
enum class AsyncType
{
    ASYNC_SAFE,
    ASYNC_UNSAFE
};

class AsyncLooper
{
public:
    using ptr = std::shared_ptr<AsyncLooper>;
    AsyncLooper(const Function &callback, AsyncType type = AsyncType::ASYNC_SAFE) 
        : _type(type), _callbackFunc(callback), _stop(false), _thread(&AsyncLooper::threadEntry, this)
    {}

    ~AsyncLooper() { stop(); }

    void stop()
    {
        _stop = true;
        _consumer_cond.notify_all();
        _thread.join();
    }

    void push(const std::string &data, const size_t len)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        if(_type == AsyncType::ASYNC_SAFE)
            _product_cond.wait(lock, [&](){ return _product_buf.writeAbleSize() >= len; });
        _product_buf.push(data.c_str(), len);
        _consumer_cond.notify_all();
    }

private:
    void threadEntry()
    {
        while (true)
        {
            {
                std::unique_lock<std::mutex> lock(_mtx);
                if(_stop && _product_buf.empty()) break;

                _consumer_cond.wait(lock, [&](){ return _stop || !_product_buf.empty(); });
                _consumer_buf.swap(_product_buf);
                _product_cond.notify_all();
            }
            _callbackFunc(_consumer_buf);
            _consumer_buf.reset();
        }
    }
private:
    Function _callbackFunc;

private:
    AsyncType _type;
    bool _stop;
    Buffer _product_buf;
    Buffer _consumer_buf;
    std::mutex _mtx;
    std::condition_variable _product_cond;
    std::condition_variable _consumer_cond;
    std::thread _thread;
};

异步写入日志的核心逻辑是采用互斥锁加条件变量,当有日志要写入内存缓冲区的时候获取锁,异步写入日志还定义了两种模式,线程安全模式和非线程安全模式:

  • ASYNC_SAFE:线程安全模式。
  • ASYNC_UNSAFE:非线程安全模式。

在线程安全模式下,需要等待生产者缓冲区的剩余可写入的大小满足当前写入的日志长度,只有在这个条件变量满足时才进行日志的写入,否则挂起。当日志写入完成后唤醒消费者线程。

消费者线程的核心逻辑是 threadEntry() 函数,该函数不断执行循环直至 _stop为真并且 Buffer 为空,循环逻辑如下:

  1. 加锁并检查是否需要终止( _stop 且无数据可消费)。
  2. 等待直到有数据或需停止。
  3. 交换生产和消费缓冲区。
  4. 唤醒可能等待生产空间的线程。

解锁后,执行回调处理 _consumer_buf,处理结束后,重置消费缓冲区。

在当前的代码逻辑中,每写入一条日志都会唤醒消费者处理线程,消费者线程就会进行缓冲区的交换,然后执行实际的日志落地程序,这样做的好处是最大限度的保证了日志的安全性,避免了可能因为系统故障而造成日志的丢失。但是这样却可能造成性能上的损失,如果每次只写入少量的日志,每次唤醒后磁盘IO就只会写入少量的数据,大大增加的磁盘IO的次数。

所以如果有需要可以对唤醒消费者线程的逻辑进行判定,如果当生产者缓冲区实际写入的数据超过缓冲区大小的60%(具体数字可根据实际来决定)就换新消费者线程进行实际落地操作。如果有时间维度的考量就可以再加上每过一段时间如果写入的数据达不到缓冲区设定的百分比的话也进行唤醒。

这样就可以做到兼具性能和实际需求。

核心功能模块实现

日志格式解析

对于日志信息支持用户自己设定不同的格式,系统提供了多种标识符,用户可以根据不通标识符来组装自己的日志信息格式。常见各个符号含义:

  • %d{format} : 输出时间(可指定日期格式)
  • %t : 线程ID
  • %p : 日志等级
  • %c : 日志器名称
  • %f : 源文件
  • %l : 行号
  • %m : 日志正文
  • %n : 换行符
  • %T : 水平制表符(代码中定义为tab)
  • 其它原文字符串不变

代码实现如下:

class FormatterItem
{   
public:
    using ptr = std::shared_ptr<FormatterItem>;
    virtual bool formatter(std::ostream &out, const Message &msg) = 0;
    virtual ~FormatterItem() {};
};

class msgFormatterItem : public FormatterItem
{
public:
    msgFormatterItem()
    {}

    bool formatter(std::ostream &out, const Message &msg)
    {
        out << msg._payload;
        return true;
    }
};

class levelFormatterIter : public FormatterItem
{
public:
    levelFormatterIter()
    {}

    bool formatter(std::ostream &out, const Message &msg)
    {
        out << Level::toString(msg._level);
        return true;
    }
};

class nameFormatterItem : public FormatterItem
{
public:
    nameFormatterItem()
    {}

    bool formatter(std::ostream &out, const Message &msg)
    {
        out << msg._name;
        return true;
    }
};

class threadFormatterItem : public FormatterItem
{
public:
    threadFormatterItem()
    {}

    bool formatter(std::ostream &out, const Message &msg)
    {
        out << msg._tid;
        return true;
    }
};

class timeFormatterItem : public FormatterItem
{
public:
    timeFormatterItem(const std::string &str = "%Y-%m-%d %H:%M:%S")
    : _format(str)
    {
        if(_format.empty()) _format = "%Y-%m-%d %H:%M:%S";
    }

    bool formatter(std::ostream &out, const Message &msg)
    {
        out << Util::Date::dateTime(_format, msg._time);
        return true;
    }
private:
    std::string _format;
};

class fileFormatterItem : public FormatterItem
{
public:
    fileFormatterItem()
    {}

    bool formatter(std::ostream &out, const Message &msg)
    {
        out << msg._file;
        return true;
    }
};

class lineFormatterItem : public FormatterItem
{
public:
    lineFormatterItem()
    {}

    bool formatter(std::ostream &out, const Message &msg)
    {
        out << msg._line;
        return true;
    }
};

class tabFormatterItem : public FormatterItem
{
public:
    tabFormatterItem()
    {}

    bool formatter(std::ostream &out, const Message &msg) override
    {
        out << "\t";
        return true;
    }
};

class nextFormatterItem : public FormatterItem
{
public:
    nextFormatterItem()
    {}

    bool formatter(std::ostream &out, const Message &msg) override
    {
        out << "\n";
        return true;
    }
};

class otherFormatterItem : public FormatterItem
{
public:
    otherFormatterItem(const std::string &str = "")
        : _str(str)
    {}

    bool formatter(std::ostream &out, const Message &msg)
    {
        out << _str;
        return true;
    }
private:
    std::string _str;
};

class Formatter
{
public:
    using ptr = std::shared_ptr<Formatter>;
    Formatter(const std::string &pattern = "[%d{%Y-%m-%d %H:%M:%S}][%t][%p][%c][%f:%l] %m%n")
        : _pattern(pattern)
    {
        assert(parsePattern());
    }

    void format(std::ostream &out, Message &msg)
    {
        for(auto &item : _items)
        {
            if(item == nullptr) abort();
            item->formatter(out, msg);
        }
    }

    std::string format(Message &msg)
    {
        std::stringstream ss;
        format(ss, msg);
        return ss.str();
    }
private:
    FormatterItem::ptr createItem(const std::string &key, const std::string &val)
    {
        if(key == "m") return std::make_shared<msgFormatterItem>();
        if(key == "d") return std::make_shared<timeFormatterItem>(val);
        if(key == "t") return std::make_shared<threadFormatterItem>();
        if(key == "c") return std::make_shared<nameFormatterItem>();
        if(key == "f") return std::make_shared<fileFormatterItem>();
        if(key == "l") return std::make_shared<lineFormatterItem>();
        if(key == "p") return std::make_shared<levelFormatterIter>();
        if(key == "T") return std::make_shared<tabFormatterItem>();
        if(key == "n") return std::make_shared<nextFormatterItem>();
        if(key == "") return std::make_shared<otherFormatterItem>(val);
        std::cerr << "Invalid key : " << key << std::endl; 
        // abort();
        return nullptr;
    }

    bool parsePattern()
    {
        std::vector<std::pair<std::string, std::string>> array;
        size_t idx = 0;
        size_t sz = _pattern.size();
        while(idx < sz)
        {
            if(_pattern[idx] == '%')
            {
                if(++idx >= sz)
                {
                    std::cerr << "The last character of the pattern format cannot be %\n";
                    return false;
                }
                if(_pattern[idx] == '%')
                {
                    array.push_back({"", "%"});
                    ++idx;
                }
                else
                {
                    std::string key, val;
                    key = _pattern[idx++];
                    if(_pattern[idx] == '{')
                    {
                        if(key != "d")
                        {
                            std::cerr << "None of the keys has {}, except for d\n";
                            return false;
                        }
                        int pos = _pattern.find_first_of('}', idx);
                        if(pos == std::string::npos)
                        {
                            std::cerr << "The pattern format less a }\n";
                            return false;
                        }
                        val = _pattern.substr(idx + 1, pos - idx - 1);
                        idx = pos + 1;
                    }
                    array.push_back({key, val});
                }
            }
            else 
            {
                int pos = _pattern.find_first_of('%', idx);
                std::string val;
                if(pos == std::string::npos)
                {
                    val = _pattern.substr(idx);
                    idx = sz;
                }
                else 
                {
                    val = _pattern.substr(idx, pos - idx);
                    idx = pos;
                }
                array.push_back({"", val});
            }
        }

        for(auto &p : array)
        {
            _items.push_back(createItem(p.first, p.second));
        }
        return true;
    }
private:
    std::string _pattern;
    std::vector<FormatterItem::ptr> _items;
};

代码中定义了 FormatterItem类,类中包含纯虚函数 formatter(std::ostream &out, const Message &msg),每个派生类实现“把消息中的某一项格式化输出”的逻辑。

各个FormatterItem子类

  • msgFormatterItem:输出日志消息内容(msg._payload)。
  • levelFormatterIter:输出日志级别(将msg._level转换成字符串)。
  • nameFormatterItem:输出日志器名称(msg._name)。
  • threadFormatterItem:输出线程ID(msg._tid)。
  • timeFormatterItem:输出日志时间(调用外部Util::Date::dateTime, 用msg._time和格式参数)。
  • fileFormatterItem:输出代码文件名(msg._file)。
  • lineFormatterItem:输出代码行号(msg._line)。
  • tabFormatterItem:输出字面字符串“/t”,应为“\t”(可能是个笔误)。
  • nextFormatterItem:输出换行符\n
  • otherFormatterItem:直接输出构造参数字符串,在pattern解析里辅助处理常文本。

还定义了 Formatter 主体,主要成员包含:

  • _pattern:日志格式字符串,用户可自定义。
  • _items:存放pattern解析得到的FormatterItem智能指针数组。

主要方法包括:

  • 构造与pattern解析(parsePattern)
    • 解析自定义的日志格式字符串_pattern,将每个格式项(如%m、%d{…}等等)解析为FormatterItem对象
    • 存到_items数组里顺序组合。
  • createItem(工厂方法):
    • 根据pattern解析得到的每个key及其附加值,返回对应类型的FormatterItem子类实例。
    • 支持常见的k-v型填充,比如%d{format}为时间格式,%m为消息内容等。
  • format
    • 解释型format:接受ostream和Message,依次调用每个FormatterItem格式化消息到流中。
    • string型format:接受一个Message,返回完整格式化后的日志字符串。

落地操作实现

日志落地操作模块是真正的日志写入磁盘的操作,无论是同步写日志还是异步写日志都是调用这个类里面的方法,在这个日志落地类的设计上采用了简单工厂模式,通过传入的落地类型以及参数来实例化不同的落地方法对象。

class Sink
{
public:
    using ptr = std::shared_ptr<Sink>;
    Sink() {}
    virtual ~Sink() {}
    virtual bool log(const std::string &data, size_t len) = 0;
};

class StdoutSink : public Sink
{
public:
    StdoutSink() {}
    ~StdoutSink() {}

    bool log(const std::string &data, size_t len) override
    {
        std::cout.write(data.c_str(), len);
        return true;
    }
};

class FileSink : public Sink
{
public:
    FileSink(const std::string &filename)
        : _filename(filename)   
    {
        if(Util::File::createPath(Util::File::path(_filename)) == false)
        {
            std::cerr << "create file path faild, file sink faild, file name is : " << _filename << std::endl;
            abort();
        }
        _ofs.open(_filename, std::ios::binary | std::ios::app);
        if(_ofs.is_open() == false)
        {
            std::cerr << "open file faild, file name is : " << _filename << std::endl;
            abort();
        }
    }

    ~FileSink() 
    {
        _ofs.close();
    }

    bool log(const std::string &data, size_t len) override
    {
        _ofs.write(data.c_str(), len);
        if(_ofs.good() == false)
        {
            std::cerr << "write file is faild, file name is : " << _filename << std::endl;
            return false;
        }
        return true;
    }

private:
    std::ofstream _ofs;
    std::string _filename;
};

class RollSinkBySize : public Sink
{
public:
    RollSinkBySize(const std::string &basename, const size_t maxsize = 1024 * 1024)
        : _basename(basename), _maxsize(maxsize), _cursize(0), _count(0)
    {
        if(Util::File::createPath(Util::File::path(_basename)) == false)
        {
            std::cerr << "create roll file path faild, roll file base name is : " << _basename << std::endl;
            abort();
        }
        // init();
    }

    ~RollSinkBySize()
    {
        _ofs.close();
    }

    bool log(const std::string &data, size_t len) override
    {
        if(init() == false)
        {
            std::cerr << "roll sink init faild\n";
            return false;
        }

        _ofs.write(data.c_str(), len);
        if(_ofs.good() == false)
        {
            std::cerr << "write roll file faild, file name is : " << _curfilename << std::endl;
            return false;
        }
        _cursize += len;
        return true;
    }

private:
    bool init()
    {
        if(_ofs.is_open() == false || _cursize >= _maxsize)
        {
            if(_ofs.is_open()) _ofs.close();
            _curfilename = getRollfileName();
            ++_count;
            _ofs.open(_curfilename, std::ios::binary | std::ios::app);
            if(_ofs.is_open() == false)
            {
                std::cerr << "init open file faild, file name is : " << _curfilename << std::endl;
                return false;
            }
            _cursize = 0;
        }
        return true;
    }

    std::string getRollfileName()
    {
        std::stringstream ss;
        ss << _basename;
        ss << Util::Date::dateTime("%Y-%m-%d_%H:%M:%S");
        ss << "_" + std::to_string(_count);
        ss << ".log";
        return ss.str();
    }

private:
    std::string _basename;
    std::string _curfilename;
    std::ofstream _ofs;
    size_t _maxsize;
    size_t _count;
    size_t _cursize;
};

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

项目中实现了一个抽象基类(接口类)Sink,定义所有日志输出通道需要实现的接口。virtual bool log(const std::string &data, size_t len) = 0; 纯虚函数,具体的输出方式由子类实现。

控制台输出实现(StdoutSink)重写log()方法,实现将日志内容直接输出到标准输出(通常是控制台)。std::cout.write(...)直接输出指定长度的数据,适应二进制或部分非字符串内容。

指定文件输出实现(FileSink)为文件输出的实现。构造函数接收文件名,并确保路径已存在,如果没有则尝试创建。以追加模式和二进制方式打开文件,便于高效地日志追加。

按文件大小自动滚动的文件输出(RollSinkBySize)支持自动按大小切换文件(日志滚动)。构造函数指定文件基本名(basename)和最大尺寸(默认1MB)。每次日志写入时:

  • 调用init()检查是否需要新建日志文件;

  • 超过指定大小就"滚动"新建文件,文件名由基本名、时间戳、编号等构成,避免重名。

  • 每次写入后更新累计写入字节数。

  • 这样保证了单个日志文件不会无限增大,便于文件管理和归档。

Sink工厂类(SinkFactory)是一个泛型工厂类,用于创建任意类型的Sink实例,参数完美转发。返回std::shared_ptr<Sink>,统一用智能指针管理,防止内存泄露,简洁高效。

日志器实现

日志器实现了同步日志器和异步日志器,根据需求可以使用不同的日志器来对日志信息进行写入。由于实例化日志器对象后,每个对象都有其自己的作用域,所以日志器又可以分为全局日志器和局部日志器,同时使用了单例模式对多日志器进行管理。同时为了简化日志器的构建,使用建造者模式来创建对应的日志器。

class Logger
{
public:
    using ptr = std::shared_ptr<Logger>;
    Logger(const std::string &logname, Formatter::ptr &formatter, Level::value level, std::vector<Sink::ptr> &sinks)
        : _logname(logname), _formatter(formatter), _level(level), _sinks(sinks.begin(), sinks.end())
    {}
    ~Logger()
    {}

    const std::string& name() { return _logname; }

    void debug(const std::string &file, const size_t line, const std::string &fmt, ...)
    {
        if(_level > Level::value::DEBUG) return;
        va_list ap;
        va_start(ap, fmt);
        char *str;
        if(vasprintf(&str, fmt.c_str(), ap) == -1)
        {
            std::cerr << "vasprintf faild\n";
            return;
        }
        va_end(ap);

        Message msg(line, _logname, file, str, Level::value::DEBUG);
        std::stringstream ss;
        _formatter->format(ss, msg);
        log(ss.str(), ss.str().size());
        free(str);
    }

    void info(const std::string &file, const size_t line, const std::string &fmt, ...)
    {
        if(_level > Level::value::INFO) return;
        va_list ap;
        va_start(ap, fmt);
        char *str;
        if(vasprintf(&str, fmt.c_str(), ap) == -1)
        {
            std::cerr << "vasprintf faild\n";
            return;
        }
        va_end(ap);

        Message msg(line, _logname, file, str, Level::value::INFO);
        std::stringstream ss;
        _formatter->format(ss, msg);
        log(ss.str().c_str(), ss.str().size());
        free(str);
    }

    void warn(const std::string &file, const size_t line, const std::string &fmt, ...)
    {
        if(_level > Level::value::WARN) return;
        va_list ap;
        va_start(ap, fmt);
        char *str;
        if(vasprintf(&str, fmt.c_str(), ap) == -1)
        {
            std::cerr << "vasprintf faild\n";
            return;
        }
        va_end(ap);

        Message msg(line, _logname, file, str, Level::value::WARN);
        std::stringstream ss;
        _formatter->format(ss, msg);
        log(ss.str().c_str(), ss.str().size());
        free(str);
    }

    void error(const std::string &file, const size_t line, const std::string &fmt, ...)
    {
        if(_level > Level::value::ERROR) return;
        va_list ap;
        va_start(ap, fmt);
        char *str;
        if(vasprintf(&str, fmt.c_str(), ap) == -1)
        {
            std::cerr << "vasprintf faild\n";
            return;
        }
        va_end(ap);

        Message msg(line, _logname, file, str, Level::value::ERROR);
        std::stringstream ss;
        _formatter->format(ss, msg);
        log(ss.str().c_str(), ss.str().size());
        free(str);
    }

    void fatal(const std::string &file, const size_t line, const std::string &fmt, ...)
    {
        if(_level > Level::value::FATAL) return;
        va_list ap;
        va_start(ap, fmt);
        char *str;
        if(vasprintf(&str, fmt.c_str(), ap) == -1)
        {
            std::cerr << "vasprintf faild\n";
            return;
        }
        va_end(ap);

        Message msg(line, _logname, file, str, Level::value::FATAL);
        std::stringstream ss;
        _formatter->format(ss, msg);
        log(ss.str().c_str(), ss.str().size());
        free(str);
    }

protected:
    virtual bool log(const std::string &data, const size_t len) = 0;

protected:
    std::mutex _mtx;
    std::string _logname;
    Formatter::ptr _formatter;
    std::atomic<Level::value> _level;
    std::vector<Sink::ptr> _sinks;
};

class SyncLogger : public Logger
{
public:
    SyncLogger(const std::string &logname, Formatter::ptr &formatter, Level::value level, std::vector<Sink::ptr> &sinks)
        : Logger(logname, formatter, level, sinks)
    {}

    ~SyncLogger()
    {}

protected:
    bool log(const std::string &data, const size_t len) override
    {
        std::unique_lock<std::mutex> lock(_mtx);
        if(_sinks.empty()) return true;
        for(auto &sink : _sinks)
        {
            sink->log(data, len);
        }
        return true;
    }
};

class AsyncLogger : public Logger
{
public:
    AsyncLogger(const std::string &logname, Formatter::ptr &formatter, 
        Level::value level, std::vector<Sink::ptr> &sinks, AsyncType type)
        : Logger(logname, formatter, level, sinks), _looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), type))
    {}

    bool log(const std::string &data, const size_t len) override
    {
        _looper->push(data, len);
        return true;
    }

    void realLog(Buffer &buffer)
    {
        if(_sinks.empty()) return;
        for(auto &sink : _sinks)
        {
            sink->log(buffer.readBegin(), buffer.readAbleSize());
        }
    }
private:
    AsyncLooper::ptr _looper;
};

enum class LogType
{
    AsyncLogger,
    SyncLogger
};

class LoggerBuilder
{
public:
    LoggerBuilder() : _loggerType(LogType::SyncLogger), _level(Level::value::DEBUG), _asyncType(AsyncType::ASYNC_SAFE)
    {}

    ~LoggerBuilder()
    {}

    void buildLoggerName(const std::string &name) { _loggerName = name; }
    void buildLoggerType(const LogType type) { _loggerType = type; }
    void buildEnableUnsafe() { _asyncType = AsyncType::ASYNC_UNSAFE; }
    void buildLoggerLevel(Level::value level) { _level = level; }
    void buildLoggerMatter(const std::string &pattern) { _formatter = std::make_shared<Formatter>(pattern); }

    template<typename SinkType, typename ...Args>
    void buildSink(Args &&...args)
    {
        Sink::ptr sink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
        _sinks.push_back(sink);
    }

    virtual Logger::ptr build() = 0;

protected:
    AsyncType _asyncType;
    LogType _loggerType;
    std::string _loggerName;
    Level::value _level;
    Formatter::ptr _formatter;
    std::vector<Sink::ptr> _sinks;
};

class LocalBuildLogger : public LoggerBuilder
{
public:
    Logger::ptr build() override
    {
        assert(!_loggerName.empty());
        if(_formatter.get() == nullptr)
        {
            _formatter = std::make_shared<Formatter>();
        }
        if(_sinks.empty())
        {
            buildSink<StdoutSink>();
        }
        if(_loggerType == LogType::AsyncLogger)
        {
            return std::make_shared<AsyncLogger>(_loggerName, _formatter, _level, _sinks, _asyncType);
        }
        return std::make_shared<SyncLogger>(_loggerName, _formatter, _level, _sinks);
    }
};


class LoggerManager
{
public:
    static LoggerManager &getInstance()
    {
        static LoggerManager _eton;
        return _eton;
    }

    bool addLogger(Logger::ptr &logger)
    {
        if(hasLogger(logger->name())) return true;
        std::unique_lock<std::mutex> lock(_mtx);
        _loggers.insert({logger->name(), logger});
        return true;
    }

    bool hasLogger(const std::string &name)
    {
        return _loggers.find(name) != _loggers.end();
    }

    Logger::ptr getLogger(const std::string &loggerName)
    {
        if(hasLogger(loggerName)) return _loggers[loggerName];
        return nullptr;
    }

    Logger::ptr rootLogger()
    {
        return getLogger("root");
    }
private:
    LoggerManager()
    {
        std::unique_ptr<wzh::LoggerBuilder> builder(new wzh::LocalBuildLogger());
        builder->buildLoggerName("root");
        _root_logger = builder->build();
        _loggers.insert({"root", _root_logger});
    }
private:
    std::mutex _mtx;
    Logger::ptr _root_logger;
    std::unordered_map<std::string, Logger::ptr> _loggers;
};

class GlobalBuildLogger : public LoggerBuilder
{
public:
    Logger::ptr build() override
    {
        assert(!_loggerName.empty());
        if(_formatter.get() == nullptr)
        {
            _formatter = std::make_shared<Formatter>();
        }
        if(_sinks.empty())
        {
            buildSink<StdoutSink>();
        }
        Logger::ptr logger;
        if(_loggerType == LogType::AsyncLogger)
        {
            logger = std::make_shared<AsyncLogger>(_loggerName, _formatter, _level, _sinks, _asyncType);
        }
        else 
        {
            logger = std::make_shared<SyncLogger>(_loggerName, _formatter, _level, _sinks);
        }
        LoggerManager::getInstance().addLogger(logger);
        return logger;
    }
};

Logger 是所有日志器的抽象基类,日志拥有名字(支持多日志器)、格式器、日志级别、输出目标(Sink)列表。

日志写入接口(debug/info/warn/error/fatal),每个日志级别都提供一个格式化变参接口。函数的大致的逻辑都一样

  • 逐级比较日志级别,决定是否输出。
  • 使用变参函数生成日志内容字符串(vasprintf)并格式化。
  • 组装Message对象(包含文件、行号、日志器名、内容和日志级别)。
  • FormatterMessage格式化为实际的输出内容。
  • log为纯虚方法,交由子类实现具体写入流程。

同步日志器的处理逻辑非常简单,在调用 log()函数后,log 实现通过锁保证线程安全,将格式化后日志内容同步写入所有Sink(例如文件、控制台等)。异步日志器的写入由log把数据发到AsyncLooper缓冲区,由专门线程批量写入,提升高并发场景的吞吐。realLog由异步线程回调调用,实现实际落地。

日志器单例管理器提供单例管理所有Logger对象的能力,内置一个root日志器。支持添加、检索Logger,通过名字唯一标识不同日志器,便于多模块多用途日志隔离管理。

采用建造者模式,实现了 Builder类,便于灵活配置日志器的各种参数:名字、类型(同步/异步)、级别、格式、输出目标等。build为纯虚函数,留给子类实现最后的实例化步骤。

  • LocalBuildLogger:只负责生成Logger对象,不管理注册;

  • GlobalBuildLogger:构建Logger并添加到全局管理器(LoggerManager),实现单例管理多日志器。

⚠在多日志器向同一个文件中写入日志信息时,存在线程安全问题,由于目前项目中日志信息的写磁盘操作是没有加锁的,只在日志器中加锁,也就是每个日志器在调用写磁盘操作函数时加锁,这个锁只保证了单个日志器对象在多线程环境下的线程安全问题,但是存在不同日志器对象在写入同一个文件时的线程安全问题。

image-20250628175119520

如果给写磁盘操作也加上锁,那么当多个日志器对象写同一个文件时,也是需要保证使用的是同一个sink对象,目前比较好的想法是,维护一个sink对象操作池,对于同一个文件路径实例化一个sink对象。

测试

本次测试主要针对日志系统的 实用性性能表现,通过设计压力测试用例,模拟高并发写入日志的场景,验证日志系统在不同写入模型(同步 vs 异步)下的表现。

测试环境

  • 运行平台:虚拟机(VMware/VirtualBox)
  • 操作系统:Ubuntu 20.04 LTS
  • 宿主机 CPU:AMD Ryzen 7 4800U with Radeon Graphics @ 1.80GHz
  • 磁盘类型:默认虚拟硬盘
  • 编译器:g++(C++11)

测试参数

参数 说明
线程数 10
总日志数 1,000,000
每条日志长度 100 字符
同步模式 SyncLogger(加锁写文件)
异步模式 AsyncLogger(后台线程写)

测试结果

同步日志模式(SyncLogger)

  • 总耗时:约 3.13s
  • 日志速率:319,861 条/秒
  • 吞吐量:31 MB/s

异步日志模式(AsyncLogger)

  • 总耗时:约 1.64s
  • 日志速率:610,835 条/秒
  • 吞吐量:59 MB/s

性能分析

  • 异步日志写入在高并发下表现优越,性能为同步的约 2 倍
  • 同步模式中线程频繁争夺锁,成为性能瓶颈;
  • 异步写入通过日志队列缓冲和独立线程写磁盘,显著降低了锁竞争和系统调用次数;
  • 实际写入日志与预期条数几乎一致,说明系统在稳定性方面也表现良好。

附件

最后附上一张项目类关系图,项目Github链接点击


网站公告

今日签到

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