目录
一、认识日志
二、时间的等级划分
三、日志的输出端
3.1 保存至文件
四、日志的部分信息
4.1 日志等级
4.2 日志时间
五、加载日志
六、日志的宏编写
七、ThreadPool + Log
一、认识日志
-
记录事件: 日志用于记录系统运行过程中发生的各种事件,包括错误、警告、信息和调试信息等。通过这些记录,可以了解系统在某一时刻的具体状态和行为。
-
问题诊断: 当系统出现故障或异常时,通过查看日志可以追踪问题的发生原因、定位错误源头,从而快速解决问题。详细的日志信息能够帮助开发人员和运维人员理解问题的背景和上下文。
-
性能监控: 日志可以记录系统的性能数据,如处理时间、响应时间和资源使用情况等。这些信息对于监控系统性能和优化系统效率非常重要。
-
安全审计: 日志记录用户操作、系统访问和安全事件等信息,可以用于安全审计和合规性检查,确保系统的安全性和合规性。
-
运行分析: 通过分析日志数据,可以发现系统运行中的趋势、模式和潜在问题,从而做出更好的决策和改进系统设计。
-
调试和开发: 在开发和测试过程中,日志信息可以帮助开发人员了解代码执行流程和程序状态,便于调试和优化代码。
总之,日志在系统运行、维护、开发和安全等各个方面都起着非常重要的作用,是确保系统稳定性、可靠性和安全性的重要手段。
二、时间的等级划分
后续通过日志的等级,就可以选择后续操作,例如直接退出、继续执行等。
enum Level
{DEBUG = 0,INFO,WARNING,ERROR,FATAL
};
三、日志的输出端
日志可以向显示器中输入,也可以向某文件中输入。
3.1 保存至文件
C++根据文件内容的数据格式分为二进制文件和文本文件。
采用文件流对象操作文件的一般步骤:
1. 定义一个文件流对象
ifstream ifile(只输入用)
ofstream ofile(只输出用)
fstream iofile(既输入又输出用)
2. 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
3. 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写
4. 关闭文件
bool gIsSave = false;//后续通过IsSave选择是否保存到文件
const std::string logname = "log.txt";
void SaveFile(const std::string &filename, const std::string &message)
{std::ofstream out(filename, std::ios::app);if (!out.is_open())return;out << message;out.close();
}
四、日志的部分信息
4.1 日志等级
首先要明确一点,日志是要输出给读者看的,我们枚举的日志等级毕竟还是整型,输出后也会导致读者看不懂的问题,所以就需要有一个函数将对应的枚举类型转化成字符串,以便于后续的输出:
std::string LevelToString(int level)
{switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknown";}
}
4.2 日志时间
日志的名称也能看出其带有很强的时间属性,所以在日志中获取操作的时间也是必不可少的操作。
这里介绍几个C++中与时间有关的概念,包括数据类型、结构体、函数等:
time_t :表述时间的数据类型
struct tm : 包含时间的各个单位的结构体,如下图,第一列为结构体成员,第二列为成员的数据类型,第三列为成员的意义,第四列为成员的取值范围。
其中,特别说明的是 tm_year 与 tm_mon ,它们分别表示的是从1900年后到现在的年数与此时的月份数,但是是从零开始的,所以在输出时,tm_year一般+1900,tm_mon一般+1。
time : 输出型参数,获取当前时间赋值给传入的 time_t 类型的变量
localtime : 返回一个struct tm 的结构体,可以将传入的 time_t 类型变量的值,依次赋值给结构体成员。
下面附上我粗鄙的理解:定义了一个 time_t 类型的变量 curr_time ,使用 time 可以将当前时间赋值给 curr_time ,假设当前时间为2024/7/16 21:48,那么curr_time中可能是一系列连续的数字标识当前时间,例如是202407162148,这样比较粗鄙,猜想系统中的机制肯定更复杂,是很不适合读者去读的,localtime 就可以将该串转化成年=2024,月=7,日=16(事实肯定不是这样因为年是从1900开始,月从0开始,但是这样说比较好理解)......然后就比较方便读者阅读了。
std::string GetTimeString()
{time_t curr_time = time(nullptr);struct tm *format_time = localtime(&curr_time);if (format_time == nullptr)return "None";char time_buffer[1024];snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",format_time->tm_year + 1900, format_time->tm_mon + 1,format_time->tm_mday,format_time->tm_hour,format_time->tm_min,format_time->tm_sec);return time_buffer;
}
五、加载日志
这里采用的类似C语言 printf 的传入参数。
va_list
、va_start
、va_end
、vsnprintf
:
- 用于处理可变参数列表的 C 标准库函数。
va_list arg
:声明一个可变参数列表变量。va_start(arg, format)
:初始化可变参数列表,从format
开始。vsnprintf(buffer, sizeof(buffer), format, arg)
:将可变参数格式化为字符串并存储到buffer
中。va_end(arg)
:结束可变参数列表的处理。
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{std::string levelstr = LevelToString(level);std::string timestr = GetTimeString();pid_t selfid = getpid();char buffer[1024];va_list arg;va_start(arg, format);vsnprintf(buffer, sizeof(buffer), format, arg);va_end(arg);std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +"[" + std::to_string(selfid) + "]" +"[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer;LockGuard lockguard(&lock);if (!issave)std::cout << message;elseSaveFile(logname, message);
}
六、日志的宏编写
do while(0)常用于宏定义中代码块的定义,__FILE__, __LINE__都是系统提供的,可以输出当前文件与当前行,此时再使用宏调用日志就简单多了。
#define LOG(level, format, ...) \do \{ \LogMessage(__FILE__, __LINE__, gIsSave, level, format, ##__VA_ARGS__); \} while (0)#define EnableFile() \do \{ \gIsSave = true; \} while (0)
#define EnableScreen() \do \{ \gIsSave = false; \} while (0)
LOG(INFO, "%s is quit...\n", thread.name().c_str());
七、ThreadPool + Log
注意,这里日志打印形式纯属个人意愿,而且需要打印出线程名的话,ThreadPool 的相关函数的传参也会进行修改,请注意甄别。
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
#include "Log.hpp"
using namespace ThreadModule;
const static int gdefaultthreadnum = 3;template <typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void ThreadSleep(){pthread_cond_wait(&_cond, &_mutex);}void ThreadWakeup(){pthread_cond_signal(&_cond);}void ThreadWakeupAll(){pthread_cond_broadcast(&_cond);}
public:ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum), _waitnum(0), _isrunning(false){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);LOG(INFO, "ThreadPool Construct()\n");}void HandlerTask(std::string name){LOG(INFO, "%s is running...\n", name.c_str());while (true){LockQueue();while (_task_queue.empty() && _isrunning){_waitnum++;ThreadSleep();_waitnum--;}if (_task_queue.empty() && !_isrunning){UnlockQueue();break;}T task = _task_queue.front();_task_queue.pop();UnlockQueue();LOG(DEBUG, "%s get a task", name.c_str());task();LOG(DEBUG, "%s handler a task, result is: %s\n", name.c_str(), t.ResultToString().c_str());}}void InitThreadPool(){for (int num = 0; num < _threadnum; num++){std::string name = "thread- " + std::to_string(num + 1);_threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1), name);LOG(INFO, "init thread %s done\n", name.c_str());}_isrunning = true;}void Start(){for (auto &thread : _threads){thread.Start();}}void Stop(){LockQueue();_isrunning = false; // 线程池退出ThreadWakeupAll();UnlockQueue();}void Wait(){for (auto &thread : _threads){thread.Join();LOG(INFO, "%s is quit...\n", thread.name().c_str());}}bool Enqueue(const T &task){bool ret = false;LockQueue();if (_isrunning){_task_queue.push(task);if (_waitnum > 0){ThreadWakeup();}LOG(DEBUG, "enqueue task success\n");ret = true;}UnlockQueue();return ret;}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:int _threadnum;std::vector<Thread> _threads;std::queue<T> _task_queue;pthread_mutex_t _mutex;pthread_cond_t _cond;int _waitnum;bool _isrunning;
};