当前位置: 首页 > news >正文

日志系统设计 与 策略模式实现

目录

一、设计模式概述

二、日志系统基础

1、日志系统的重要性

2、日志格式规范

必须包含的字段

可选字段

3、现有日志解决方案

三、策略模式详解

1、策略模式定义

2、在日志系统中的应用

3、预期日志格式

四、日志系统实现解析

1、头文件结构

2、策略模式实现

1. 策略基类

2. 具体策略实现

3、日志等级与时间戳处理

4、核心Logger类实现

5、全局访问与宏定义

6、注意:关于多态(Polymorphism)和智能指针(std::unique_ptr)的使用

1. 这份代码是否安全?

2. 对象截断(Object Slicing)何时发生?

3. 代码示例分析

4. 如何确保多态调用?

(1) 基类必须定义虚函数

(2) 派生类必须覆盖虚函数

(3) 通过基类指针调用

5. 总结

6. 额外建议

7、完整日志系统实现代码

五、线程安全实现

互斥锁封装

六、使用示例与分析

1、基本使用示例

2、输出结果分析

3、设计优势

七、改进与扩展建议

1、性能优化方向

2、功能扩展建议

八、总结


一、设计模式概述

        在IT行业快速发展的背景下,软件开发领域涌现出大量开发者,技术水平参差不齐。为了帮助初级开发者更好地理解和实现常见问题的解决方案,资深开发者总结了一系列经典的设计模式。

        设计模式是软件设计中常见问题的可重用解决方案,它们不是具体的代码实现,而是解决特定问题的模板或策略。设计模式主要分为三大类:

  • 创建型模式:关注对象的创建机制(如工厂模式、单例模式)

  • 结构型模式:处理对象间的组合关系(如适配器模式、装饰器模式)

  • 行为型模式:描述对象间的通信方式(如策略模式、观察者模式)

本案例将重点讲解策略模式在日志系统设计中的应用。


二、日志系统基础

1、日志系统的重要性

        计算机日志是记录系统和软件运行事件的专用文件,主要用于实时监控运行状态、追踪异常信息,便于快速定位故障并辅助程序员进行调试修复。作为系统维护的核心工具,日志在故障排查和安全管理中发挥着关键作用,其主要作用包括:

  • 运行监控:实时跟踪系统状态

  • 问题诊断:记录异常信息帮助快速定位问题

  • 安全审计:追踪安全相关事件

  • 性能分析:记录操作耗时等性能数据

2、日志格式规范

必须包含的字段

  • 时间戳:精确到秒的事件发生时间(推荐格式:YYYY-MM-DD HH:MM:SS)

  • 日志等级:表示日志严重程度(DEBUG/INFO/WARNING/ERROR/FATAL)

  • 日志内容:实际记录的信息

可选字段

  • 源代码位置:文件名和行号(FILELINE

  • 进程/线程信息:进程ID、线程ID

  • 模块/功能标识:区分不同业务模块

  • 会话/请求ID:追踪特定请求链路

3、现有日志解决方案

业界已有多种成熟的日志库:

  • C++: spdlog, glog, Boost.Log, Log4cxx

  • Java: Log4j, Logback, java.util.logging

  • Python: logging模块

本案例选择自定义实现,主要为了:

  • 第一,深入理解日志系统内部机制

  • 第二,演示策略模式的具体应用

  • 第三,根据特定需求定制功能


三、策略模式详解

1、策略模式定义

        策略模式(Strategy Pattern)是一种行为设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户端。

核心思想:

  • 定义算法家族(抽象策略接口)

  • 封装每个算法(具体策略实现)

  • 客户端通过组合方式使用策略(上下文类)

2、在日志系统中的应用

在日志系统中,不同的日志输出方式(控制台、文件、网络等)可以视为不同的策略:

// 策略接口
class LogStrategy {
public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;
};// 具体策略:控制台输出
class ConsoleLogStrategy : public LogStrategy {// 实现细节...
};// 具体策略:文件输出
class FileLogStrategy : public LogStrategy {// 实现细节...
};

3、预期日志格式

为优化日志设计,我们采用了策略模式。关于策略模式的详细介绍,请参考相关代码和博客内容。我们期望的日志格式如下:

[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world

四、日志系统实现解析

1、头文件结构

#ifndef __LOG_HPP__
#define __LOG_HPP__// 系统头文件
#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> // C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>// 自定义模块
#include "Mutex.hpp"namespace LogModule {// 实现细节...
}#endif

2、策略模式实现

1. 策略基类

class LogStrategy {
public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;
};

这段代码定义了一个名为 LogStrategy 的抽象基类(Abstract Base Class),用于实现策略模式(Strategy Pattern)中的策略接口。

1. 类定义与目的

class LogStrategy {
public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;
};
  • 作用:定义日志记录的策略接口,允许不同的日志记录方式(如文件日志、控制台日志、网络日志等)通过继承该类实现多态行为。

  • 设计模式:这是策略模式的典型应用,将算法(这里是日志记录方式)封装为对象,便于动态切换。

2. 成员函数解析

(1) 虚析构函数

virtual ~LogStrategy() = default;

关键点

  • 声明为 virtual,确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数(避免资源泄漏)。

  • = default 表示使用编译器生成的默认析构函数(简洁且高效)。

为什么需要:如果基类析构函数非虚,通过基类指针 delete 派生类对象时,只会调用基类的析构函数,导致派生类部分未被正确释放。(重点!!!)

(2) 纯虚函数 SyncLog

virtual void SyncLog(const std::string &message) = 0;

关键点

  • = 0 表示这是一个纯虚函数,LogStrategy 成为抽象类,不能直接实例化。

  • 派生类必须实现此函数,提供具体的日志记录逻辑。

参数:接收一个 const std::string& 类型的消息,表示待记录的日志内容。

设计意图:强制所有派生策略实现统一的日志接口,但内部行为可以不同(如写入文件、发送到服务器等)。

2. 具体策略实现

控制台日志策略

class ConsoleLogStrategy : public LogStrategy {
public:ConsoleLogStrategy() = default;void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex); // 线程安全std::cout << message << gsep;}private:Mutex _mutex; // 互斥锁保证线程安全
};

