CLion实现log日志系统
日志存储:文本文件,文件后缀为 .log
日志内容:时间、级别、文件、行号、内容
日志级别:debug < info < warn < error < fatal
日志翻滚:当日志的大小超过设置值,那么日志内容将保存到新文件
注:生成的文件在cmake-build-debug
Logger.h
#pragma once
#include <string>
#include <fstream>
namespace yazi {namespace utility {#define debug(format,...) Logger::instance()->log(Logger::DEBUG,__FILE__,__LINE__,format,##__VA_ARGS__)#define info(format,...) Logger::instance()->log(Logger::INFO,__FILE__,__LINE__,format,##__VA_ARGS__)#define warn(format,...) Logger::instance()->log(Logger::WARN,__FILE__,__LINE__,format,##__VA_ARGS__)#define error(format,...) Logger::instance()->log(Logger::ERROR,__FILE__,__LINE__,format,##__VA_ARGS__)#define fatal(format,...) Logger::instance()->log(Logger::FATAL,__FILE__,__LINE__,format,##__VA_ARGS__)//__FILE__和__LINE__是编译器内置的预定义宏,用来获取当前正在被编译的源文件的名称和行号。//fatal(format,...) 支持两种调用方式,都没问题://无可变参数:fatal("纯文本日志");(靠 ## 消除逗号)//有可变参数:fatal("带占位符的日志:%d", 参数);(靠 ## 保留逗号)//这也是为什么 C++ 可变参数宏几乎都会用 ##__VA_ARGS__ 的原因 —— 兼容两种场景,避免语法错误。class Logger {public:enum Level{DEBUG=0, INFO, WARN, ERROR, FATAL,LEVEL_COUNT};//enum是枚举类型,给第一个设为0,其他依次递增。最后的LEVEL_COUNT值为5,刚好可以代表枚举类型的数量。static Logger * instance();//返回值是 Logger*(指向 Logger 类唯一实例的指针)。void open(const std::string & filename);//打开日志文件void close();//关闭日志文件void log(Level level,const char * file,int line,const char * format,...);//写日志void level(Level level) {//设置日志级别m_level = level;}void max(int bytes) {//设置日志文件最大字节数m_max = bytes;}private:Logger();//构造函数私有化,禁止外部创建实例~Logger();//析构函数私有化,禁止外部销毁实例void rotate();//日志轮转private:std::string m_filename;//日志文件名std::ofstream m_fout;//文件流Level m_level;//日志级别int m_max;//日志文件最大字节数int m_len;//当前日志文件大小static const char * s_level[LEVEL_COUNT];//日志级别字符串static Logger * m_instance;//声明静态实例指针};}
}
Logger.cpp
#include "Logger.h"#include <cstdarg>
#include <cstring>
#include <iostream>
using namespace yazi::utility;
#include<ctime>
const char * Logger::s_level[LEVEL_COUNT] = {"DEBUG","INFO","WARN","ERROR","FATAL"
};
//这行代码定义的 s_level 数组是枚举 Level 和字符串名称之间的 “映射表”,
//作用是将抽象的枚举值(整数)转换为人类可读的日志级别字符串(如 "DEBUG"),是日志格式化输出的关键。
Logger * Logger::m_instance = nullptr;//初始化静态指针为 nullptrLogger::Logger():m_level(DEBUG),m_max(0),m_len(0){}
Logger::~Logger() {close();
}
// 获取单例实例的函数
Logger * Logger::instance() {if(m_instance == nullptr) {// 第一次调用时,实例为空m_instance = new Logger();// 才创建实例}return m_instance;
}
/*
* 你的 Logger 类是懒汉式单例,实现步骤可概括为:1、私有化构造 / 析构函数,禁止外部创建实例;2、用静态指针 m_instance 存储唯一实例地址;3、初始化为 nullptr,实现 “按需创建”;4、通过 instance() 函数控制,确保只创建一次实例并返回。**/
void Logger::open(const std::string & filename) {m_filename = filename;//保存文件名m_fout.open(filename,std::ios::app);// 打开文件(追加模式)if(m_fout.fail()) {throw std::logic_error("open file failed " + filename);}m_fout.seekp(0,std::ios::end);//// 移动文件指针到末尾m_len = m_fout.tellp();// // 获取当前文件大小(字节数)//tellp():返回当前写指针的位置(距离文件开头的字节数),这个值刚好等于文件的当前大小(因为指针在末尾)
}void Logger::close() {m_fout.close();
}void Logger::log(Level level,const char * file,int line,const char *format,...) {if(m_level > level) {return;}if(m_fout.fail()) {throw std::logic_error("open file failed" + m_filename);}time_t ticks = time(nullptr);//获取当前系统时间(秒级,从1970-01-01 00:00:00开始计算)struct tm * ptm = localtime(&ticks);//把秒级时间转换为“本地时间结构体”(包含年、月、日、时、分、秒)//struct tm 是 “时间分解器”,把秒数转成年月日时分秒;char timestamp[32];memset(timestamp,0,sizeof(timestamp));strftime(timestamp,sizeof(timestamp),"%Y-%m-%d %H:%M:%S",ptm);//strftime 是 “时间格式化器”,按规则把 struct tm 转成字符串;const char * fmt = "%s %s %s:%d ";int size = snprintf(nullptr,0,fmt,timestamp,s_level[level],file,line);//不能直接用 level 代替 s_level[level],因为 level 是枚举值(整数),//而日志需要输出人类能看懂的级别名称(比如 "DEBUG"、"FATAL")//snprintf(目标缓冲区, 最大长度, 格式化字符串, 参数1, 参数2, ...);/*一、snprintf返回值:1、如果成功:返回「格式化后字符串的总长度」(不包含最后的 \0),不管是否写满缓冲区。2、如果失败:返回负数(比如格式化出错)。二、这行代码的特殊用法:nullptr + 0 组合当我们给 snprintf 传前两个参数为 nullptr(空指针,没有实际缓冲区)和 0(最大长度为 0)时,snprintf 会做两件事:1、不写入任何数据:因为没有缓冲区(nullptr),且最大长度为 0,无法存储任何字符。2、依然计算长度:按照 fmt 格式和后面的参数(timestamp、s_level[level] 等),计算出 “如果正常写入,最终字符串的总长度”,并把这个长度作为返回值返回。
*/if(size>0) {char * buffer = new char[size+1];snprintf(buffer,size+1,fmt,timestamp,s_level[level],file,line);buffer[size] = '\0';//std::cout<<buffer<<std::endl;m_fout<<buffer;m_len += size;delete [] buffer;}va_list arg_ptr;va_start(arg_ptr,format);size = vsnprintf(nullptr,0,format,arg_ptr);va_end(arg_ptr);/**1. va_list arg_ptr; —— 定义 “可变参数列表” 变量va_list 不是普通类型,而是 C 标准库定义的一个 “类型别名”(本质是一个指针或结构体),作用是 “存储可变参数的地址”,相当于一个 “容器”,用来装 ... 里的所有参数。arg_ptr 就是这个容器的名字,后续会通过它来访问 ... 中的参数。2. va_start(arg_ptr, format); —— 初始化容器,指向第一个可变参数作用:告诉编译器 “从哪个位置开始,后面的就是可变参数”,并让 arg_ptr 指向第一个可变参数的地址。关键:第二个参数 format 是 可变参数前面的最后一个固定参数(log 函数中,... 前面的固定参数依次是 level、file、line、format,最后一个固定参数就是 format)。编译器通过 format 的地址,就能找到它后面第一个可变参数的地址,然后把这个地址存到 arg_ptr 里。举个例子:如果调用 log(DEBUG, "main.cpp", 10, "用户 %s 年龄 %d", "张三", 25),那么:format 是 "用户 %s 年龄 %d"(固定参数的最后一个)。va_start 会让 arg_ptr 指向第一个可变参数 "张三" 的地址。3. size = vsnprintf(nullptr, 0, format, arg_ptr); —— 用可变参数计算字符串长度这行就是利用前面初始化好的 arg_ptr(装着可变参数的容器),调用 vsnprintf 计算 “格式化后的字符串长度”。细节:nullptr + 0:表示 “不实际存储字符串,只计算长度”(前面讲过的 vsnprintf 特殊用法)。format:用户传入的格式化字符串(比如 "用户 %s 年龄 %d")。arg_ptr:传给 vsnprintf,让它能读取到 ... 中的参数("张三"、25)。最终:size 会得到格式化后的字符串总长度(比如 "用户 张三 年龄 25" 的长度是 16)。4. va_end(arg_ptr); —— 释放容器,避免内存问题作用:“清理”arg_ptr 这个容器,告诉编译器 “可变参数已经处理完了”,释放相关的临时资源(比如避免野指针)。必须调用:这是 C 标准的要求,只要用了 va_start 初始化 va_list,就必须用 va_end 收尾,否则可能导致内存泄漏或程序异常。*/if(size>0) {char * content = new char[size+1];va_start(arg_ptr,format);vsnprintf(content,size+1,format,arg_ptr);va_end(arg_ptr);m_fout<<content;m_len += size;delete [] content;}m_fout<<std::endl;m_fout.flush();/**强制让 m_fout 把当前缓冲区中所有未写入文件的数据,立刻同步到磁盘文件,清空缓冲区。举个例子:如果你的日志是 debug("程序开始运行"),执行 m_fout << "程序开始运行" 后,数据可能还在内存缓冲区里;调用 flush() 后,这行日志才会真正写到 app.log(或你指定的日志文件)中。*///std::cout<<timestamp<<std::endl;if(m_max > 0 && m_len > m_max) {rotate();}
}
void Logger::rotate() {//轮转日志文件close();time_t ticks = time(nullptr);//获取当前系统时间(秒级,从1970-01-01 00:00:00开始计算)struct tm * ptm = localtime(&ticks);//把秒级时间转换为“本地时间结构体”(包含年、月、日、时、分、秒)//struct tm 是 “时间分解器”,把秒数转成年月日时分秒;char timestamp[32];//定义一个字符数组存储时间戳字符串memset(timestamp,0,sizeof(timestamp));//把数组初始化为全0(避免残留垃圾数据)strftime(timestamp,sizeof(timestamp),"%Y-%m-%d_%H-%M-%S",ptm);//格式化时间:按 "%Y-%m-%d_%H-%M-%S" 格式把时间结构体转成字符串//strftime 是 “时间格式化器”,按规则把 struct tm 转成字符串;std::string new_filename = m_filename + "." + timestamp;if(rename(m_filename.c_str(),new_filename.c_str())!=0) {//rename(old_name, new_name):C 标准库函数,用于重命名文件(把 old_name 改成 new_name)。//m_filename.c_str() → 把 C++ 字符串 m_filename 转成 C 风格字符串(rename 要求)throw std::logic_error("rename file failed: " + std::string(strerror(errno)));}open(m_filename);//重命名完成后,旧的 app.log 已经变成备份文件,需要重新创建一个新的 app.log 继续写日志;//调用 open(m_filename) 会以 “追加模式” 创建新的 app.log(如果文件不存在则创建,存在则继续追加),//并初始化 m_fout(文件流)和 m_len(文件大小,重置为 0)。}
main.cpp
#include <iostream>
#include "utility/Logger.h"
using namespace yazi::utility;
int main() {Logger::instance()->open("./test.log");Logger::instance()->max(1024);//Logger::instance()->level(Logger::DEBUG);Logger::instance()->log(Logger::DEBUG, __FILE__, __LINE__, "hello world");Logger::instance()->log(Logger::INFO, __FILE__, __LINE__, "name is %s,age is %d ", "yazi", 18);debug("name is %s,age is %d ", "slim", 19);info("name is %s,age is %d ", "tom", 20);warn("name is %s,age is %d ", "jack", 21);error("name is %s,age is %d ", "ciic", 22);fatal("name is %s,age is %d ", "yob", 23);return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.29)
project(Logger)set(CMAKE_CXX_STANDARD 20)add_executable(Logger main.cpputility/Logger.cpputility/Logger.h
)