Logger的组装(c++20)

引言

本篇文章将综合前几篇文章,appender,format,buffer,组装我们的一个Logger,其作用是可以输出日志到特定的地方。这也是我们日志器的核心部分。

Logger.h

我们的日志器并不是只有一个,每一个日志器负责不同的部分,而我们一定有一个根日志器,这个根日志器的format是我们默认设定的,而其他的子日志器可能有着不同的format,appender,因为输出的地方毕竟是不同的。但是当子日志器没有我们设定的appender的时候,就会调用根日志器的appender,用默认输出,所以我们每一个子日志器还要保留一个根日志器的指针。

我们的架构其实和appender差不多,首先我们要限制访问Logger的接口,我们使用Logger的情况只有两个任务,第一个是submit,目的是要把我们输出的内容交给Logger,第二个是setRoot,目的是方便当我们创造一个Logger的时候让这个Logger保存其根Logger的指针。

我们辨别不同Logger的方法是根据其名字来判断,所以我们之后会有一个LoggerManager来专门管理所有的子Logger

对于一个Logger,我们需要操控Appender,这个Appender是控制我们的Logger将内容输出到哪里的一个零件,而至于输出什么,就是FormatterPattern的事情了。一个Logger会有多个Appender,因为有不同的输出地方。

并且我们依然使用桥接的设计模式,把我们所有的实现细节全部隐藏到impl里面,这里有一个同的地方和Appender,Appender不仅仅是要隐藏实现的细节,而且还用来扩展可以多处输出,一个Appender通过impl可以达到输出到文件里面和输出到终端里面,但是这里仅仅是为了隐藏细节(锁,原子变量)和方便更改,因为我们的所有实现都是在impl里面实现的,也就是说我们如果要发生修改代码,我们不会影响Logger里面的代码。

cpp 复制代码
// 用来代理我们的内部权限,严格控制谁可以访问我们的Logger私有实现
namespace detail {
class LoggerAccess;
} // namespace detail

class PANS_API Logger final 
{
public:
    class Impl;
    explicit Logger(std::string name = "root");
    ~Logger();

    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
    Logger(Logger&&) = delete;
    Logger& operator=(Logger&&) = delete;

    [[nodiscard]] bool shouldLog(LogLevel::Level level) const noexcept;
    [[nodiscard]] LogLevel::Level getLevel() const noexcept;
    void setLevel(LogLevel::Level level) noexcept;
    [[nodiscard]] std::string_view getName() const noexcept;

    void setFormatter(std::string_view pattern);
    [[nodiscard]] std::string getFormatterPattern() const;
    void addAppender(AppenderPtr appender);
    void removeAppender(const AppenderPtr& appender);
    void clearAppenders();

    void flush();
    void sync();
private:
    std::unique_ptr<Impl> impl_;
    friend class detail::LoggerAccess;
};

using LoggerPtr = std::shared_ptr<Logger>;

[[nodiscard]] PANS_API LoggerPtr GetRootLogger();
[[nodiscard]] PANS_API LoggerPtr GetLogger(std::string_view name);

我们这里使用读写锁,因为我们这里读多写少,很多情况下是读取有没有appenders,而读写锁是需要辅助加锁工具的,我们的读锁用的是shared_lock,而写锁用的是unique_lock

cpp 复制代码
class Logger::Impl
{
public:
    explicit Impl(std::string name);
    [[nodiscard]] bool shouldLog(LogLevel::Level level) const noexcept;
    void submit(const detail::LogRecordView& record) noexcept;
    void setLevel(LogLevel::Level level) noexcept;
    [[nodiscard]] LogLevel::Level getLevel() const noexcept;
    [[nodiscard]] std::string_view getName() const noexcept;

    void setFormatter(std::shared_ptr<const detail::Formatter> formatter);
    [[nodiscard]] std::shared_ptr<const detail::Formatter> getFormatter() const noexcept;
    void addAppender(AppenderPtr appender);
    void removeAppender(const AppenderPtr& appender);
    void clearAppenders();

    void flush();
    void sync();

    void setRoot(const LoggerPtr& root) noexcept;

private:
    std::string name_;
    std::atomic<LogLevel::Level> level_{LogLevel::Level::LOG_DEBUG};

    mutable std::shared_mutex mutex_;
    std::shared_ptr<const detail::Formatter> formatter_;
    std::vector<AppenderPtr> appenders_;

    LoggerPtr root_;
};

namespace detail {
/**
 * LoggerAccess本质上是一个内部权限访问的桥梁,
 * 用于在不扩大Logger公共API的情况下,访问其私有实现
 */
class LoggerAccess final 
{
public:
    static void Submit(Logger& logger, const LogRecordView& record) noexcept;
    static void SetRoot(Logger& logger, const LoggerPtr& root) noexcept;
};
}

这个就是我们隐藏实现的细节。