这段代码定义了一个具体的日志策略类 ConsoleLogStrategy,继承自 LogStrategy,用于将日志输出到控制台。

1. 类定义与继承关系

class ConsoleLogStrategy : public LogStrategy {
public:ConsoleLogStrategy() = default;void SyncLog(const std::string &message) override;
private:Mutex _mutex;
};
  • 继承:通过 public LogStrategy 继承基类的纯虚接口,表明这是一个具体的策略实现。

  • 构造函数= default 表示使用编译器生成的默认构造函数,无需额外初始化逻辑。

  • 线程安全:通过 Mutex _mutex 和 LockGuard 实现多线程环境下的安全输出。

2. 关键实现:SyncLog 方法

void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex); // 加锁std::cout << message << gsep; // 输出日志
}

(1) 线程安全设计

  • Mutex _mutex:成员变量,用于保护共享资源(这里是 std::cout)。

  • LockGuard(我们可以想象是 std::lock_guard<Mutex> 的别名,但是我这里使用的是自己已经封装好了的Mutex.hpp):

    • 在构造时自动加锁(_mutex.lock())。

    • 在作用域结束时(如函数退出)自动解锁(通过 RAII 机制)。

    • 避免手动管理锁导致的死锁或忘记解锁问题。

(2) 日志输出

  • std::cout:标准输出流,将日志打印到控制台。

  • gsep(在代码中已经定义,是全局变量):

    • 为分隔符(如"\r\n"),用于格式化日志。

    • 也可以改为局部常量或通过参数配置,减少全局状态依赖。

文件日志策略

// 文件打印日志的策略 : 子类
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";class FileLogStrategy : public LogStrategy {
public:// 构造函数FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path), _file(file) {// 初始化日志目录if (!std::filesystem::exists(_path)) {try {std::filesystem::create_directories(_path);} catch (const std::filesystem::filesystem_error &e) {std::cerr << e.what() << '\n';}}}// 实现基类的纯虚函数void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex);std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;std::ofstream out(filename, std::ios::app); // 追加模式if (out.is_open()) {out << message << gsep;out.close();}}private:std::string _path;  // 日志文件所在路径std::string _file;  // 日志文件名Mutex _mutex;       // 互斥锁,保证线程安全
};

FileLogStrategy 是日志模块中实现文件日志策略的类,继承自 LogStrategy 基类。

成员变量

  • _path: 存储日志文件的目录路径:默认值为 "./log"(由 defaultpath 常量定义)

  • _file: 存储日志文件名:默认值为 "my.log"(由 defaultfile 常量定义)

  • _mutex: 互斥锁对象:用于保证多线程环境下文件写入的线程安全

构造函数

FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path), _file(file) {// 初始化日志目录if (!std::filesystem::exists(_path)) {try {std::filesystem::create_directories(_path);} catch (const std::filesystem::filesystem_error &e) {std::cerr << e.what() << '\n';}}
}

参数:

  • path: 日志目录路径(可选,默认 defaultpath

  • file: 日志文件名(可选,默认 defaultfile

功能:

  • 初始化成员变量 _path 和 _file

  • 检查日志目录是否存在,如果不存在则尝试创建

  • 使用 try-catch 捕获可能的文件系统异常并输出错误信息

构造函数这里使用了 C++17 引入的 <filesystem> 库来处理文件系统操作。下面讲解std::filesystem 的相关功能以及这段代码的作用:

1. std::filesystem 简介

    std::filesystem 是 C++17 标准中引入的库,用于处理文件和目录路径、遍历文件系统、查询文件信息等操作。它提供了跨平台的文件系统操作接口,替代了之前的平台相关 API(如 Windows 的 <windows.h> 或 POSIX 的 <dirent.h>)。

2. 代码功能解析

这段代码是一个构造函数,用于初始化一个文件日志策略类 FileLogStrategy,主要功能是确保日志目录存在:

FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path), _file(file) {  // 初始化成员变量// 检查目录是否存在if (!std::filesystem::exists(_path)) {  // 如果路径不存在try {// 创建目录(包括所有必要的父目录)std::filesystem::create_directories(_path);} catch (const std::filesystem::filesystem_error &e) {// 捕获并处理文件系统错误std::cerr << e.what() << '\n';}}
}

3. 关键函数说明

(1) std::filesystem::exists()

  • 作用:检查给定路径是否存在(文件或目录)

  • 参数:接受一个路径对象(可以是 std::stringstd::filesystem::path 或字符数组)

  • 返回值bool,存在返回 true,否则 false

  • 示例

    bool exists = std::filesystem::exists("/path/to/check");

(2) std::filesystem::create_directories()

  • 作用:创建目录及其所有不存在的父目录(类似 mkdir -p

  • 参数:路径对象

  • 异常:如果创建失败会抛出 std::filesystem::filesystem_error

  • 与 create_directory() 的区别

    • create_directory() 只能创建单级目录,父目录必须已存在

    • create_directories() 可以创建多级目录

  • 示例

    std::filesystem::create_directories("path/to/log/dir");

(3) std::filesystem::filesystem_error

  • 作用:文件系统操作失败时抛出的异常类型

  • 常用方法

    • what():返回错误描述字符串

    • path1():返回第一个相关路径

    • path2():返回第二个相关路径(如果有)

    • code():返回 std::error_code 对象

4. 完整工作流程

  1. 构造函数接收日志路径 _path 和文件名 _file

  2. 使用 exists() 检查路径是否存在

  3. 如果不存在,尝试用 create_directories() 创建目录

  4. 如果创建过程中出错(如权限不足),捕获异常并输出错误信息

5. 现代 C++ 改进建议

  • 可以使用 std::filesystem::path 类型代替 std::string 表示路径

  • C++17 允许使用结构化绑定处理异常:

    catch (const std::filesystem::filesystem_error& e) {auto [ec, msg] = std::make_pair(e.code(), e.what());std::cerr << "Error " << ec << ": " << msg << '\n';
    }

6. 编译注意事项

使用 std::filesystem 需要:

  • C++17 或更高版本支持(编译选项如 -std=c++17

  • 链接文件系统库(如 g++ 需要 -lstdc++fs,但较新版本已集成)

这个构造函数展示了文件系统库的典型用法:路径检查 + 目录创建 + 错误处理,是日志系统初始化的常见模式。

SyncLog 方法

void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex);std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;std::ofstream out(filename, std::ios::app); // 追加模式if (out.is_open()) {out << message << gsep;out.close();}
}

参数:message - 要写入的日志消息

功能:

  • 使用 LockGuard 获取互斥锁,保证线程安全

  • 构造完整的日志文件路径:

    • 如果路径已以 '/' 结尾,直接拼接文件名

    • 否则添加 '/' 后再拼接文件名

  • 以追加模式 (std::ios::app) 打开文件流

  • 如果文件成功打开:

    • 写入日志消息和分隔符 (gsep,定义为 "\r\n")

    • 关闭文件流

        这个函数实现了一个线程安全的日志同步写入功能,使用了 std::ofstream 进行文件操作。下面讲解 std::ofstream 的相关用法以及整个函数的逻辑。

1. std::ofstream 详解

    std::ofstream 是 C++ 标准库 <fstream> 提供的输出文件流类,用于向文件写入数据。它是 std::ostream 的派生类,支持 << 操作符,类似于 std::cout,但写入文件而非控制台。

(1) 构造函数

std::ofstream out(filename, std::ios::app);
  • 作用:打开文件 filename,准备写入。

  • 参数

    • filename:文件路径(std::string 或 const char*)。

    • std::ios::app:打开模式,表示追加模式(append),新数据写入文件末尾,不会覆盖原有内容。

  • 其他常见打开模式(可组合使用):

    • std::ios::out:默认模式,写入文件(覆盖原有内容)。

    • std::ios::binary:二进制模式。

    • std::ios::trunc:如果文件存在,清空内容(默认行为,除非指定 app 或 ate)。

    • std::ios::ate:打开后定位到文件末尾(但写入时仍可移动指针)。

(2) is_open()

if (out.is_open())
  • 作用:检查文件是否成功打开。

  • 返回值bool,成功打开返回 true,否则 false(如权限不足、路径不存在等)。

(3) << 操作符

out << message << gsep;
  • 作用:向文件写入数据,类似于 std::cout

  • 支持类型:字符串、数字、自定义类型(需重载 <<)。

(4) close()

out.close();
  • 作用:关闭文件流,释放资源。

  • 注意:析构时会自动调用,但显式关闭是良好实践。

2. 代码逻辑解析

void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex);  // 1. 加锁,保证线程安全// 2. 构造完整文件路径(确保路径以 '/' 结尾)std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;// 3. 以追加模式打开文件std::ofstream out(filename, std::ios::app);// 4. 检查文件是否成功打开if (out.is_open()) {out << message << gsep;  // 5. 写入日志消息和分隔符out.close();             // 6. 关闭文件}
}

