Logger.hpp
// 头文件保护宏:防止头文件被重复包含,是C/C++的常规做法
#ifndef __LOGGER_HPP
#define __LOGGER_HPP
// 标准输入输出流:用于控制台打印
#include <iostream>
// C标准输入输出:用于snprintf等函数
#include <cstdio>
// 字符串处理:日志内容存储和拼接
#include <string>
// 智能指针:管理日志策略对象的生命周期,避免内存泄漏
#include <memory>
// 字符串流:高效拼接日志内容(比直接字符串+=更高效)
#include <sstream>
// 时间相关:基础时间函数
#include <ctime>
// 高精度时间:获取微秒级时间戳,提升日志时间精度
#include <sys/time.h>
// 获取进程ID:syscall相关,getpid()需要
#include <unistd.h>
// 文件系统操作:C++17标准,用于创建日志目录
#include <filesystem>
// 文件流:用于日志文件的写入
#include <fstream>
// 自定义互斥锁:用于保证日志输出的线程安全(需确保Mutex.hpp实现了线程安全的锁)
#include "Mutex.hpp"
// 日志模块命名空间:封装所有日志相关的类型和函数,避免命名冲突
namespace NS_LOG_MODULE
{
// 日志等级枚举:定义不同严重程度的日志类型,便于分类和筛选
enum class LogLevel
{
INFO, // 普通信息:程序正常运行的状态提示
WARNING, // 警告:非致命问题,需关注但不影响程序运行
ERROR, // 错误:功能异常,部分模块可能受影响
FATAL, // 致命错误:程序无法继续运行,需立即终止
DEBUG // 调试信息:开发阶段用,发布时可关闭
};
/**
* @brief 将日志等级枚举转换为对应的字符串,便于日志输出展示
* @param level 日志等级枚举值
* @return 对应等级的字符串(如LogLevel::INFO返回"INFO")
*/
std::string LogLevel2Message(LogLevel level)
{
switch (level)
{
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::DEBUG:
return "DEBUG";
default:
return "UNKNOWN";// 防御性编程:处理未知等级
}
}
/**
* @brief 获取当前系统的高精度时间(精确到微秒)
* @return 格式化后的时间字符串,格式:YYYY-MM-DD HH:MM:SS.微秒
* @note 使用localtime_r(可重入版本)而非localtime,避免线程安全问题
*/
std::string GetCurrentTime()
{
// 存储秒+微秒的高精度时间结构体
struct timeval current_time;
// 获取当前时间(秒+微秒),第二个参数为时区(nullptr表示本地时区)
int n = gettimeofday(¤t_time, nullptr);
(void)n;
// 分解时间为年/月/日/时/分/秒的结构体
struct tm struct_time;
// localtime_r是线程安全的,将秒数转换为本地时间结构体
localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
// 存储格式化后的时间字符串
char timestr[128];
// 格式化时间:年(+1900)、月(+1)、日、时、分、秒、微秒
snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
struct_time.tm_year + 1900, // tm_year是从1900年开始的偏移量
struct_time.tm_mon + 1, // tm_mon范围0-11,需+1
struct_time.tm_mday, // 日(1-31)
struct_time.tm_hour, // 时(0-23)
struct_time.tm_min, // 分(0-59)
struct_time.tm_sec, // 秒(0-59)
current_time.tv_usec); // 微秒(0-999999)
return timestr;
}
// ===================== 日志输出策略模式 =====================
// 设计思路:策略模式——将"日志输出行为"封装为独立的策略类,
// 可灵活切换(控制台/文件/按等级分文件),符合开闭原则
/**
* @brief 日志输出策略的抽象基类(策略接口)
* @note 所有具体输出策略(控制台、文件)都需继承此类并实现SyncLog
*/
class LogStrategy
{
public:
// 虚析构函数:确保子类析构时能正确调用自身的析构函数
virtual ~LogStrategy() = default;
/**
* @brief 同步输出日志的纯虚函数(核心接口)
* @param message 格式化后的完整日志字符串
*/
virtual void SyncLog(const std::string &message) = 0;
};
/**----------------------------------------------------------------------------
* @brief 控制台日志输出策略:将日志打印到控制台(stderr)
* @note 加互斥锁保证多线程下日志不被打乱
*/
class ConsoleStrategy : public LogStrategy
{
public:
/**
* @brief 实现控制台日志输出
* @param message 完整日志字符串
* @note 使用std::cerr而非std::cout:cerr无缓冲,实时输出;cout有缓冲可能延迟
*/
void SyncLog(const std::string &message) override
{
// 加锁:保证多线程下控制台输出的日志是完整的(避免不同线程日志混在一起)
LockGuard lockguard(_mutex);
std::cerr << message << std::endl; // ??
}
~ConsoleStrategy()
{
}
private:
Mutex _mutex;// 如果多个线程用的都是一个logger实例的函数,会导致并发问题,所以需要加锁
//如果多个线程分别用不同的logger实例的函数,那么需要加上 static,变成所有实例共用这一把锁
//static Mutex _mutex;
//但是在绝大多数的工程实践中,日志系统确实只需要一个实例(单例模式)就够了。
};
// 日志文件默认配置:默认日志目录、默认日志文件名
const std::string defaultpath = "./log"; // 默认日志存储目录
const std::string defaultfilename = "log.txt"; // 默认日志文件名
/**---------------------------------------------------------------------------------
* @brief 文件日志输出策略:将日志写入指定文件
* @note 1. 自动创建日志目录 2. 追加模式写入 3. 加锁保证线程安全
*/
class FileLogStrategy : public LogStrategy
{
public:
/**
* @brief 构造函数:初始化日志路径和文件名,自动创建日志目录
* @param path 日志存储目录(默认:./log)
* @param name 日志文件名(默认:log.txt)
*/
FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
: _logpath(path),
_logfilename(name)
{
// 加锁:保证多线程下目录创建操作的原子性
LockGuard lockguard(_mutex);
// 如果目录已存在,直接返回;否则创建目录
if (std::filesystem::exists(_logpath))
return;
try
{
// 创建目录(包括多级目录,如./log/2024/01)
std::filesystem::create_directories(_logpath);
}
catch (const std::filesystem::filesystem_error &e)
{
// 捕获目录创建失败的异常,打印错误信息
std::cerr << e.what() << '\n';
}
/*try-catch 只能捕获代码中**主动抛出(throw)**的错误,
或者标准库里定义好的错误。
它捕获不了真正的内存崩溃(Segfault)。*/
}
/**
* @brief 实现文件日志输出
* @param message 完整日志字符串
* @note 1. 追加模式打开文件(ios::app) 2. 每次写入后关闭文件(保证数据落盘)
*/
void SyncLog(const std::string &message) override
{
//为锁构建局部作用域{}
{
// 加锁:保证多线程下文件写入的原子性(避免文件内容错乱)
LockGuard lockguard(_mutex);
// 路径拼接修正:如果目录末尾没有/,补充/(如./log → ./log/)
if (!_logpath.empty() && _logpath.back() != '/')
{
_logpath += "/";
}
// 完整的日志文件路径(如./log/log.txt)
std::string targetlog = _logpath + _logfilename;
// 以追加模式打开文件(ios::app:写入到文件末尾)
std::ofstream out(targetlog, std::ios::app);
// 检查文件是否成功打开
if (!out.is_open())
{
std::cerr << "open " << targetlog << "failed" << std::endl;
return;
}
// 写入日志内容(换行分隔)
out << message << "\n";
// 关闭文件:确保数据立即落盘(避免缓冲区数据丢失)
out.close();
}
}
~FileLogStrategy()
{
}
//~FileLogStrategy() = default;也可这样写
private:
std::string _logpath; // 日志文件存储目录
std::string _logfilename; // 日志文件名
Mutex _mutex; // 文件写入的互斥锁
};
// 交给大家
// const std::string defaultfilename = "log.info";
// const std::string defaultfilename = "log.warning";
// const std::string defaultfilename = "log.fatal";
// const std::string defaultfilename = "log.error";
// const std::string defaultfilename = "log.debug";
// 文件策略&&分日志等级来进行保存
// class FileLogLevelStrategy : public LogStrategy
// {
// public:
// private:
// };
// ===================== 核心日志类 =====================
/**
* @brief 日志核心类:负责日志内容的构建和输出策略的管理
* @note 1. 组合日志输出策略(控制台/文件) 2. 提供RAII风格的日志构建方式
*/
class Logger
{
// 日志的生成:
// 构建日志字符串
public:
/**
* @brief 默认构造函数:默认使用控制台输出策略
*/
Logger()
{
UseConsoleStrategy();
}
/**
* @brief 切换为控制台输出策略
* @note 使用std::make_unique创建智能指针,自动管理内存
*/
void UseConsoleStrategy()
{
_strategy = std::make_unique<ConsoleStrategy>();
}
/**
* @brief 切换为文件输出策略
*/
void UseFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
/**
* @brief 日志消息内部类:RAII风格构建日志内容,标识一条完整的日志信息= 做半部分固定部分 + 右半部分不固定部分
* @note 1. 构造时拼接固定日志头(时间、等级、PID等) 2. 析构时自动输出日志 3. 支持<<流式拼接日志内容
*/
class LogMessage
{
public:
/**
* @brief 构造函数:初始化日志固定头信息
* @param level 日志等级
* @param filename 日志所在的源文件名(__FILE__宏)
* @param line 日志所在的行号(__LINE__宏)
* @param logger 外部Logger对象的引用(用于调用输出策略)
*/
LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
: _level(level),
_curr_time(GetCurrentTime()), // 当前高精度时间
_pid(getpid()), // 当前进程ID
_filename(filename), // 源文件名
_line(line), // 行号
_logger(logger) // 外部Logger引用
{
// 拼接日志固定头:[时间] [等级] [PID] [文件名] [行号] -
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2Message(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< " - ";
// 初始化日志内容为固定头
_loginfo = ss.str();
}
/**
* @brief 重载<<运算符:支持流式拼接日志内容
* @tparam T 任意可输出的类型(字符串、数字、对象等)
* @param info 要拼接的日志内容
* @return 当前LogMessage对象的引用(支持链式调用,如log << "a" << "b")
*/
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info; // 将任意类型转换为字符串
_loginfo += ss.str();// 拼接到日志内容中
return *this; // 返回自身,支持链式调用
}
/**
* @brief 析构函数:自动输出日志(RAII核心)
* @note 生命周期结束时自动调用策略的SyncLog输出日志,无需手动调用
*/
~LogMessage()
{
// 检查策略是否有效
if (_logger._strategy)
{
// 调用策略的输出函数
_logger._strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level; // 日志等级
std::string _curr_time; // 当前时间字符串
pid_t _pid; // 进程ID
std::string _filename; // 源文件名
int _line; // 行号
std::string _loginfo; // 完整的日志内容(固定头+自定义内容)
// 一个引用,引用外部的Logger类对象
Logger &_logger; // 方便我们后续进行策略式刷新
};
/**
* @brief 重载()运算符:创建LogMessage对象,触发日志构建
* @param level 日志等级
* @param filename 源文件名(__FILE__)
* @param line 行号(__LINE__)
* @return LogMessage对象(RAII风格,析构时自动输出)
*/
LogMessage operator()(LogLevel level, std::string filename, int line)
{
// 这里已经不是内部类了
// 故意采用拷贝LogMessage
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
// 日志输出策略的智能指针:自动管理策略对象的生命周期
std::unique_ptr<LogStrategy> _strategy; // 刷新策略
};
// 全局日志对象:整个程序共享,方便直接使用
Logger logger;
// 宏定义:快速切换输出策略(简化调用)
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
// 核心宏:简化日志调用(结合__FILE__/__LINE__自动填充文件名和行号)
// 使用示例:LOG(LogLevel::INFO) << "程序启动成功";
#define LOG(level) logger(level, __FILE__, __LINE__)
}
#endif // __LOGGER_HPP
__FILE__和__LINE__是 C/C++ 编译器内置的预定义宏(也叫编译器宏)
宏名 含义 类型 __FILE__被替换为当前源代码文件的路径字符串(如 /home/user/project/logger.cpp)字符串常量 __LINE__被替换为当前代码行的行号整数(比如你写
printf("%d", __LINE__);在第 20 行,就会替换成 20)
整数常量
Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include "Logger.hpp"
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Ptr()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard // RAII风格代码
{
public:
LockGuard(Mutex &lock):_lockref(lock)
{
_lockref.Lock();
}
~LockGuard()
{
_lockref.Unlock();
}
private:
Mutex &_lockref;
};
Main.cc
#include "Logger.hpp"
#include <thread>
#include <vector>
using namespace NS_LOG_MODULE;
void TestConsoleLog()
{
std::cout << "========= 测试控制台输出 =========" << std::endl;
// 默认是 ConsoleStrategy
ENABLE_CONSOLE_LOG_STRATEGY();
LOG(LogLevel::INFO) << "这是一条普通信息";
LOG(LogLevel::WARNING) << "这是一个警告, 错误码: " << 404;
LOG(LogLevel::DEBUG) << "调试信息: 变量 x = " << 3.14;
sleep(1); // 暂停一下,方便观察时间戳变化
}
void TestFileLog()
{
std::cout << "\n========= 测试文件输出 =========" << std::endl;
// 切换到文件策略
// 这会自动创建 ./log 目录和 log.txt 文件
ENABLE_FILE_LOG_STRATEGY();
std::cout << "策略已切换为文件,请查看 ./log/log.txt" << std::endl;
LOG(LogLevel::INFO) << "这条信息应该出现在文件中";
LOG(LogLevel::ERROR) << "数据库连接失败";
LOG(LogLevel::FATAL) << "系统即将崩溃!!";
// 模拟批量写入
for(int i = 0; i < 5; i++) {
LOG(LogLevel::DEBUG) << "文件日志计数: " << i;
}
}
// 模拟多线程写日志,测试线程安全
void ThreadTask(int id)
{
LOG(LogLevel::INFO) << "我是线程 " << id << ", 正在运行...";
}
void TestMultiThread()
{
std::cout << "\n========= 测试多线程并发写入 =========" << std::endl;
// 此时策略依然是文件策略
std::vector<std::thread> threads;
for(int i = 0; i < 5; i++)
{
threads.emplace_back(ThreadTask, i);
}
for(auto& t : threads)
{
t.join();
}
std::cout << "多线程测试完成,请检查日志文件是否有乱序或缺失。" << std::endl;
}
int main()
{
// 1. 测试屏幕打印
TestConsoleLog();
// 2. 测试文件写入
TestFileLog();
// 3. 测试多线程并发
TestMultiThread();
// 4. 切回控制台(可选,演示灵活性)
std::cout << "\n========= 切回控制台 =========" << std::endl;
ENABLE_CONSOLE_LOG_STRATEGY();
LOG(LogLevel::INFO) << "我又回到了屏幕上";
return 0;
}
Makefile
CXX = g++
# 定义编译选项
# -Wall: 开启警告
# -g: 生成调试信息
# -MMD: 关键参数!自动生成依赖文件(.d),但在生成的依赖中忽略系统头文件(<iostream>等)
# -MP: 为头文件生成伪目标,防止删除头文件后导致 make 报错
CXXFLAGS = -MMD
TARGET = main
SRCS = Main.cc
$(TARGET):$(SRCS)
$(CXX) $(SRCS) -o $(TARGET)
.PHONY:clean
clean:
rm -rf $(TARGET)
1万+



被折叠的 条评论
为什么被折叠?



