实现简单日志

日志与策略模式

什么是设计模式

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

日志认识

Linux 日志:系统的"黑匣子"与"日记本"

简单来说,Linux 日志是系统运行时持续记录的、按时间排序的事件流水账。它就像系统的"黑匣子"和"日记本",忠实地记录下内核、服务、应用程序和用户的每一个重要动作。

核心比喻

  • 系统的史官: 不带有情感地记录"谁,在什么时候,做了什么,结果是成功还是失败"。
  • 故障侦探: 当系统出现问题(无法启动、服务崩溃、网络不通)时,它是你调查原因的第一现场。
  • 安全卫士: 记录所有登录尝试、权限变更,是发现入侵和异常行为的关键证据。
  • 性能分析师: 通过分析日志,可以了解系统负载、资源消耗和应用程序的运行效率。

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

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

• 时间戳

• 日志等级

• 日志内容

以下几个指标是可选的

• 文件名行号

• 进程,线程相关id信息等

日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。

这里我们采用设计模式-策略模式来进行日志的设计

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

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

日志封装

time函数

time() 是C标准库中用于获取当前时间的函数,返回自 Unix 纪元(1970-01-01 00:00:00 UTC)以来经过的秒数。

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

time_t time(time_t *tloc);

参数说明

参数 说明
tloc 可选参数。如果不为 NULL,则当前时间也会存储到这个指针指向的位置

返回值

  • 成功:返回当前时间(从 1970-01-01 00:00:00 UTC 开始的秒数)
  • 失败:返回 time_t(-1),并设置 errno

注意事项

  1. 时间精度 :time ()只提供秒级精度,如果需要更高精度,考虑使用:
    • gettimeofday()(微秒级,已过时但广泛支持)
    • clock_gettime()(纳秒级,现代Linux推荐)
  2. 时区问题:time()返回的是从 UTC 时间 1970-01-01 00:00:00 开始的秒数,不包含时区信息。
  3. 2038年问题:在32位系统中,time_t 通常是有符号32位整数,会在2038年溢出。现代64位系统已解决此问题。
  4. 线程安全:time() 本身是线程安全的,但转换函数如 localtime()不是线程安全的。

localtime_r 函数

localtime_r 是 Linux/Unix 系统中线程安全的本地时间转换函数,用于将 time_t 类型的时间戳转换为本地时间的 struct tm 结构。

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

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

参数说明

参数 说明
timep 指向时间戳的指针(从1970-01-01 00:00:00 UTC开始的秒数)
result(输出型参数) 用户提供的缓冲区,用于存储转换结果

返回值

  • 成功:返回指向 result 的指针
  • 失败:返回 NULL

struct tm 结构体成员

cpp 复制代码
struct tm {
    int tm_sec;     // 秒 [0, 60](60用于闰秒)
    int tm_min;     // 分 [0, 59]
    int tm_hour;    // 时 [0, 23]
    int tm_mday;    // 月中的日 [1, 31]
    int tm_mon;     // 月 [0, 11](0 = 一月)
    int tm_year;    // 年=(year-1900)(从1900年开始)
    int tm_wday;    // 星期 [0, 6](0 = 周日)
    int tm_yday;    // 年中的日 [0, 365]
    int tm_isdst;   // 夏令时标志:>0(启用)、0(禁用)、<0(未知)
};

与 localtime() 的区别

特性 localtime() localtime_r()
线程安全 ❌ 否(使用静态缓冲区) ✅ 是(用户提供缓冲区)
可重入 ❌ 否 ✅ 是
缓冲区 内部静态缓冲区 用户提供的缓冲区
多线程 不安全 安全

相关函数

函数 描述
gmtime_r() 线程安全的 UTC 时间转换
strftime() 格式化时间字符串
mktime() 将 struct tm 转换为 time_t
tzset() 设置时区信息

代码实践

cpp 复制代码
log_test:log_test.cpp
	g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
	rm -f log_test
cpp 复制代码
#pragma once

#include <iostream>
#include <mutex>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    pthread_mutex_t *Get()
    {
        return &_lock;
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard
{
public:
    LockGuard(Mutex *_mutex):_mutexp(_mutex)
    {
        _mutexp->Lock();
    }
    ~LockGuard()
    {
        _mutexp->Unlock();
    }
private:
    Mutex *_mutexp;
};
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <filesystem> // C++17 文件操作
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp"

// 规定出场景的日志等级
enum class LogLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

// 日志转换成为字符串
std::string Level2String(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";
    }
}

// 根据时间戳,获取可读性较强的时间信息
// 20XX-08-04 12:27:03
std::string GetCurrentTime()
{
    // 1. 获取时间戳
    time_t currtime = time(nullptr);

    // 2. 如何把时间戳转换成为20XX-08-04 12:27:03
    struct tm currtm;
    localtime_r(&currtime, &currtm);

    // 3. 转换成为字符串
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
             currtm.tm_year + 1900,
             currtm.tm_mon + 1,
             currtm.tm_mday,
             currtm.tm_hour,
             currtm.tm_min,
             currtm.tm_sec);

    return timebuffer;
}

// 策略模式,策略接口
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备(显示器,文件)
// 基类方法
class LogStrategy
{
public:
    // 不同模式核心是刷新方式的不同
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &logmessage) = 0;
};

// 控制台日志策略,就是日志只向显示器打印,方便我们debug
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
    ~ConsoleLogStrategy()
    {
    }
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }

