实现简单日志

日志与策略模式

什么是设计模式

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;
}
相关推荐
Mike117.1 小时前
GBase 8a 日期边界写法和时间窗口取数偏差
数据库
SPC的存折3 小时前
1、Redis数据库基础
linux·运维·服务器·数据库·redis·缓存
爱学习的小囧4 小时前
VMware ESXi 6.7U3v 新版特性、驱动集成教程和资源包、部署教程及高频问答详情
运维·服务器·虚拟化·esxi6.7·esxi蟹卡驱动
小疙瘩4 小时前
只是记录自己发布若依分离系统到linux过程中遇到的问题
linux·运维·服务器
dldw7774 小时前
IE无法正常登录windows2000server的FTP服务器
运维·服务器·网络
我是伪码农5 小时前
外卖餐具智能推荐
linux·服务器·前端
汤愈韬6 小时前
下一代防火墙通用原理
运维·服务器·网络·security
皮皮林5516 小时前
强烈建议大家使用 Linux 做开发?
linux
IMPYLH6 小时前
Linux 的 od 命令
linux·运维·服务器·bash
And_Ii6 小时前
LCR 168. 丑数
c++