一个完善的系统基础部分一般会包括日志的打印输出以及日志文件的管理,如果只使用c/c++原生的打印接口,会有很多问题:
1、最直接的一个问题就是打印日志太多的话,运行内存会暴涨,会有太多的日志内容堆积在缓冲区;
2、多线程情况下的printf可能会出现日志的混乱;
3、日志格式容易不规范,关键信息可能会缺失,比如严重程度、日志的准确时间、输出日志的线程、函数名称、文件行号等等;
4、不利于日志的后期整合处理,一个服务可能会有很多节点,不规范的日志打印整合以后很难进行分析和定位
日志系统需要解决上述的这些问题,因此一个好用的日志系统,应该具有以下的特点:
1、支持多线程日志输出,输出效率足够高,每秒达到10000+行日志的输出(程序超出这个需求,可能要考虑是不是日志打印太多了);
2、日志文件自动切分,应避免单个日志文件过大,切分方式可以是按天、小时、分钟、文件大小超出限制等;
3、日志自动清理,超过一定天数的日志应自动删除(当然可以通过额外的定时脚本达到这个目的),避免长期运行后积累的日志过多,过大;
4、每条日志应分等级,严重等级较低日志可以通过配置而不打印;
5、日志应统一记录以下信息:严重等级、发生时间(毫秒级)、发生地点(具体到文件行和函数名)、生成者(进程信息、线程信息、开发人员自定义信息),比如:
这里要输出线程名称,需要使用开源库中cppfd::Thread类型的线程,否则是一个未知名称的线程。
基于前两篇文章《利用std::atomic实现无锁队列-CSDN博客》《一种轻量的C++无锁多线程框架-CSDN博客》 应很容易理解日志系统的实现原理,详细的代码见:GitHub - laiyongcong/cppfoundation: c++ basic library
代码总量也就三百行左右,十分轻量,使用上也很简单:
#include "Log.h"
void TestLog() {
LogConfig cfg;
cfg.ProcessName = "testLog";
Log::Init(cfg);
uint64_t uTime1 = cppfd::Utils::GetCurrentTime();
for (int i = 0; i < 100000; i++) {
LOG_TRACE("this is test log %d", i);
}
uint64_t uTime2 = cppfd::Utils::GetCurrentTime();
std::cout << "log cost " << uTime2 - uTime1 << std::endl;
Log::Destroy();
}
日志输出可以使用定义好的宏:
#if CPPFD_PLATFORM == CPPFD_PLATFORM_WIN32
# define LOG_WARNING(fmt, ...) cppfd::Log::WriteLog(cppfd::ELogLevel_Warning, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), __VA_ARGS__)
# define LOG_FATAL(fmt, ...) cppfd::Log::WriteLog(cppfd::ELogLevel_Fatal, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), __VA_ARGS__)
# define LOG_ERROR(fmt, ...) cppfd::Log::WriteLog(cppfd::ELogLevel_Error, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), __VA_ARGS__)
# define LOG_TRACE(fmt, ...) cppfd::Log::WriteLog(cppfd::ELogLevel_Info, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), __VA_ARGS__)
# define LOG_DEBUG(fmt, ...) cppfd::Log::WriteLog(cppfd::ELogLevel_Debug, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), __VA_ARGS__)
#else
# define LOG_WARNING(fmt, _arg...) cppfd::Log::WriteLog(cppfd::ELogLevel_Warning, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), ##_arg)
# define LOG_FATAL(fmt, _arg...) cppfd::Log::WriteLog(cppfd::ELogLevel_Fatal, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), ##_arg)
# define LOG_ERROR(fmt, _arg...) cppfd::Log::WriteLog(cppfd::ELogLevel_Error, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), ##_arg)
# define LOG_TRACE(fmt, _arg...) cppfd::Log::WriteLog(cppfd::ELogLevel_Info, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), ##_arg)
# define LOG_DEBUG(fmt, _arg...) cppfd::Log::WriteLog(cppfd::ELogLevel_Debug, "[%s:%d][fun:%s]" fmt, cppfd::Log::GetFileName(__FILE__), __LINE__, cppfd::Log::GetFuncName(__FUNCTION__), ##_arg)
#endif
日志可配置的信息,已经手写了反射,如果通过反射来读取配置文件,可以直接找到这个结构体的反射信息:
struct LogConfig {
String Path; //日志存放路径
String FileName; //文件名(前缀部分)
int LogLevel; //限制输出的日志等级 0 fatal, 1 error,2 warning,3 info, 4 debug,
uint32_t MaxSize; //单个文件的最大限制 MB
int FileCut; //日志的时间切分方式, 0 天, 1 小时,2分钟
uint32_t KeepDays; //日志保留的天数
String ProcessName; //进程名称,服务多个节点合并时,可以通过此区分
LogConfig() : Path("./log"), FileName("cppfd_log"), LogLevel(ELogLevel_Info), MaxSize(100), FileCut(ELogCut_Day), KeepDays(3) {}
};