cpp 复制代码
constexpr std::string_view DEFAULT_LOG_PATTERN = "%d{%Y-%m-%d %H:%M:%S}.%u%Tthread=%t%Tfiber=%F%T[%p]%T%f:%l%T%m%n";

Logger::Logger(std::string name) 
    : impl_(std::make_unique<Impl>(std::move(name)))
{}

Logger::~Logger() = default;

bool Logger::shouldLog(LogLevel::Level level) const noexcept
{
    return impl_->shouldLog(level);
}

void Logger::setLevel(LogLevel::Level level) noexcept
{
    impl_->setLevel(level);
}

LogLevel::Level Logger::getLevel() const noexcept
{
    return impl_->getLevel();
}

std::string_view Logger::getName() const noexcept
{
    return impl_->getName();
}

void Logger::setFormatter(std::string_view pattern)
{
    auto formatter = std::make_shared<const detail::Formatter>(pattern);
    impl_->setFormatter(std::move(formatter));
}

std::string Logger::getFormatterPattern() const 
{
    const auto formatter = impl_->getFormatter();
    return formatter == nullptr ? std::string() : formatter->getPattern();
}

void Logger::addAppender(AppenderPtr appender)
{
    impl_->addAppender(appender);
}

void Logger::removeAppender(const AppenderPtr& appender)
{
    impl_->removeAppender(appender);
}

void Logger::clearAppenders()
{
    impl_->clearAppenders();
}

void Logger::flush()
{
    impl_->flush();
}

void Logger::sync()
{
    impl_->sync();
}

这个是impl的构造函数,通过名字来创建了一个指针,并且把format的形式设置为默认,这个构造函数的调用就是在Logger的构造函数里面,也就是创建Logger的同时也就创建了其impl

cpp 复制代码
Logger::Impl::Impl(std::string name)
    : name_(name)
{
    if (name.empty())
    {
        throw std::invalid_argument("logger name cannot be empty");
    }
    formatter_ = std::make_shared<const detail::Formatter>(DEFAULT_LOG_PATTERN);
}

判断一个Logger该不该输出这个日志,首先判断其等级是不是足够,当等级足够的情况下,我们判断其有没有appenders,如果没有appender,那么就没有办法定向的输出到我们需要的位置,比如说文件,比如说终端。

cpp 复制代码
bool Logger::Impl::shouldLog(LogLevel::Level level) const noexcept
{
    if (static_cast<u8>(level) < static_cast<u8>(getLevel()))
    {
        return false;
    }
    {
        std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁
        if (!appenders_.empty())
        {
            return true;
        }
    }
    return root_ != nullptr && root_->shouldLog(level);
}

提交我们要打印的日志,我们这里需要捕捉错误的原因是因为我们要把日志写到我们的缓冲区里面,如果我们日志过长就需要开辟缓冲区,但是没有开辟成功(内存不够就会报错),需要catch。我们整个的过程是通过format把我们的record解析完之后放在缓冲区里面,然后遍历appender,把所有符合标准的appender输出到对应的地方。

cpp 复制代码
void Logger::Impl::submit(const detail::LogRecordView& record) noexcept
{
    if (static_cast<u8>(record.level_) < static_cast<u8>(getLevel()))
    {
        return;
    }
    std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁
    if (appenders_.empty()) 
    {
        lock.unlock();
        if (root_ != nullptr)
        {
            root_->impl_->submit(record);
        }
        return;
    }
    // InlineBuffer,它遇到长日志的时候,可能会使用堆内存
    // vector开辟堆内存的时候,会有极小概率抛bad_alloc异常,这里不能让异常逃逸出去
    try 
    {
        detail::FormattedRecordBuffer formatted_record;
        formatter_->format(record, formatted_record);
        for (const AppenderPtr& appender : appenders_)
        {
            detail::AppenderAccess::Append(appender, record.level_, formatted_record.view());
        }
    }
    catch(...)
    {
        constexpr std::string_view message = "pans logger : record formatting failed\n";
        std::fwrite(message.data(), 1, message.size(), stderr);
    }
}
  • Formatter :保存整个格式字符串 pattern_,比如 "[%p] %m%n"
  • FormatItem :负责格式中的一个部分,比如 %p 输出等级、%m 输出消息、%n 输出换行,普通文字也有对应的 item。

一个formatter保存了对应的格式串,我们最外部的接口传递进来的是string类型,也就是转化成了指针,所以在内部的设置时就是用指针的形式。

cpp 复制代码
void Logger::Impl::setLevel(LogLevel::Level level) noexcept
{
    level_.store(level, std::memory_order_release);
}

void Logger::Impl::setFormatter(std::shared_ptr<const detail::Formatter> formatter)
{
    ASSERT_RETNONE2(formatter != nullptr, "logger formatter cannot be null");
    std::unique_lock<std::shared_mutex> lock(mutex_); // 写锁
    formatter_ = std::move(formatter);
}