(1) 线程安全

LockGuard lockguard(_mutex);
  • 使用 LockGuard(可能是 std::lock_guard<std::mutex> 的封装)保护共享资源(日志文件),防止多线程同时写入导致数据混乱。

(2) 路径拼接

std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;

确保路径格式正确:

  • 如果 _path 已经以 / 结尾(如 "/var/log/"),直接拼接 _file

  • 否则,补上 /(如 _path="logs"_file="app.log" → "logs/app.log")。

(3) 文件打开模式

std::ofstream out(filename, std::ios::app);

std::ios::app

  • 每次写入都在文件末尾追加,不会覆盖原有内容。

  • 适合日志场景,避免丢失历史记录。

(4) 写入日志

out << message << gsep;
  • message:要写入的日志内容。

  • gsep:可能是全局定义的日志分隔符(如 "\n" 或 "\r\n")。

(5) 关闭文件

out.close();
  • 显式关闭文件,确保数据刷新到磁盘(虽然析构时也会自动关闭)。

特点分析

  • 线程安全

    • 使用 Mutex 和 LockGuard 确保多线程环境下文件写入的原子性

    • 每个文件操作都在互斥锁保护下进行

  • 路径处理

    • 智能处理路径结尾的斜杠,确保路径拼接正确

    • 使用 C++17 的 std::filesystem 进行目录操作

  • 文件操作

    • 使用追加模式 (std::ios::app) 打开文件,确保新日志追加到文件末尾

    • 检查文件是否成功打开后再进行写入

  • 错误处理

    • 捕获并处理目录创建可能抛出的异常

    • 静默处理文件打开失败的情况(仅不写入,不抛出异常)

3、日志等级与时间戳处理

// 日志等级枚举
enum class LogLevel {DEBUG,INFO,WARNING,ERROR,FATAL
};// 日志等级转字符串
std::string Level2Str(LogLevel level) {switch (level) {case LogLevel::DEBUG: return "DEBUG";case LogLevel::INFO: return "INFO";case LogLevel::WARNING: return "WARNING";case LogLevel::ERROR: return "ERROR";case LogLevel::FATAL: return "FATAL";default: return "UNKNOWN";}
}// 获取格式化时间戳
std::string GetTimeStamp() {time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm); // 线程安全的时间转换char timebuffer[128];snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year+1900,curr_tm.tm_mon+1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuffer;
}

        这段代码实现了一个线程安全的日志系统,支持控制台输出文件写入两种策略,并采用了策略模式设计。下面我将从日志等级、时间戳、策略模式、线程安全、宏定义等方面详细解析。

1. 日志等级 (LogLevel)

enum class LogLevel {DEBUG,INFO,WARNING,ERROR,FATAL
};
  • 作用:定义日志的级别,用于区分日志的重要性。

  • 优点

    • enum class 避免命名冲突(强类型枚举)。

    • 扩展性强,可轻松添加新级别。

枚举值日志等级典型用途
DEBUG调试级用于开发调试的详细信息,通常在生产环境中关闭。
INFO信息级记录程序正常运行时的关键信息(如服务启动、配置加载等)。
WARNING警告级表示潜在问题,程序仍能运行但可能需要关注(如磁盘空间不足)。
ERROR错误级记录影响部分功能的错误,但程序仍可继续运行(如请求参数无效)。
FATAL致命级导致程序无法继续运行的严重错误(如数据库连接失败),通常触发进程终止。

