日志与策略模式
什么是设计模式
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
注意事项
- 时间精度 :time ()只提供秒级精度,如果需要更高精度,考虑使用:
- gettimeofday()(微秒级,已过时但广泛支持)
- clock_gettime()(纳秒级,现代Linux推荐)
- 时区问题:time()返回的是从 UTC 时间 1970-01-01 00:00:00 开始的秒数,不包含时区信息。
- 2038年问题:在32位系统中,time_t 通常是有符号32位整数,会在2038年溢出。现代64位系统已解决此问题。
- 线程安全: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;
}
