日志的实现

目录

日志与策略模式

Log.hpp

 class LogStrategy基类

class ConsoleLogStrategy派生类

 classFileLogStrategy派生类

日志等级

获得时间戳

localtime_r函数详解

函数原型

struct tm结构的指针

Logger类(重点)

class LogMessage 日志信息类

std::stringstream

用法

重载 流输出运算符和析构函数


日志与策略模式

什么是设计模式?

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

日志认识

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

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

  • 时间戳
  • 日志等级
  • 日志内容
  • 文件名(可选)
  • 行号(可选)
  • 进程/线程id(可选)

日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。 这里我们采用设计模式-策略模式来进行日志的设计

我们想要的日志格式如下:

[ 可读性很好的时间 ] [ 日志等级 ] [ 进程 pid] [ 打印对应日志的文件名 ][ 行号 ] - 消息内容,支持可 变参数

Log.hpp

首先创建Log.hpp文件,命名一个LogModule的空间域,我们将来的代码就在这里面实现

#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 LogMudoule
{



}

#endif

 class LogStrategy基类

策略模式就是利用多态的特性,我们先定义出一个LogStrategy的基类,后面通过我们传的不同派生类来实现不同的模式,比如向屏幕打印的ConsoleLogStrategy派生类和向文件写入的FileLogStrategy派生类

using namespace MutexModule;

    const std::string gsep = "\r\n";
    // 策略模式,C++多态特性
    // 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
    //  刷新策略基类
  class LogStrategy
    {
    public:
        virtual ~LogStrategy()
        {
        }
        virtual void SyncLog(const std::string &message)=0;   
        //控制台虚函数
    };

gsep是换行符,因为要使用锁来保护临界资源,这里Sync是同步的意思,基类的析构函数要记得加上virture。SyncLog是纯虚函数(强制派生类重写虚函数,因为不重写实例化不出对象。)

class ConsoleLogStrategy派生类

下面的ConsoleLogStrategy派生类实现的也十分简单

 // 显示器打印日志的策略  子类
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }

        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cout << message << gsep;//向屏幕打印消息
        }
        ~ConsoleLogStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

LockGuard 是我们封装的互斥锁的类,通过lockguard对象的创建来保护临界资源,析构后释放锁

不懂互斥锁的话可以去看看博客,我们的重点是完成日志,后续不再讲解锁

override是检查派生类虚函数是否重写了基类的某个虚函数。如果对于多态的知识不怎么了解的话,我十分推荐您去看这篇多态博客,点击多态就能进去阅读

 classFileLogStrategy派生类

下面的FileLogStrategy派生类稍微复杂一点但是并不难理解

// 文件打印日志的策略 : 子类
    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);
            const 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:
        const std::string _path;
        const std::string _file;
        Mutex _mutex;
    };

理解一个类首先要去看它的私有成员,日志要向文件进行打印,首先要知道它的路径,其次就是它的文件名。所以能理解_path和_file两个成员变量

C++17 引入了一个重要的新特性:​文件系统库​ (std::filesystem),它提供了处理文件系统和路径的标准方法。这个库基于 Boost.Filesystem,并经过了标准化处理。在构造函数中完成_path和_file的初始化,然后利用std::filesystem::exists来检查路径是否存在,存在就返回,不存在就创建一个路径(在当前路径下的Log目录下)

SyncLog函数中filename就是我们要创建的文件名,它由_path和_file组合,

中间的三目操作符意思是如果 _path 的最后一个字符是 '/',那么整个三目运算符的结果就是空字符串 "",否则就是字符串 "/"。然后,这个结果被用于与 _path 和 _file 进行字符串拼接。

ofstream是 C++ 标准库中的一个类,全称为 ​Output File Stream​(输出文件流)。它用于将数据从程序写入到文件中,是文件操作的重要组成部分。

  • std::ofstream: 是C++标准库中用于文件输出的流类,定义在头文件 <fstream>中。
  • out: 是定义的输出文件流对象的名称。
  • filename: 是要打开的文件名(可以是字符串、字符数组等)。
  • std::ios::app: 是打开文件的模式标志,表示以追加模式(append)打开。
  • out.close():表示关闭文件

好了,到这里我们日志就实现了大概1/2,我们这里可以去检验一下我们写错没有

#include <iostream>
#include "Log.hpp"

using namespace LogMudoule;
int main()
{
   // std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleLogStrategy>();
    std::unique_ptr<LogStrategy> strategy = std::make_unique<FileLogStrategy>();
    strategy->SyncLog("hello");
    return 0;
}

strategy是智能指针由派生类 classFileLogStrategy完成赋值(多态)表示我们选择文件写入的模式

下面的图片也表明了我们的代码无误

日志等级

利用枚举实现日志等级

// 1. 形成日志等级
enum class LogLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

我们使用的枚举实际上是0、1、2、3等数字,最终我们的日志是一串字符串所以我们还要类型转换一下,也是非常简单的。

std::string LeveltoStr(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;
}

localtime_r函数详解

localtime_r是一个用于将时间戳转换为本地时间的线程安全函数。它是 localtime函数的可重入(reentrant)版本,在多线程编程中特别重要。

函数原型

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

参数说明

  • timep: 指向 time_t类型时间的指针,表示从 1970-01-01 00:00:00 UTC 开始的秒数
  • result: 指向 struct tm结构的指针,用于存储转换后的时间信息

返回值

  • 成功时返回指向 result的指针
  • 失败时返回 NUL

struct tm结构的指针