std::shared_ptr<const detail::Formatter> Logger::Impl::getFormatter() const noexcept
{
    std::shared_lock<std::shared_mutex> lock(mutex_);
    return formatter_;
}

void Logger::Impl::addAppender(AppenderPtr appender)
{
    ASSERT_RETNONE2(appender != nullptr, "logger appender cannot be null");
    std::unique_lock<std::shared_mutex> lock(mutex_);
    appenders_.push_back(std::move(appender));
}

void Logger::Impl::removeAppender(const AppenderPtr& appender)
{
    std::unique_lock<std::shared_mutex> lock(mutex_);
    std::erase(appenders_, appender);
}

void Logger::Impl::clearAppenders() 
{
    std::unique_lock<std::shared_mutex> lock(mutex_);
    appenders_.clear();
}

void Logger::Impl::flush()
{
    std::shared_lock<std::shared_mutex> lock(mutex_);
    if (appenders_.empty())
    {
        lock.unlock();
        if (root_ != nullptr)
        {
            root_->flush();
        }
        return;
    }
    for (const AppenderPtr& appender : appenders_)
    {
        appender->flush();
    }
}

void Logger::Impl::sync()
{
    std::shared_lock<std::shared_mutex> lock(mutex_);
    if (appenders_.empty())
    {
        lock.unlock();
        if (root_ != nullptr)
        {
            root_->sync();
        }
        return;
    }

    for (const AppenderPtr& appender : appenders_) 
    {
        appender->sync();
    }
}

void Logger::Impl::setRoot(const LoggerPtr& root) noexcept
{
    root_ = root;
}

这个就是我们对外部提供的接口,调用的其实就是impl里面的实现

cpp 复制代码
void detail::LoggerAccess::Submit(Logger& logger, const LogRecordView& record) noexcept
{
    logger.impl_->submit(record);
}

void detail::LoggerAccess::SetRoot(Logger& logger, const LoggerPtr& root) noexcept
{
    logger.impl_->setRoot(root);
}

最后就是LogerManager,用map的数据结构来存储Logger的指针和名字,并且设置root的指针,这一个manager是单例模式,每一次我们getLogger(),如果没有找到我们需要的Logger,我们都会创建一个新的Logger,同时对这个logger指定我们已经设定好的root

cpp 复制代码
class LoggerManager final
{
public:
    LoggerManager()
        : root_(std::make_shared<Logger>("root"))
    {
        root_->addAppender(MakeStdoutAppender());
        loggers_.emplace("root", root_);
    }

    [[nodiscard]] LoggerPtr getRoot() const noexcept
    {
        return root_;
    }

    [[nodiscard]] LoggerPtr getLogger(std::string_view name)
    {
        ASSERT_RETVAL2(!name.empty(), nullptr, "logger name cannot be empty");
        std::lock_guard<std::mutex> lock(mutex_);
        const auto iterator = loggers_.find(std::string(name));
        if (iterator != loggers_.end())
        {
            return iterator->second;
        }

        auto logger = std::make_shared<Logger>(std::string(name));
        // 给新来的logger绑定根logger,以防止如果新的logger没有设置appender,那么我们就可以用根logger
        detail::LoggerAccess::SetRoot(*logger, root_);
        loggers_.emplace(logger->getName(), logger);
        return logger;
    }

private:
    mutable std::mutex mutex_;
    std::unordered_map<std::string, LoggerPtr> loggers_;
    LoggerPtr root_;
};

LoggerManager& GetLoggerManager()
{
    static LoggerManager manager;
    return manager;
}

LoggerPtr GetRootLogger()
{
    return GetLoggerManager().getRoot();
}

LoggerPtr GetLogger(std::string_view name)
{
    return GetLoggerManager().getLogger(name);
}

总结

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

相关推荐
尾善爱看海2 小时前
前端算法与手写题集
前端·算法
繁星蓝雨2 小时前
C++的设计与演进大纲(如何从C语言一步步成长为C++)
c语言·开发语言·c++·c++历史·c++演进
程序员阿鹏2 小时前
为什么MySQL InnoDB选择B+树?
数据结构·数据库·b树·sql·mysql·算法·缓存
蒸蒸yyyyzwd3 小时前
机试学习笔记
c++·求职招聘
AI 思录3 小时前
Prompt 事故档案(八):日常表达被标为“待校准”,AI 的爹味语法从哪里来
大数据·人工智能·算法·prompt·用户体验·ai合规
月光船幽幽3 小时前
跨范式映射的稳定接口设计
人工智能·python·算法
handler014 小时前
【Linux】线程与虚拟内存的底层世界
linux·运维·服务器·c++·线程·虚拟地址空间·共享内存
郝学胜-神的一滴4 小时前
C++11 工程级应用 09:告别无谓拷贝,解锁高性能移动语义
开发语言·数据结构·c++·vscode·软件工程·visual studio
2601_965742224 小时前
全媒体运营与短视频代运营,两者有什么区别?
大数据·数据结构·人工智能·算法·ai·媒体