前言
对于开发者来说,最通常的定位手段,就是打断点来定位问题。这当开发者能看到全模块代码时,自然没什么问题。但是当项目中模块解耦比较好,每个开发团队只关注于本模块的开发,对其他模块的依赖仅仅是通过api接口,这样当模块间出现异常时,定位手段就显得有点不足,或是需要其他团队成员协助定位,这样人力和时间成本的开销就变大了。
对于测试团队和维护团队来说,用户侧在操作软件时发现了一个问题,比如说操作到某一步就发生了某种异常(不限于不符合预期或崩溃),这个时候,维护团队或测试团队就得快速的复现问题,如果是本地协同办公,交代讲解还不是很麻烦;如果是异地或涉及到政企单位,通常不可能或较困难通过描述复现场景。
这个时候就需要新的手段来辅助开发者进行问题的界定,或辅助维护团队快速的复现问题(或是用户的操作)。
日志管理系统就是这样一个辅助工具,它可以帮助开发者和维护团队快速的定位问题,并帮助测试团队复现问题。
本文就来介绍通常该怎么为自己的软件搭建一套日志管理系统。
日志管理系统的定义
日志管理系统是支持开发人员在用户操作的节点记录用户行为及业务异常信息的系统。
该系统具有使用简便的特点,最好是仅包含头文件即可进行使用。
本文介绍的日志管理系统可自定义日志格式,线程安全,效率高,可实现自动按日期创建日志文件/定时创建日志文件,支持单/多线程,异步/同步,阻塞非阻塞模式,支持多目标输出。
本日志管理系统的原理
本日志底层实现主要是利用三个关键能力来管理日志。分别是
- registry
- logger
- sink
其分别承担管理日志,收集日志,输出日志的功能。
本日志管理系统的设计
设计思路
日志主要使用了spdlog
作为第三方日志库,并提供简便的接口使用日志系统。
关键库的算法设计
关键算法1
spdlog
通过logger
收集日志信息,并管理着输出到不同终端的sink
,sink
需要对新产生的日志进行等级判断,等于和大于等级要求的,可以入槽,等待刷新时写入磁盘。
关键算法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");
...
}
总结
本文介绍了日志管理系统的原理、设计思路、关键库的算法设计、主要接口定义、主要实现逻辑,并给出了一些示例代码。希望能对读者有所帮助。
如果有不明白的地方,欢迎留言交流。