【Linux】多线程实战 —— 日志类 | 策略模式

🌈欢迎来到Linux专栏 ~~ 日志类

日志与策略模式

现在开始,我们结合我们之前所做的所有封装,进行一个线程池的设计。在写之前,我们要做如下准备

🔹 准备线程的封装

🔹 准备锁和条件变量的封装

🔹 引入日志,对线程进行封装

前两个我们都做过了,接下来聊聊日志 ~

一、日志与策略模式

什么是设计模式?

IT行业这么火,涌入的人很多。俗话说林子大了啥鸟都有,大佬和菜鸡们两极分化的越来越严重.。为了让菜鸡们不太拖大佬的后腿,于是大佬们针对⼀些经典的常见的场景,给定了⼀些对应的解决方案,这个就是设计模式

接下来认识一下日志

计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要⼯具。

日志格式以下几个指标是必须得有的

  • 时间戳
  • 日志等级
  • 日志内容

以下几个指标是可选的

  • 文件名行号
  • 进程,线程相关id信息等

日志有现成的解决方案,如:spdlogglogBoost.LogLog4cxx等等,我们依旧采用自定义日志的方式。

这里我们采用设计模式-策略模式来进行日志的设计,目标格式如下:

cpp 复制代码
[可读性很好的时间] [⽇志等级] [进程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️⃣gettimeofdayPOSIX 系统调用用于获取当前时间,精度为微秒(µs)

cpp 复制代码
#include <sys/time.h>

int gettimeofday(struct timeval *tv, struct timezone *tz);

tv输出型参数,其类型是一个结构体

cpp 复制代码
struct timeval {
    time_t      tv_sec;   // 秒
    suseconds_t tv_usec;  // 微秒 
};

tz时区信息 ,默认北京时区,已废弃,传 nullptr 即可

2️⃣时间戳信息转换localtime_r() 线程安全版本

c 复制代码
#include <time.h>

struct tm* localtime_r(const time_t* timer, struct tm* result);

函数功能:把秒为单位的时间戳,转化成struct tm结构体

  • timer:传入的时间戳

返回值:成功返回result结构体变量的地址,失败返回NULL

cpp 复制代码
struct tm {
    int tm_sec;    /* Seconds (0-60) */
    int tm_min;    /* Minutes (0-59) */
    int tm_hour;   /* Hours (0-23) */
    int tm_mday;   /* Day of the month (1-31) */
    int tm_mon;    /* Month (0-11) */
    int tm_year;   /* Year - 1900 */
    int tm_wday;   /* Day of the week (0-6, Sunday = 0) */
    int tm_yday;   /* Day in the year (0-365, 1 Jan = 0) */
    int tm_isdst;  /* Daylight saving time */
};

二、策略模式

策略模式的 骨架基类是用纯虚函数来定义,作为策略的接口!

真正实现时,是子类去继承基类的函数,进行重新实现!

首先来看看基类的实现

cpp 复制代码
//策略接口 ~ 基类
class LogStrategy
{
public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &message) = 0;
};

default:让编译器生成默认实现,同时保持虚析构属性

= 0:表示纯虚函数,不提供实现,子类必须重写所有纯虚函数

📌向显示器打印策略

此处不需要实现构造函数,因其私有成员只有一把锁,在LockGuard lockguard(_mutex)处就可以完成锁的无参构造以及初始化

cpp 复制代码
// 控制台日志刷新策略,日志将来想显示器打印
class ConsoleStrategy : public LogStrategy
{
public:
    void SyncLog(const std::string &message) override 
    {
        LockGuard lockguard(_mutex);
        std::cerr << message << std::endl;
    }
    ~ConsoleStrategy()
    {}
private:
    Mutex _mutex;
};

override :编译器检查是否真正覆盖了基类虚函数

📌文件策略

直接放上源码------

