C++高手进阶:如何设计自己的日志管理系统

前言

对于开发者来说,最通常的定位手段,就是打断点来定位问题。这当开发者能看到全模块代码时,自然没什么问题。但是当项目中模块解耦比较好,每个开发团队只关注于本模块的开发,对其他模块的依赖仅仅是通过api接口,这样当模块间出现异常时,定位手段就显得有点不足,或是需要其他团队成员协助定位,这样人力和时间成本的开销就变大了。

对于测试团队和维护团队来说,用户侧在操作软件时发现了一个问题,比如说操作到某一步就发生了某种异常(不限于不符合预期或崩溃),这个时候,维护团队或测试团队就得快速的复现问题,如果是本地协同办公,交代讲解还不是很麻烦;如果是异地或涉及到政企单位,通常不可能或较困难通过描述复现场景。

这个时候就需要新的手段来辅助开发者进行问题的界定,或辅助维护团队快速的复现问题(或是用户的操作)。

日志管理系统就是这样一个辅助工具,它可以帮助开发者和维护团队快速的定位问题,并帮助测试团队复现问题。

本文就来介绍通常该怎么为自己的软件搭建一套日志管理系统。

日志管理系统的定义

日志管理系统是支持开发人员在用户操作的节点记录用户行为及业务异常信息的系统。

该系统具有使用简便的特点,最好是仅包含头文件即可进行使用。

本文介绍的日志管理系统可自定义日志格式,线程安全,效率高,可实现自动按日期创建日志文件/定时创建日志文件,支持单/多线程,异步/同步,阻塞非阻塞模式,支持多目标输出。

本日志管理系统的原理

本日志底层实现主要是利用三个关键能力来管理日志。分别是

  • registry
  • logger
  • sink

其分别承担管理日志,收集日志,输出日志的功能。

本日志管理系统的设计

设计思路

日志主要使用了spdlog 作为第三方日志库,并提供简便的接口使用日志系统。

关键库的算法设计

关键算法1

spdlog通过logger收集日志信息,并管理着输出到不同终端的sinksink需要对新产生的日志进行等级判断,等于和大于等级要求的,可以入槽,等待刷新时写入磁盘。
在这里插入图片描述

关键算法2

刷新是指全部日志都真正写入磁盘,同步写日志时,log 每满 4kb,则会自动往磁盘写入。只有当正常与异常时的流程结束之前调用 flush 才可以保证不丢失日志信息。而异常情况的处理

在这里插入图片描述

主要宏定义

宏名定义
LOG_INFO日志等级
LOG_WARN日志等级
LOG_ERROR日志等级
LOG_FLUSH在程序正常结束和异常处理的时候调用,保证所有日志都能写入磁盘
LOG_ERROR_LOCATION代码逻辑异常处具体异常信息记录

主要实现逻辑

#define LOG_INFO(...) Log::info(__VA_ARGS__)
#define LOG_WARN(...) Log::warn(__VA_ARGS__)
#define LOG_ERROR(...) Log::error(__VA_ARGS__)

#define LOG_ERROR_LOCATION Log::error("The error occurred in the line: %d, file : %s", __LINE__, __FILE__)

#define LOG_FLUSH Log::flush()

class BASIC_TOOL_EXPORT Log {
public:
    enum class Level {
        INFO = 0,
        WARN,
        ERROR
    };

    static void init(Level level);
    static void uninit();

    // 获取日志存储路径及日志记录时间
    static void getLogInfos(std::string& logPath, std::string& timeStr);

    static void info(const char* format, ...);
    static void warn(const char* format, ...);
    static void error(const char* format, ...);

    static void info(const wchar_t* format, ...);
    static void warn(const wchar_t* format, ...);
    static void error(const wchar_t* format, ...);

    static void flush();

public:
    log();
    ~log();

public:
    spdlog::logger* m_pLogger = nullptr;
    std::string m_logPath;
};

using Type = Log::Level;
void Log::init(Level level)
{
    static bool bInitialized = false;
    if (bInitialized)
    {
        return;
    }
    bInitialized = true;

    // 创建日志路径及文件名
    // ...
    // 日志路径 : m_logpath
    // 日志文件名 : logName

    std::wstring fullPath = m_logpath + logName;

    auto rotatingSink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(fullPath, FULL_PATH_LEN, FULL_PATH_SINK_NUM);
    auto ringBufferSink = std::make_shared<spdlog::sinks::ringbuffer_sink_mt>(static_cast<size_t>(SINK_SIZE));
    m_pLogger = new spdlog::logger("global_log", { rotatingSink, ringBufferSink });
    m_pLogger->set_level(static_cast<spdlog::level::level_enum>(type));
}

void Log::uninit()
{
    flush();
    if (m_pLogger) {
        spdlog::shutdown();
        delete m_pLogger;
        m_pLogger = nullptr;
    }
}

void Log::flush()
{
    if (m_pLogger) {
        m_pLogger->flush();
    }
}

void Log::getLogInfos(std::string& logPath)
{
    logPath = g_logImpl.m_logPath;
}

void Log::info(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    char buf[BUFFER_LEN] = {};
    vsnprintf(buf, BUFFER_LEN, fmt, args);
    m_pLogger->info(fmt);
    va_end(args);
}

void Log::warn(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, BUFFER_LEN, fmt, args);
    m_pLogger->warn(fmt);
    va_end(args);
}


void Log::error(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, BUFFER_LEN, fmt, args);
    m_pLogger->error(fmt);
    va_end(args);
}

void Log::info(const wchar_t* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    wchar_t wbuf[HALF_BUFFER_LEN] = {};
    vswprintf(wbuf, HALF_BUFFER_LEN, fmt, args);
    WideCharToMultiByte(CP_OEMCP, 0, wbuf, -1, buf, BUFFER_LEN, nullptr, 0);
    m_pLogger->info(buf);
    va_end(args);
}

void Log::warn(const wchar_t* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    wchar_t wbuf[HALF_BUFFER_LEN] = {};
    vswprintf(wbuf, HALF_BUFFER_LEN, fmt, args);
    WideCharToMultiByte(CP_OEMCP, 0, wbuf, -1, buf, BUFFER_LEN, nullptr, 0);
    m_pLogger->warn(buf);
    va_end(args);
}


void Log::error(const wchar_t* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    wchar_t wbuf[HALF_BUFFER_LEN] = {};
    vswprintf(wbuf, HALF_BUFFER_LEN, fmt, args);
    WideCharToMultiByte(CP_OEMCP, 0, wbuf, -1, buf, BUFFER_LEN, nullptr, 0);
    m_pLogger->error(buf);
    va_end(args);
}

使用者如何用

通常开发者当想用日志记录信息时,仅仅需要包含该日志定义的头文件,然后使用封装好的宏就行了。

// example
#include "log.h"

void func()
{
    ...
    LOG_INFO("This is a info log");
    LOG_INFO("This is a info log: %.3f, %.3f, %.3f", data.x, data.y, data.z);
    LOG_ERROR("This is a error log");
    ...
}

总结

本文介绍了日志管理系统的原理、设计思路、关键库的算法设计、主要接口定义、主要实现逻辑,并给出了一些示例代码。希望能对读者有所帮助。

如果有不明白的地方,欢迎留言交流。

  • 31
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值