private:
    // 显示器也是临界资源,保证输出线程安全
    Mutex _lock;
};

// 默认路径和日志名称
const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";

// 文件日志策略
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
    // 构造函数,建立出来指定的目录结构和文件结构
    FileLogStrategy(const std::string &dir = logdefaultdir,
                    const std::string filename = logfilename)
        : _dir_path_name(dir), _filename(filename)
    {
        LockGuard lockguard(&_lock);
        if (std::filesystem::exists(_dir_path_name))
        {
            return;
        }
        try
        {
            std::filesystem::create_directories(_dir_path_name);
        }
        catch (const std::filesystem::filesystem_error &e)
        {
            std::cerr << e.what() << "\r\n";
        }
    }
    // 将一条日志信息写入到文件中
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::string target = _dir_path_name;
            target += "/";
            target += _filename;
            // 追加方式
            std::ofstream out(target.c_str(), std::ios::app); // append
            if (!out.is_open())
            {
                return;
            }
            out << logmessage << "\n"; // out.write
            out.close();
        }
    }

    ~FileLogStrategy()
    {
    }

private:
    std::string _dir_path_name; // log
    std::string _filename;      // hello.log => log/hello.log
    Mutex _lock;
};

// 具体的日志类
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:
    Logger()
    {
    }
    void EnableConsoleLogStrategy()
    {
        _strategy = std::make_unique<ConsoleLogStrategy>();
    }
    void EnableFileLogStrategy()
    {
        _strategy = std::make_unique<FileLogStrategy>();
    }

    // 内部类,实现RAII风格的日志格式化和刷新
    // 这个LogMessage,表示一条完整的日志对象
    class LogMessage
    {
    public:
        // RAII风格,构造的时候构建好日志头部信息
        LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
            : _curr_time(GetCurrentTime()),
              _level(level),
              _pid(getpid()),
              _filename(filename),
              _line(line),
              _logger(logger)
        {
            // stringstream不允许拷贝,所以这里就当做格式化功能使用
            std::stringstream ss;
            ss << "[" << _curr_time << "] "
               << "[" << Level2String(_level) << "] "
               << "[" << _pid << "] "
               << "[" << _filename << "] "
               << "[" << _line << "]"
               << " - ";
            _loginfo = ss.str();
        }
        // 重载 << 支持C++风格的日志输入,使用模版,表示支持任意类型
        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            std::stringstream ss;
            ss << info;
            _loginfo += ss.str();
            return *this;
        }
        // RAII风格,析构的时候进行日志持久化,采用指定的策略
        ~LogMessage()
        {
            if (_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
        }

    private:
        std::string _curr_time; // 日志时间
        LogLevel _level;        // 日志等级
        pid_t _pid;             // 进程pid
        std::string _filename;
        int _line;

        std::string _loginfo; // 一条合并完成的,完整的日志信息
        Logger &_logger;      // 引用外部logger类, 方便使用策略进行刷新
    };
    // 故意拷贝,形成LogMessage临时对象,后续在被<<时,会被持续引用,
    // 直到完成输入,才会自动析构临时LogMessage,至此也完成了日志的显示或者刷新
    // 同时,形成的临时对象内包含独立日志数据
    // 未来采用宏替换,进行文件名和代码行数的获取
    LogMessage operator()(LogLevel level, std::string filename, int line)
    {
        return LogMessage(level, filename, line, *this);
    }
    ~Logger()
    {
    }

private:
    // 写入日志的策略
    std::unique_ptr<LogStrategy> _strategy;
};

// 定义全局的logger对象
Logger logger;

// 使用宏,可以进行代码插入,方便随时获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)

// 提供选择使用何种日志策略的方法
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()
cpp 复制代码
#include "Logger.hpp"
#include <unistd.h>

int main()
{
    //向显示器刷新
    EnableConsoleLogStrategy();
    //EnableFileLogStrategy();

    // RAII风格的日志构建和输出刷新的过程
    LOG(LogLevel::ERROR) << "hello world" << ", 3.14 " << 123;
    LOG(LogLevel::WARNING) << "hello world" << ", 3.14 " << 123;
    LOG(LogLevel::ERROR) << "hello world" << ", 3.14 " << 123;
    LOG(LogLevel::ERROR) << "hello world" << ", 3.14 " << 123;
    return 0;
}
相关推荐
ZouZou老师8 小时前
C++设计模式之装饰器模式:以家具生产为例
c++·设计模式·装饰器模式
HerayChen8 小时前
HbuilderX 内存溢出报错
java·大数据·linux
ZouZou老师8 小时前
C++设计模式之桥接模式:以家具生产为例
c++·设计模式·桥接模式
程序员小白条8 小时前
0经验如何找实习?
java·开发语言·数据结构·数据库·链表
呱呱巨基8 小时前
Linux 进程概念
linux·c++·笔记·学习
土星云SaturnCloud8 小时前
不止是替代:从机械风扇的可靠性困局,看服务器散热技术新范式
服务器·网络·人工智能·ai
liulilittle8 小时前
C++ 浮点数封装。
linux·服务器·开发语言·前端·网络·数据库·c++
Xの哲學9 小时前
Linux Miscdevice深度剖析:从原理到实战的完整指南
linux·服务器·算法·架构·边缘计算
郭涤生9 小时前
QT 架构笔记
java·数据库·系统架构