cpp 复制代码
class FileLogStrategy : public LogStrategy
{
public:
    FileLogStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename) :
    _logpath(path),
    _logfilename(filename)
    {
        LockGuard lockguard(_mutex); //保证在进行文件操作时是原子性的
        if(std::filesystem::exists(_logpath))
            return;
        try
        {
            std::filesystem::create_directories(_logpath);//不存在就创建
        }
        catch(const std::filesystem::filesystem_error &e)
        {
            std::cerr << e.what() << '\n';
        }
        if(!_logpath.empty() && _logpath.back() != '/') //back返回字符串的最后一个字符
        {
            _logpath += "/";
        }
    }

    void SyncLog(const std::string &message) override
    {
        {
            LockGuard lockguard(_mutex); //同理加锁保护
            
            std::string targetlog = _logpath + _logfilename;  //"./loglog.txt" 需要加分隔符
            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()
    {}
private:
    std::string _logpath;
    std::string _logfilename;
    Mutex _mutex;
};

向指定的文件进行写入,那么其私有成员必须有文件名以及文件路径

此处故意把二者分开,因为有可能该路径下的目录是不存在的,就需要手动的创建了

今天我们不打算使用系统调用(open等)去实现文件新建等工作,而是使用C++17的新特性:

cpp 复制代码
#include <filesystem> 

那么我们怎么判断一个文件的路径是否真的存在呢?

  • stat() 根据 文件路径 获取文件属性信息。
c 复制代码
#include <sys/stat.h>

int stat(const char *pathname, struct stat *buf);

pathname文件路径(输入)

buf文件属性(输出)

输出的结构体里包括了

cpp 复制代码
struct stat
{
    mode_t st_mode;      // 文件类型+权限
    off_t st_size;       // 文件大小
    time_t st_atime;     // 访问时间
    time_t st_mtime;     // 修改时间
    ino_t st_ino;        // inode号
};

今天我们要用文件系统 封装的exists函数,其功能:判断路径是否存在

不存在就调用create_directories函数去帮我们创建

由于创建目录成功与否关键在于当前目录是否有执行权限x ,创建失败就需要抛出异常

cpp 复制代码
FileLogStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename) :
_logpath(path),
_logfilename(filename)
{
    LockGuard lockguard(_mutex); //保证在进行文件操作时是原子性的
    if(std::filesystem::exists(_logpath))
        return;
    try
    {
        std::filesystem::create_directories(_logpath);//不存在就创建
    }
    catch(const std::filesystem::filesystem_error &e)
    {
        std::cerr << e.what() << '\n';
    }
}

接着是C++中的文件操作分为三大类:

  • ifstream读文件
  • ofstream写文件
  • fstream读+写

三、日志类以及日志生成

上述的所有工作都是为了服务于日志类

日志类的目标:

  1. 帮助日志的生成
  2. 根据不同的策略,来进行刷新

1️⃣日志刷新

根据不同的策略,来进行刷新

cpp 复制代码
//日志对象,全局使用 ------ 把日志对象变成一个大的临界资源
Logger logger;
//不暴露全局对象,直接使用宏来调用全局对象
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

class Logger
{
public:
    Logger()
    {
    }
    void UseConsoleStrategy()
    {
        // 1. 堆上创建一个 ConsoleStrategy 对象
        // 2. 自动包进 unique_ptr 管理生命周期
        // 3. 隐式转化 为 unique_ptr<LogStrategy> 
        // 4. 移动赋值给 _strategy
        _strategy = std::make_unique<ConsoleStrategy>();
    }

    void UseFileStrategy()
    {
        _strategy = std::make_unique<FileLogStrategy>();
    }

    void Debug(const std::string &message)
    {
        if(_strategy != nullptr)
        {
            _strategy->SyncLog(message);
        }
    }

    ~Logger()
    {
    }
private:
    // 虽然虚基类不能直接定义对象,但可以定义指针对象
    std::unique_ptr<LogStrategy> _strategy; // 刷新策略
};

实现注意细节

  • 💥想把日志对象在全局上进行使用,也就要把日志对象变成一个大的临界资源。-
  • 为了方便我们后续的使用,还提供了两个宏,从而不暴露全局对象,直接使用宏来调用全局对象

接着我们直接测试一下,顺便捋顺执行顺序,Main.cc代码如下