struct tm {
    int tm_sec;    // 秒 [0, 59]
    int tm_min;    // 分 [0, 59]
    int tm_hour;   // 时 [0, 23]
    int tm_mday;   // 日 [1, 31]
    int tm_mon;    // 月 [0, 11] (0 = 一月)
    int tm_year;   // 年 (从1900开始)
    int tm_wday;   // 星期 [0, 6] (0 = 周日)
    int tm_yday;   // 年中的日 [0, 365]
    int tm_isdst;  // 夏令时标志 (正数: 是, 0: 否, 负数: 未知)
};

snprintf格式化输入函数,我们期望的年是4位,月是两位(不足补0——%02d)天、时分秒也是如此

实现效果如下

Logger类(重点)

我们在Logger类中完成整个日志的实现它的结构如下

class Logger
{
public:
    Logger()
    {
        SelectConsoleLogStrategy();
    }
    void SelectFileLogStrategy()
    {
        _fflush_strategy = std::make_unique<FileLogStrategy>();
    }
    void SelectConsoleLogStrategy()
    {
        _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
    }

    class LogMessage
    {
    public:
    
    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类里实现一个内部类LogMessage最后再谈

这里Logger的构造函数是选择一种模式,我们这里设置的是向屏幕打印

Logger的私有成员变量是指向基类LogStrategy的智能指针(多态行为)在SelectConsoleLogStrategy()函数中完成赋值就是选择对应模式

我们在Logger类中还完成了一个仿函数-故意没写引用&,这个仿函数的作用是体现在接下来的LogMessage类里重载<<中

LogMessage operator()实际使用方式

// 传统方式可能这样写:
logger.log(DEBUG, "main.cpp", 42) << "This is a debug message";

// 使用 operator() 可以这样写:
logger(DEBUG, "main.cpp", 42) << "This is a debug message";

class LogMessage 日志信息类

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 << "] "
           << "[" << LeveltoStr(_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;
};

老样子先看私有成员前五个对应开头提到的我们希望日志中包含的信息即

  • 时间戳
  • 日志等级
  • 日志内容
  • 文件名(可选)
  • 行号(可选)
  • 进程/线程id(可选)

我们需要把这五个信息合并成一条信息,这就是_loginfo的作用

我们希望logger(DEBUG, "main.cpp", 42) << "This is a debug message";时通过_logger来实现在屏幕上的打印,具体后面详说,所以在LogMessage中我们还定义Logger &_logger成员

LogMessage的构造函数就是输出一条完整信息,这里我们利用std::stringstream

std::stringstream

std::stringstream是 C++ 标准库中的一个类,它提供了内存中的字符串流处理功能,结合了字符串的灵活性和流的操作接口。它是 <sstream>头文件的一部分。

用法

std::stringstream ss;

// 向流中插入数据
ss << "Hello, " << 42 << " " << 3.14 << " " << std::boolalpha << true;

// 获取完整的字符串
std::string result = ss.str();
std::cout << result; // 输出: Hello, 42 3.14 true

在我们LogMessage中它的作用有两个

//1
    _loginfo = ss.str(); // 获取格式化后的字符串

//2
    ss << info;          // 将任意类型转换为字符串
    _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);
    }

日志的右半部分是可变的,也许是整数,也许是字符串还可能是其他类型,这里我们要设置一个模板

通过stringstream类的ss对象来实现类型转换并且追加到字符串_loginfo中,返回类型是LogMessage是因为我们期望<<能实现连续使用即下面这样

logger(LogLevel::DEBUG, "main.cc", 10) << "hello world" << 3.143;

我们在上面实现的仿函数没有使用引用,当我们向上面代码这样使用时,logger返回的是一份临时对象(类型是LogMessage包含_loginfo的信息)临时对象在下一行代码就会被析构,析构时就会调用SyncLog函数打印对应的信息在屏幕上

最后收尾工作

// 全局日志对象
Logger logger;

// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Select_Console_Log_Strategy() logger.SelectConsoleLogStrategy()
#define Select_File_Log_Strategy() logger.SelectFileLogStrategy()

定义全局对象这样我们就能直接选择模式了

Select_Console_Log_Strategy();
LOG(LogLevel::DEBUG) << "hello world" << 3.141;
LOG(LogLevel::DEBUG) << "hello world" << 3.142;

Select_File_Log_Strategy();
LOG(LogLevel::DEBUG) << "hello world" << 3.143;
LOG(LogLevel::DEBUG) << "hello world" << 3.144;

这就是日志代码的讲解,下面是完整代码

#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 LogMudoule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";
    // 策略模式,C++多态特性
    // 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
    //  刷新策略基类
    class LogStrategy
    {
    public:
        virtual ~LogStrategy()
        {
        }
        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);
            const 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:
        const std::string _path;
        const std::string _file;
        Mutex _mutex;
    };

    // 1. 形成日志等级
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    std::string LeveltoStr(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;
    }

    class Logger
    {
    public:
        Logger()
        {
          SelectFileLogStrategy();
        }
        void SelectFileLogStrategy()
        {
            _fflush_strategy = std::make_unique<FileLogStrategy>();
        }
        void SelectConsoleLogStrategy()
        {
            _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 << "] "
                   << "[" << LeveltoStr(_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);
        }
        ~Logger()
        {
        }
    private:
        std::unique_ptr<LogStrategy> _fflush_strategy;
    };
    // 全局日志对象
    Logger logger;

    // 使用宏,简化用户操作,获取文件名和行号
    #define LOG(level) logger(level, __FILE__, __LINE__)
    #define Select_Console_Log_Strategy() logger.SelectConsoleLogStrategy()
    #define Select_File_Log_Strategy() logger.SelectFileLogStrategy()
}
#endif

评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值