补充说明:

  • 严重程度递增DEBUG → INFO → WARNING → ERROR → FATAL(从低到高,通常高严重等级会触发通知机制)

  • 生产环境建议

    • 开发环境:可开启 DEBUG

    • 生产环境:通常只记录 INFO 及以上等级(WARNING/ERROR/FATAL

  • 常见应用场景

    • FATAL 错误可能伴随日志文件输出和系统告警。

    • DEBUG 日志可能包含敏感信息,需谨慎使用。

        在C++中,enum class(也称为强类型枚举作用域枚举)是C++11引入的一种改进的枚举类型定义方式。相比于传统的C风格枚举(enum),enum class具有以下优势和特性:

1. 作用域隔离(Scoped Enumeration)

  • 传统enum的问题:枚举值会暴露在外部作用域中,可能导致命名冲突。

    enum LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };
    enum Color { RED, GREEN, BLUE }; // 错误:DEBUG与RED冲突(如果值相同)
  • enum class的解决方案:枚举值的作用域限定在枚举类内部,必须通过枚举类型名访问。

    enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };
    enum class Color { RED, GREEN, BLUE };
    LogLevel level = LogLevel::DEBUG; // 正确
    Color c = Color::RED;            // 正确

2. 强类型(Strong Typing)

  • 传统enum的问题:枚举值可以隐式转换为整数(如int),可能导致误用。

    enum LogLevel { DEBUG, INFO };
    int x = DEBUG; // 隐式转换为int,可能引发逻辑错误
  • enum class的解决方案:枚举值不会隐式转换为整数,必须显式转换。

    enum class LogLevel { DEBUG, INFO };
    // int x = LogLevel::DEBUG; // 错误:不能隐式转换
    int x = static_cast<int>(LogLevel::DEBUG); // 必须显式转换

3. 避免命名污染

  • 传统枚举的枚举值(如DEBUG)可能与其他全局标识符冲突,而enum class的枚举值必须通过EnumType::Value访问,避免了污染外部命名空间。

4. 可以指定底层类型

  • enum class允许显式指定枚举的底层整数类型(默认为int),节省内存或匹配接口要求。

    enum class LogLevel : uint8_t { DEBUG, INFO, WARNING, ERROR, FATAL };
    // 每个枚举值仅占用1字节

代码示例解析

enum class LogLevel {DEBUG,INFO,WARNING,ERROR,FATAL
};
  • 含义:定义了一个强类型枚举LogLevel,包含5个枚举值,分别表示日志级别。

  • 用途

    • 在日志系统中限制日志级别的取值范围。

    • 避免直接使用整数或字符串表示级别,提高代码可读性和类型安全性。

    • 例如:

      void log(LogLevel level, const std::string& message) {if (level == LogLevel::FATAL) {// 处理严重错误}
      }

对比总结

特性传统enumenum class
作用域全局枚举类内部
类型安全弱(可隐式转换)强(需显式转换)
命名冲突风险
底层类型指定不支持支持(如: uint8_t

何时使用?

  • 优先使用enum class:需要类型安全、避免命名冲突或明确底层类型时。

  • 传统enum:需要与C代码交互或兼容旧代码时(但现代C++应尽量避免)。

通过enum class,C++提供了更安全、更清晰的枚举类型,是推荐的做法。

日志等级转字符串 (Level2Str)

std::string Level2Str(LogLevel level) {switch (level) {case LogLevel::DEBUG: return "DEBUG";case LogLevel::INFO: return "INFO";case LogLevel::WARNING: return "WARNING";case LogLevel::ERROR: return "ERROR";case LogLevel::FATAL: return "FATAL";default: return "UNKNOWN";}
}
  • 作用:将枚举值转换为可读的字符串,便于日志输出。

  • 示例

    LogLevel level = LogLevel::ERROR;
    std::cout << Level2Str(level); // 输出 "ERROR"

2. 时间戳 (GetTimeStamp)

std::string GetTimeStamp() {time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm); // 线程安全的时间转换char timebuffer[128];snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year+1900,curr_tm.tm_mon+1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuffer;
}
  • 作用:获取当前时间,格式化为 YYYY-MM-DD HH:MM:SS

  • 关键点

    • localtime_r 是线程安全的(localtime 是非线程安全的)。

    • tm_year 是从 1900 开始的年数,需 +1900

    • tm_mon 是 0~11,需 +1

  • 输出示例:2023-10-05 14:30:45

        这段代码定义了一个函数 GetTimeStamp(),用于获取当前系统时间并格式化为易读的字符串(如 "2023-10-05 14:30:45")。以下是逐行解析,重点讲解涉及时间的函数和用法:

1. 获取当前时间戳

time_t curr = time(nullptr);

time() 函数

  • 作用:获取当前系统的日历时间(Calendar Time),即从 1970年1月1日00:00:00 UTC(Unix 纪元)开始的秒数。

  • 参数nullptr 表示不存储结果,直接返回时间值。

  • 返回值time_t 类型(通常是整数或浮点数,表示秒数)。

  • 示例:如果当前时间是 1970年1月1日00:00:01 UTC,time() 返回 1

2. 转换为本地时间结构体

struct tm curr_tm;
localtime_r(&curr, &curr_tm); // 线程安全的时间转换

struct tm

  • 是一个结构体,用于存储分解时间(Broken-down Time),包含年、月、日、时、分、秒等字段:

    struct tm {int tm_sec;   // 秒 [0, 60](含闰秒)int tm_min;   // 分 [0, 59]int tm_hour;  // 时 [0, 23]int tm_mday;  // 日 [1, 31]int tm_mon;   // 月 [0, 11](0表示1月!)int tm_year;  // 年(实际年份 - 1900)// 其他字段(如星期、夏令时标志等)
    };
  • 注意tm_year 是从 1900 年开始的偏移量,tm_mon 是 0-based(0=1月)。

localtime_r() 函数

  • 作用:将 time_t 转换为本地时间的 struct tm线程安全_r 表示 reentrant,可重入)。

  • 对比非线程安全版本

    struct tm* localtime(const time_t* timer); // 返回静态内存,多线程不安全!
  • 参数

    • &curr:输入的 time_t 时间戳。

    • &curr_tm:输出的 struct tm 结构体。