c 复制代码
#include "Logger.hpp"

using namespace LOG_MODULE;
int main()
{
    ENABLE_CONSOLE_LOG_STRATEGY();
    logger.Debug("你好哇\n");
    logger.Debug("你好哇\n");
    logger.Debug("你好哇\n");
    logger.Debug("你好哇\n");
    logger.Debug("你好哇\n");

    ENABLE_FILE_LOG_STRATEGY();
    logger.Debug("早上坏\n");
    logger.Debug("早上坏\n");
    logger.Debug("早上坏\n");
    return 0;
}

运行结果如下:

整体的流程如下:

  1. 调用 Logger() 无参构造
  2. ENABLE_CONSOLE_LOG_STRATEGY() 宏展开logger.UseConsoleStrategy();,并且创建一个智能指针指向刚创建的ConsoleStrategy 对象,最后赋值给_strategy
  3. Logger::Debug ------ 进入去调用Debug
  4. _strategy 实际指向的是 ConsoleStrategy,通过虚函数表调用:打印对应对象的打印方式
  5. 重复执行
  6. 最后智能指针自动析构

定义基类策略接口,子类对象定义好,最终在日志类里定义一个基类指针,后续指针指向哪个策略就能拿到对应策略的方法

这意味着后续如果想新增策略,就只需新增一组新的继承关系,并实现基类的方法,就可以解耦的去执行 ~ 优雅

🔥策略模式是基于纯虚接口,用统一的接口类,让子类去继承并且实现方法,从而实现代码横向扩展的设计模式

2️⃣日志生成

如何生成一天天的日志信息呢? ------ 设计一个内部类:表示一条完整的日志信息

cpp 复制代码
左半部分的组成
[2024-08-04 12:27:03.123456] [DEBUG] [202938] [main.cc] [16] - hello world
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~   ~~~~~   ~~~~~~~  ~~~~~~~~  ~~
 时间戳                              日志等级  PID      文件名   行号
 \_______________  左半部分(元数据)  _______________/
完整格式:[时间] [等级] [PID] [文件] [行号] - 

其中左半部分的格式是相对固定的! 变化也只是值发生变化。而日志信息的右半部分的形式是会随格式变而变,于是先实现左半部分

使用 std::stringstream字符串文件流) 拼接左半部分:

std::stringstream 是一种内存文件流 ------它将各种类型的数据(int、pid_t、std::string)通过 operator<< 统一格式化为字符串,起到类似"文件流"的缓冲和拼接作用。它与实际的文件流 std::ofstream 形成对照:

日志部分,为了在进行使用的时候,想仿照C++输出模式进行使用,就对()进行重载

重载 :这是对 Logger 类的函数调用运算符 operator() 的重载,使 Logger 对象可以像函数一样被调用。它充当工厂方法------接收 (level, filename, line) 参数,调用 LogMessage 构造函数创建临时对象并拷贝返回

cpp 复制代码
LogMessage operator()(LogLevel level, std::string filename, int line)
{
    return LogMessage(level, filename, line); // 传入时,构建临时拷贝构建Message,返回新对象
}
  • 此处不属于内部类, 对()进行重载,故意采用拷贝LogMessage

接着进行拼接右半部分:

cpp 复制代码
template<typename T>
LogMessage &operator << (const T &info)
{
   std::stringstream ss;
   ss << info;
   _loginfo += ss.str();
   //this指针是成员函数的隐含指针,指向当前LogMessage对象
   return *this; //返回当前LogMessage对象,方便下次继续使用 <<
}

函数模版template<typename T> --- 支持任意类型 (int、double、const char*、std::string 等)进行转换

字符串文件流 :将任意类型 T 通过 operator<< 统一转为字符串 ,然后追加到 _loginfo

返回this指针 :返回 LogMessage& 引用

举个例子:

c 复制代码
logger(DEBUG, "f.cc", 42) << "x=" << 42 << " y=" << 3.14;
//  ① LogMessage          ②        ③     ④
cpp 复制代码
this  = &当前对象      // 地址,类型 LogMessage*
*this = 当前对象       // 对象本身,类型 LogMessage&

*this 解引用得到对象引用 ,才能让下一个 << 继续作用在同一个 LogMessage 对象上,实现标准库风格的连续拼接。

接下来我想让 LogMessageRAII风格进行刷新:

  • 构造是构建出左半部分,<< 重载一直获取右半部分,最后在析构时进行刷新,让后释放
    -于是乎在内部类里引用外部类,方便后续在析构时进行策略性刷新
cpp 复制代码
~LogMessage()
{
    // 析构时判断
    if (_logger._strategy)
    {
        //此时_loginfo就是完整日志信息了
        _logger._strategy->SyncLog(_loginfo);
    }
}

最后我们来理顺一下从整体的输出流程:

LogMessage调用完后,会被自动析构(同时根据外部类的策略进行刷新)

最后跑一下测试:

四、完整代码

Main.cc:

cpp 复制代码
#include "Logger.hpp"

using namespace LOG_MODULE;

int main()
{
    ENABLE_CONSOLE_LOG_STRATEGY();
    LOG(LogLevel::WARNING) << "hello" << 10 << 1.2 << "下午坏";

    ENABLE_FILE_LOG_STRATEGY();
    LOG(LogLevel::WARNING) << "hello" << 10 << 1.2 << "下午坏";
    LOG(LogLevel::WARNING) << "hello" << 10 << 1.2 << "下午坏";
    LOG(LogLevel::WARNING) << "hello" << 10 << 1.2 << "下午坏";
    return 0;
}

Logger.hpp:

cpp 复制代码
#ifndef LOGGER_HPP
#define LOGGER_HPP

#include <iostream>
#include <string>
#include <ctime>
#include <time.h>
#include <stdio.h>
#include <memory>
#include <unistd.h>
#include <sys/time.h>
#include "Mutex.hpp"
#include <filesystem> //C++17
#include <fstream>
#include <sstream>