下翻手册,我们可以看到struct tm 结构体的定义:

3. 格式化时间字符串

char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year + 1900, // 年份需要 +1900curr_tm.tm_mon + 1,     // 月份需要 +1(0=1月)curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);

snprintf() 函数

  • 作用:将格式化数据写入字符缓冲区,避免缓冲区溢出(比 sprintf() 更安全)。

  • 格式化字符串

    • %4d:年份(4位宽度,如 2023)。

    • %02d:月份/日期/时间(2位宽度,不足补零,如 05)。

  • 字段调整

    • tm_year + 1900(因为tm_year 是从 1900 年开始的偏移量:将 tm_year(如 123)转换为实际年份(如 2023)。

    • tm_mon + 1(tm_mon 是 0-based(0=1月):将 tm_mon(0-based)转换为自然月(1-12)。

4. 返回字符串

return timebuffer;
  • 将格式化后的字符串(如 "2023-10-05 14:30:45")返回。

完整代码逻辑总结

  • 获取时间戳time(nullptr) 返回当前时间的 time_t 值。

  • 转换为本地时间localtime_r() 将 time_t 分解为年、月、日等字段。

  • 格式化字符串:将 struct tm 的字段拼接为 YYYY-MM-DD HH:MM:SS 格式。

  • 返回结果:返回格式化后的字符串。

4、核心Logger类实现

class Logger {
public:Logger() {EnableConsoleLogStrategy(); // 默认使用控制台策略}// 策略切换方法void EnableFileLogStrategy() {_fflush_strategy = std::make_unique<FileLogStrategy>();}void EnableConsoleLogStrategy() {_fflush_strategy = std::make_unique<ConsoleLogStrategy>();}// 日志消息类(内部类)class LogMessage {public:LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger) {// 构建日志前缀std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();}// 流式输出支持template <typename T>LogMessage &operator<<(const T &info) {std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}// 析构时自动输出日志~LogMessage() {if (_logger._fflush_strategy) {_logger._fflush_strategy->SyncLog(_loginfo);}}private:std::string _curr_time;LogLevel _level;pid_t _pid;std::string _src_name;int _line_number;std::string _loginfo;Logger &_logger;};// 日志入口操作符LogMessage operator()(LogLevel level, std::string name, int line) {return LogMessage(level, name, line, *this);}private:std::unique_ptr<LogStrategy> _fflush_strategy; // 当前日志策略
};

Logger 类是日志系统的核心类,负责协调日志的生成和输出策略。下面从多个方面详细解析这个类的设计:

1. 类结构概述

Logger 类采用了策略模式,将日志输出的具体实现与日志生成逻辑分离。主要包含:

  • 策略管理(切换控制台/文件输出)

  • 日志消息生成(内部类 LogMessage

  • 流式输出支持(通过 operator<<

2. 构造函数与默认行为

Logger() {EnableConsoleLogStrategy(); // 默认使用控制台策略
}
  • 构造函数中默认启用控制台日志策略

  • 这种设计确保即使不显式配置,日志系统也能正常工作

3. 策略管理方法

void EnableFileLogStrategy() {_fflush_strategy = std::make_unique<FileLogStrategy>();
}void EnableConsoleLogStrategy() {_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
  • 使用 std::unique_ptr 管理策略对象,确保自动内存管理

  • 通过这两个方法可以动态切换日志输出目标

  • 体现了策略模式的核心思想:运行时切换算法

4. 内部类 LogMessage

这是日志系统的关键部分,负责:

  1. 构建完整的日志消息

  2. 支持流式输出

  3. 在析构时自动输出日志

4.1 构造函数

LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger) {// 构建日志前缀std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();
}
  • 收集日志所需的所有元信息:时间戳、日志级别、进程ID、源文件名、行号

  • 构建格式化的日志前缀,例如:[2023-05-20 14:30:45] [INFO] [1234] [main.cpp] [42] -

        这段代码定义了一个构造函数,用于初始化日志消息对象(可能是 LogMessage 类)。它使用了 std::stringstream 来构建日志前缀,并通过 Logger &logger 参数关联日志记录器。

1. 构造函数参数分析

LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger) { ... }

(1) 参数说明

参数

类型

作用

level

LogLevel &

日志级别(如 DEBUG、INFO、ERROR),用于分类日志重要性。

src_name

std::string &

源文件名或模块名,标识日志来源。

line_number

int

代码行号,用于精确定位日志位置。

logger

Logger &

日志记录器对象的引用,用于后续实际输出日志。

(2) 成员变量初始化

  • _curr_time(GetTimeStamp()):调用 GetTimeStamp() 函数获取当前时间戳(可能是字符串或时间对象),记录日志时间。

  • _level(level):保存日志级别,后续可能用于过滤或格式化。

  • _pid(getpid()):调用系统函数 getpid() 获取进程 ID,用于区分多进程日志。

  • _src_name(src_name):保存源文件名或模块名。

  • _line_number(line_number):保存代码行号。

  • _logger(logger):保存日志记录器的引用,后续通过 _logger 输出日志。

2. std::stringstream 的作用

std::stringstream ss;
ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";
_loginfo = ss.str();

(1) 功能

  • 构建日志前缀:将日志的元信息(时间、级别、进程 ID、源文件、行号)格式化为字符串,存储到成员变量 _loginfo 中。

  • 示例输出[2023-01-01 12:00:00] [INFO] [1234] [main.cpp] [42] - This is a log message.

(2) 关键操作

  • std::stringstream:C++ 标准库中的字符串流,支持流式操作(<<),用于高效拼接字符串。

  • Level2Str(_level):假设是将 LogLevel 枚举转换为字符串(如 DEBUG → "DEBUG")。

  • ss.str():将流内容转换为 std::string,赋值给 _loginfo

(3) 优势

  • 可读性:结构化前缀使日志易于阅读和分析。

  • 灵活性:可轻松调整格式(如添加/删除字段)。

  • 性能stringstream 比多次字符串拼接更高效。

3. Logger &logger 的作用和意义

(1) 为什么传递 Logger 引用?

  • 解耦LogMessage 只负责构建日志内容,不关心具体输出方式(文件、控制台、网络等)。实际输出由 Logger 实现,遵循单一职责原则

  • 灵活性:可通过不同 Logger 实现切换日志行为(如 FileLoggerConsoleLogger)。

  • 避免拷贝:传递引用避免 Logger 对象的拷贝开销。

(2) 典型用法

后续可能通过 _logger 输出完整日志:

void LogMessage::Output(const std::string &message) {_logger.Log(_loginfo + message); // 组合前缀和实际消息
}

(3) 设计意义

  • 策略模式Logger 是日志输出策略,LogMessage 是日志内容构建器,二者分离。

  • 可扩展性:新增日志输出方式只需实现新的 Logger 子类,无需修改 LogMessage

4.2 流式输出支持

template <typename T>
LogMessage &operator<<(const T &info) {std::stringstream ss;ss << info;_loginfo += ss.str();return *this;
}
  • 支持链式调用,如 LOG(INFO) << "Message" << 123 << 3.14

  • 使用模板支持任意类型的输出

  • 将输入内容转换为字符串(ss.str())并追加到日志消息中

4.3 自动输出机制

~LogMessage() {if (_logger._fflush_strategy) {_logger._fflush_strategy->SyncLog(_loginfo);}
}
  • 利用RAII机制,在对象析构时自动输出日志

  • 确保即使发生异常也能记录日志

  • 检查策略对象(可以通过Logger构造选择哪个策略)是否存在,避免空指针解引用

5. 日志入口操作符

LogMessage operator()(LogLevel level, std::string name, int line) {return LogMessage(level, name, line, *this);
}

        这段代码定义了一个函数调用运算符重载operator()),用于快速创建 LogMessage 对象。它通常出现在 Logger 类中,目的是提供一种简洁的语法来生成日志消息,重载函数调用操作符,提供简洁的API。

作用

  • 这是一个运算符重载,允许 Logger 对象像函数一样被调用(即 Logger() 语法)。

  • 它构造并返回一个 LogMessage 对象,将当前 Logger 对象(*this)传递给 LogMessage 的构造函数。

  • 返回 LogMessage 临时对象,支持流式输出。

参数:

  • level: 日志级别

  • name: 源文件名(通常通过宏传递 __FILE__

  • line: 行号(通常通过宏传递 __LINE__

典型使用场景

假设在 Logger 类中定义了此运算符,用户可以这样使用:

Logger logger; // 假设已初始化
logger(LogLevel::INFO, "main.cpp", 42) << "This is a log message.";

这行代码会:

  1. 调用 Logger::operator(),生成一个临时的 LogMessage 对象。

  2. 通过 LogMessage 的流式操作(如 operator<<)追加日志内容。

  3. 在 LogMessage 析构时(如语句结束时),自动将日志输出到 logger

临时对象的析构时机:

  • 临时对象会在这个完整表达式结束时被析构。

  • 在 logger(...) << "message" 中:

    • logger(...) 返回 LogMessage 临时对象。

    • << "message" 调用 LogMessage::operator<<,返回 LogMessage&(通常是 *this)。

    • 完整表达式是 logger(...) << "message",因此临时 LogMessage 会在整个语句结束时(即 ; 处)被析构。

参数与返回值

(1) 参数

参数

类型

作用

level

LogLevel

日志级别(如 INFO、ERROR)。

name

std::string

源文件名或模块名。

line

int

代码行号。

(2) 返回值

  • 返回一个 LogMessage 对象(通常是临时对象,支持链式调用)。

  • LogMessage 的构造函数接受 *this(当前 Logger 对象的引用),用于后续日志输出。

5、全局访问与宏定义

// 全局日志对象
Logger logger;// 简化使用的宏定义
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()

这段代码定义了一个全局日志对象 logger,并通过宏简化日志调用和日志策略配置。以下是详细解析:

1. 全局日志对象

Logger logger;
  • 作用:声明一个全局的 Logger 实例,供整个程序使用。

  • 特点

    • 全局可见,任何地方都可以直接使用 logger 记录日志。

    • 通常在程序启动时初始化(如 main() 开头),并在程序结束时自动析构。

  • 潜在问题

    • 全局变量可能导致依赖耦合,不利于单元测试。

    • 如果程序是多线程的,需要确保 Logger 的线程安全(如加锁或使用线程安全的数据结构)。

2. 宏定义解析

(1) LOG(level) 宏

#define LOG(level) logger(level, __FILE__, __LINE__)
  • 作用:简化日志调用,自动填充文件名和行号。

  • 展开后

    LOG(LogLevel::INFO) << "This is a log message.";
    // 等价于:
    logger(LogLevel::INFO, __FILE__, __LINE__) << "This is a log message.";
  • 关键点(详细回顾C语言的章节)

    • __FILE__ 是预定义宏,表示当前源文件名(如 "main.cpp")。

    • __LINE__ 是预定义宏,表示当前行号(如 42)。

    • 这样日志会自动记录代码位置,便于调试。

使用示例

LOG(LogLevel::DEBUG) << "x = " << x;
LOG(LogLevel::ERROR) << "File not found: " << filename;

(2) 日志策略配置宏

#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
  • 作用:简化日志输出策略的配置。

  • 展开后

    Enable_Console_Log_Strategy(); // 等价于 logger.EnableConsoleLogStrategy();
    Enable_File_Log_Strategy();    // 等价于 logger.EnableFileLogStrategy();
  • 典型用途

    • EnableConsoleLogStrategy():启用控制台输出(如 std::cout)。

    • EnableFileLogStrategy():启用文件输出(如写入 log.txt)。

  • 设计意图

    • 通过宏隐藏具体实现,使代码更简洁。

    • 如果未来 Logger 的策略配置方法改名,只需修改宏定义,无需改动所有调用处。

使用示例

int main() {Enable_Console_Log_Strategy(); // 启用控制台日志Enable_File_Log_Strategy();    // 启用文件日志LOG(LogLevel::INFO) << "Application started.";return 0;
}

6、注意:关于多态(Polymorphism)智能指针(std::unique_ptr的使用

        具体来说, _fflush_strategy 是 LogStrategy* 类型,但被赋值为 FileLogStrategy 或 ConsoleLogStrategy 的实例时,是否能正确访问派生类的成员,而不会发生对象截断(Object Slicing)

1. 这份代码是否安全?

是的,代码是安全的,不会发生对象截断,原因如下:

std::unique_ptr<LogStrategy> 存储的是派生类对象

  • std::make_unique<FileLogStrategy>() 会在堆上创建一个 FileLogStrategy 对象,并返回 std::unique_ptr<FileLogStrategy>,但可以隐式转换为 std::unique_ptr<LogStrategy>(因为 FileLogStrategy 继承自 LogStrategy)。

  • 此时 _fflush_strategy 仍然指向完整的 FileLogStrategy 对象,不会发生截断

多态调用(Virtual Functions)

  • 只要 LogStrategy 定义了虚函数(如 void Log(const std::string& message)),并且 FileLogStrategy 和 ConsoleLogStrategy 覆盖了这些虚函数,那么通过 _fflush_strategy->Log(...) 调用时,会正确执行派生类的实现。

  • 如果没有虚函数,则会发生静态绑定(早期绑定),可能调用到基类的实现(但不会截断对象)。

2. 对象截断(Object Slicing)何时发生?

对象截断通常发生在以下情况:

LogStrategy strategy = FileLogStrategy(); // ❌ 错误!派生类部分被截断
  • 这里 FileLogStrategy 对象被拷贝构造到 LogStrategy 对象,但 LogStrategy 无法存储派生类的额外数据,导致派生类部分被丢弃

  •  std::unique_ptr 方式不会发生这种情况,因为它是通过指针管理完整对象。

3. 代码示例分析

class Logger {
public:Logger() {EnableConsoleLogStrategy(); // 默认使用控制台策略}void EnableFileLogStrategy() {_fflush_strategy = std::make_unique<FileLogStrategy>(); // 安全,存储完整 FileLogStrategy 对象}void EnableConsoleLogStrategy() {_fflush_strategy = std::make_unique<ConsoleLogStrategy>(); // 安全,存储完整 ConsoleLogStrategy 对象}private:std::unique_ptr<LogStrategy> _fflush_strategy; // 基类指针,指向派生类对象
};

_fflush_strategy 可以安全地指向 FileLogStrategy 或 ConsoleLogStrategy,因为:

  • 它们都是 LogStrategy 的派生类。

  • std::unique_ptr 会正确管理对象的生命周期(自动释放内存)。

  • 如果 LogStrategy 有虚函数,并且派生类正确覆盖它们,那么调用时会执行正确的派生类逻辑。

4. 如何确保多态调用?

(1) 基类必须定义虚函数
class LogStrategy {
public:virtual ~LogStrategy() = default; // 虚析构函数,确保派生类对象能正确释放virtual void Log(const std::string& message) = 0; // 纯虚函数,强制派生类实现
};
(2) 派生类必须覆盖虚函数
class FileLogStrategy : public LogStrategy {
public:void Log(const std::string& message) override {std::cout << "FileLog: " << message << std::endl;}
};class ConsoleLogStrategy : public LogStrategy {
public:void Log(const std::string& message) override {std::cout << "ConsoleLog: " << message << std::endl;}
};
(3) 通过基类指针调用
Logger logger;
logger._fflush_strategy->Log("Hello, world!"); // 根据当前策略调用 FileLogStrategy 或 ConsoleLogStrategy 的实现

5. 总结

  • 代码是安全的std::unique_ptr<LogStrategy> 可以正确管理 FileLogStrategy 或 ConsoleLogStrategy 对象,不会发生截断。

  • 必须使用虚函数,否则 _fflush_strategy->Log(...) 会调用 LogStrategy 的默认实现(如果存在),而不是派生类的实现。

  • 对象截断只发生在值拷贝(非指针/引用)的情况下,而代码使用的是智能指针,因此不会发生。

6. 额外建议

  • 如果 LogStrategy 是一个接口类(只有纯虚函数),可以将其析构函数设为 = default 并标记为 override

    virtual ~LogStrategy() = default;
  • 考虑使用 std::move 优化策略切换(避免不必要的内存分配):

    void EnableFileLogStrategy() {_fflush_strategy = std::make_unique<FileLogStrategy>(); // 已有优化
    }

7、完整日志系统实现代码

#ifndef __LOG_HPP__
#define __LOG_HPP__#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"namespace LogModule
{using namespace MutexModule;const std::string gsep = "\r\n";// 策略模式,C++多态特性// 2. 刷新策略 a: 显示器打印 b:向指定的文件写入//  刷新策略基类class LogStrategy{public:~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;};// 显示器打印日志的策略 : 子类class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::cout << message << gsep;}~ConsoleLogStrategy(){}private:Mutex _mutex;};// 文件打印日志的策略 : 子类const std::string defaultpath = "./log";const std::string defaultfile = "my.log";class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path),_file(file){LockGuard lockguard(_mutex);if (std::filesystem::exists(_path)){return;}try{std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"std::ofstream out(filename, std::ios::app);                              // 追加写入的 方式打开if (!out.is_open()){return;}out << message << gsep;out.close();}~FileLogStrategy(){}private:std::string _path; // 日志文件所在路径std::string _file; // 日志文件本身Mutex _mutex;};// 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式// 1. 形成日志等级enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};std::string Level2Str(LogLevel level){switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "UNKNOWN";}}std::string GetTimeStamp(){time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm);char timebuffer[128];snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year+1900,curr_tm.tm_mon+1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuffer;}// 1. 形成日志 && 2. 根据不同的策略,完成刷新class Logger{public:Logger(){EnableConsoleLogStrategy();}void EnableFileLogStrategy(){_fflush_strategy = std::make_unique<FileLogStrategy>();}void EnableConsoleLogStrategy(){_fflush_strategy = std::make_unique<ConsoleLogStrategy>();}// 表示的是未来的一条日志class LogMessage{public:LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger){// 日志的左边部分,合并起来std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();}// LogMessage() << "hell world" << "XXXX" << 3.14 << 1234template <typename T>LogMessage &operator<<(const T &info){// a = b = c =d;// 日志的右半部分,可变的std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}~LogMessage(){if (_logger._fflush_strategy){_logger._fflush_strategy->SyncLog(_loginfo);}}private:std::string _curr_time;LogLevel _level;pid_t _pid;std::string _src_name;int _line_number;std::string _loginfo; // 合并之后,一条完整的信息Logger &_logger;};// 这里故意写成返回临时对象LogMessage operator()(LogLevel level, std::string name, int line){return LogMessage(level, name, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _fflush_strategy;};// 全局日志对象Logger logger;// 使用宏,简化用户操作,获取文件名和行号#define LOG(level) logger(level, __FILE__, __LINE__)#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}#endif

五、线程安全实现

互斥锁封装

#pragma once
#include <iostream>
#include <pthread.h>namespace MutexModule {class Mutex {public:Mutex() {pthread_mutex_init(&_mutex, nullptr);}void Lock() {pthread_mutex_lock(&_mutex);}void Unlock() {pthread_mutex_unlock(&_mutex);}~Mutex() {pthread_mutex_destroy(&_mutex);}pthread_mutex_t *Get() {return &_mutex;}private:pthread_mutex_t _mutex;};// RAII锁守卫class LockGuard {public:LockGuard(Mutex &mutex) : _mutex(mutex) {_mutex.Lock();}~LockGuard() {_mutex.Unlock();}private:Mutex &_mutex;};
}

六、使用示例与分析

1、基本使用示例

#include "Log.hpp"
using namespace LogModule;void fun() {int a = 10;LOG(LogLevel::FATAL) << "hello world" << 1234 << ", 3.14" << 'c' << a;
}int main() {// 使用控制台日志LOG(LogLevel::DEBUG) << "hello world";// 切换到文件日志Enable_File_Log_Strategy();LOG(LogLevel::WARNING) << "hello world";fun();return 0;
}

2、输出结果分析

控制台输出示例 和 文件日志内容(my.log)如下:

3、设计优势

  • 策略模式灵活性:运行时切换日志输出方式、易于扩展新的输出策略(如网络日志、数据库日志)

  • 线程安全性:每个策略实现内部使用互斥锁、RAII模式的LockGuard确保异常安全

  • 易用性:宏定义简化调用、流式接口支持多种数据类型

  • 性能考虑:日志格式化与输出分离、析构时自动触发输出,确保异常情况也能记录


七、改进与扩展建议

1、性能优化方向

  • 异步日志:引入生产者-消费者模型、使用队列缓冲日志消息

  • 日志级别过滤:在Logger类中添加级别过滤、避免不必要日志的格式化开销

  • 批量写入:文件日志策略积累一定量后批量写入、减少IO操作次数

2、功能扩展建议

  • 日志滚动策略:按文件大小或时间分割日志、自动清理旧日志文件

  • 多输出目标:支持同时输出到控制台和文件、不同级别输出到不同目标

  • 格式化定制:支持自定义日志格式模板、添加颜色输出(控制台)

  • 上下文信息:支持添加请求ID等跨调用链信息、线程本地存储优化上下文传递


八、总结

本日志系统实现展示了策略模式在软件开发中的典型应用:

  • 分离变与不变:不变:日志记录的核心流程;变:不同的输出方式(策略)

  • 开闭原则:对扩展开放(新增策略)、对修改关闭(无需改动Logger类)

  • 组合优于继承:通过组合方式使用策略、避免继承带来的强耦合

        这种设计使得系统既保持了核心功能的稳定性,又具备了足够的灵活性来应对变化的需求。在实际项目中,可以根据具体需求在此基础上进行扩展和优化。

http://www.dtcms.com/a/564968.html

相关文章:

  • 电子规划书商务网站建设城市分站cms
  • 【android驱动开发十三】内核常见的十种死机类型
  • Flutter 移动端性能优化指南:内存、电量与 UI 渲染
  • 广告网站建设网wordpress登录更改域名后
  • 【JUnit实战3_25】第十五章:表现层测试(上)—— HtmlUnit 在网页测试中的用法
  • OpenComic,一款跨平台的漫画阅读器
  • 基于springboot的社区疫情物资管理系统的设计与实现(代码+数据库+LW)
  • 3.3V与5V电平转换方法、电路原理分析
  • python mysql-connector、PyMySQL基础
  • 【Javascript】如何硬拷贝一个数组?
  • 少儿编程不止学技术:6 大学习习惯的蜕变与思维能力的跃迁
  • 自动驾驶运动规划 | 基于自行车模型的运动学模型和横向动力学模型详细推导图解
  • 软文营销的技巧有哪些网站建设和优化内容最重要性
  • 我局在网站建设方面wordpress 搜索没反应
  • C语言基础之函数指针4
  • 深入浅出 Java 虚拟机之进阶部分
  • 医疗保健|医疗养老|基于Java+vue的医疗保健系统(源码+数据库+文档)
  • 网站建设方案书组网方案网站攻击
  • Python循环
  • 基于自适应傅里叶分解(AFD)及其改进算法的信号分解与重构实现
  • Linux Shell awk
  • iBM(i2)图表数据优化及重点人员分析(三)
  • 做两个一摸一样的网站有没有专门做家乡图片的网站
  • Ubuntu 22.04 离线升级 OpenSSH 到 9.8p1
  • Dify 插件开发与打包教程 (Mac)
  • FastMCP 入门:用 Python 快速搭建 MCP 服务器接入 LLM
  • 常见DGX A100服务器维修故障问题及解决方法
  • Linux系统编程——exec函数族
  • 简单搭建express服务器
  • 设置网站建设大连专业网站设计服务商