namespace LOG_MODULE
{
    // 日志等级
    enum class LogLevel
    {
        INFO,    // 正常
        WARNING, // 告警
        ERROR,
        FATAL,
        DEBUG
    };
    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 "UNKOWN";
        }
    }

    // 1.时间戳 2.日期 + 时间
    std::string GetCurrentTime()
    {
        struct timeval current_time;
        int n = gettimeofday(&current_time, nullptr);
        (void)n;

        // current_time.tv_sec 、current_time.tv_usec
        struct tm struct_time;
        localtime_r(&(current_time.tv_sec), &struct_time);

        char timestr[128];
        snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d-%02d-%02d.%ld",
                 struct_time.tm_year + 1900,
                 struct_time.tm_mon + 1,
                 struct_time.tm_mday,
                 struct_time.tm_hour,
                 struct_time.tm_min,
                 struct_time.tm_sec,
                 current_time.tv_usec);
        return timestr;
    }

    // 输出角度  -- 刷新策略
    // 1. 显示器打印
    // 2. 文件写入

    // 日志的生成
    // 1. 构建日志字符串
    // 2. 根据不同的策略进行刷新

    // 策略模式 策略接口 ~ 基类
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 控制台日志刷新策略,日志将来想显示器打印
    class ConsoleStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cerr << message << std::endl;
        }
        ~ConsoleStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";
    // 文件策略:
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename)
            : _logpath(path),
              _logfilename(filename)
        {
            LockGuard lockguard(_mutex); // 保证在进行文件操作时是原子性的

            if (std::filesystem::exists(_logpath))
                return;
            try
            {
                std::filesystem::create_directories(_logpath); // 不存在就创建
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(const std::string &message) override
        {
            {
                LockGuard lockguard(_mutex); // 同理加锁保护

                if (!_logpath.empty() && _logpath.back() != '/') // back返回字符串的最后一个字符
                {
                    _logpath += "/";
                }

                std::string targetlog = _logpath + _logfilename; //"./loglog.txt" 需要加分隔符
                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()
        {
        }

    private:
        std::string _logpath;
        std::string _logfilename;
        Mutex _mutex;
    };

    class Logger
    {
    public:
        Logger()
        {
            // 用户不选择,默认选择控制台策略
            UseConsoleStrategy();
        }

        void UseConsoleStrategy()
        {
            // 1. 堆上创建一个 ConsoleStrategy 对象
            // 2. 自动包进 unique_ptr 管理生命周期
            // 3. 隐式转化 为 unique_ptr<ConsoleStrategy>
            // 4. 移动赋值给 _strategy
            _strategy = std::make_unique<ConsoleStrategy>();
        }

        void UseFileStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }

        // 测试策略选择
        //  void Debug(const std::string &message)
        //  {
        //      if(_strategy != nullptr)
        //      {
        //          _strategy->SyncLog(message);
        //      }
        //  }

        // 内部类,表示一条完整的日志信息
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
                : _level(level),
                  _curr_time(GetCurrentTime()),
                  _pid(getpid()),
                  _filename(filename),
                  _line(line),
                  _logger(logger)
            {
                //[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world 只剩下日志内容了
                // 先构建左半部分  ------ 字符串拼接
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << LogLevel2Message(_level) << "] " // LogLevel是枚举类型,在输出是会被流式输出当做是整数去打印
                   << "[" << _pid << "] "
                   << "[" << _filename << "] "
                   << "[" << _line << "] "
                   << "- ";

                _loginfo = ss.str();
            }
            // 右半部分:重载<<
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                // this指针是成员函数的隐含指针,指向当前LogMessage对象
                return *this; // 返回当前LogMessage对象,方便下次继续使用 <<
            }

            ~LogMessage()
            {
                // 析构时判断
                if (_logger._strategy)
                {
                    // 此时_loginfo就是完整日志信息了
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

        private:
            LogLevel _level;
            std::string _curr_time;
            pid_t _pid;
            std::string _filename;
            int _line;
            std::string _loginfo; // 一条完整的日志信息

            // 引用外部的Logger类对象
            Logger &_logger; // 引用外部类,方便后续进行策略性刷新
        };

        // 此处不属于内部类了 对()进行重载
        // 故意采用拷贝LogMessage
        LogMessage operator()(LogLevel level, std::string filename, int liner)
        {
            // 还需传入当前的外部类对象:*this即可
            return LogMessage(level, filename, liner, *this); // 传入时,构建临时拷贝构建Message,返回新对象
        }

        ~Logger()
        {
        }

    private:
        // 虽然虚基类不能直接定义对象,但可以定义指针对象
        std::unique_ptr<LogStrategy> _strategy; // 刷新策略
    };

    // 日志对象,全局使用 ------ 把日志对象变成一个大的临界资源
    Logger logger;
    // 不暴露全局对象,直接使用宏来调用全局对象
#define LOG(level) logger(level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

};
#endif

拓展:文件策略 | 区分日志等级来进行保存

📢写在最后

接下来是线程池与线程安全 ------ 冲冲冲🚀

相关推荐
qq_4523962315 小时前
第七篇:《Docker 存储:Volume、Bind Mount 与 tmpfs》
运维·docker·容器
闻缺陷则喜何志丹15 小时前
P8134 [ICPC 2020 WF] Opportunity Cost|普及+
c++·算法·洛谷
love8888_cnsd15 小时前
Git & Linux 速查表
java·linux·git·后端·elasticsearch
不会C语言的男孩15 小时前
C++ Primer Plus 第2章:开始学习C++
开发语言·c++
handler0115 小时前
【Linux】五种IO模型详解
linux·运维·服务器·c语言·网络·笔记·php
c2385615 小时前
MySrting的模拟实现
开发语言·c++·算法
Rabitebla15 小时前
C++ 继承详解(下):默认成员函数、虚继承底层与设计取舍
c语言·开发语言·数据结构·c++·算法·leetcode
运维行者_21 小时前
Applications Manager中的Redis监控
大数据·服务器·数据库·人工智能·网络协议
xingyuzhisuan1 天前
网络 Token 常见故障原理,基础排查科普
运维·服务器·网络·php