日志系统(最新版)

基础知识

日志,由服务器自动创建,并记录运行状态,错误信息,访问数据的文件。

同步日志,日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。

生产者-消费者模型,并发编程中的经典模型。以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中push消息,消费者线程从缓冲区中pop消息。

阻塞队列,将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区。

异步日志,将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。

单例模式,最简单也是被问到最多的设计模式之一,保证一个类只创建一个实例,同时提供全局访问的方法。

整体概述

本项目中,使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式。

其中异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push进队列,写线程从队列中取出内容,写入日志文件。

日志系统大致可以分成两部分,其一是单例模式与阻塞队列的定义,其二是日志类的定义与使用。

本文内容

本篇将介绍单例模式与阻塞队列的定义,具体的涉及到单例模式、生产者-消费者模型,阻塞队列的代码实现。

单例模式,描述懒汉与饿汉两种单例模式,并结合线程安全进行讨论。

生产者-消费者模型,描述条件变量,基于该同步机制实现简单的生产者-消费者模型。

代码实现,结合代码对阻塞队列的设计进行详解。

单例模式

单例模式作为最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。

单例模式有两种实现方法,分别是懒汉和饿汉模式。顾名思义,懒汉模式,即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化;饿汉模式,即迫不及待,在程序运行时立即初始化。

#include <pthread.h>// 单例类,确保整个程序只有一个实例
class single {
private:// 私有静态指针变量指向唯一的实例static single* p;// 静态互斥锁,用于线程安全的实例创建static pthread_mutex_t lock;// 私有化构造函数,防止外部创建实例single() {// 初始化互斥锁pthread_mutex_init(&lock, nullptr);}// 私有化析构函数,避免外部删除实例~single() {}public:// 公有静态方法,用于获取单例实例static single* getinstance();
};// 定义静态互斥锁变量
pthread_mutex_t single::lock;// 静态实例指针初始化为nullptr
single* single::p = nullptr;/*** 获取单例实例的方法* 该方法确保在多线程环境下安全地创建单例对象* 返回值:单例对象指针*/
single* single::getinstance() {// 如果实例尚未创建if (nullptr == p) {// 加锁,防止多线程同时创建实例pthread_mutex_lock(&lock);// 再次检查实例是否创建,避免不必要的创建if (nullptr == p) {// 实例化单例对象p = new single;}// 解锁pthread_mutex_unlock(&lock);}// 返回单例实例return p;
}

为什么要用双检测,只检测一次不行吗?

如果只检测一次,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。

局部静态变量之线程安全懒汉模式

前面的双检测锁模式,写起来不太优雅,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。

// 单例类,防止外部实例化和删除
class single {
private:// 私有构造函数,防止外部实例化single() {}// 私有析构函数,防止外部delete~single() {}public:// 获取单例对象的静态方法static single* getinstance();};// 获取单例对象的实现
single* single::getinstance() {// 静态局部变量,保证其只被实例化一次static single obj;// 返回单例对象的地址return &obj;
}

这时候有人说了,这种方法不加锁会不会造成线程安全问题?

其实,C++0X以后,要求编译器保证内部静态变量的线程安全性,故C++0x之后该实现是线程安全的,C++0x之前仍需加锁,其中C++0x是C++11标准成为正式标准之前的草案临时名字。

所以,如果使用C++11之前的标准,还是需要加锁,这里同样给出加锁的版本。

// 单例类single,确保整个程序中只有一个实例
class single {
private:// 静态互斥锁,用于线程安全的实例化static pthread_mutex_t lock;// 私有构造函数,防止外部直接构造对象single() {// 初始化互斥锁pthread_mutex_init(&lock, nullptr);}// 私有析构函数,避免对象被删除~single(){}public:// 获取单例对象的静态方法static single* getinstance();
};// 静态互斥锁变量
pthread_mutex_t single::lock;// 单例模式的线程安全实例化方法
single* single::getinstance() {// 加锁,保证线程安全pthread_mutex_lock(&lock);// 静态局部变量,保证只在第一次调用时初始化static single obj;// 解锁pthread_mutex_unlock(&lock);// 返回单例对象的地址return &obj;
}
饿汉模式

饿汉模式不需要用锁,就可以实现线程安全。原因在于,在程序运行时就定义了对象,并对其初始化。之后,不管哪个线程调用成员函数getinstance(),都只不过是返回一个对象的指针而已。所以是线程安全的,不需要在获取实例的成员函数中加锁。

#include <iostream>
using namespace std;// 定义一个单例类
class single{
private:// 静态成员指针,用于存储单例对象的唯一实例static single* p;// 私有构造函数,防止外部直接创建对象single() {}// 私有析构函数,防止外部删除对象~single() {} 
public: // 静态方法,返回单例对象的指针static single* getinstance();
};// 静态成员变量初始化,创建并初始化单例对象
single* single::p = new single();// 获取单例对象的指针方法
single* single::getinstance() {return p;
}int main() {// 获取单例对象的指针single* p1 = single::getinstance();// 再次获取单例对象的指针single* p2 = single::getinstance();// 检查两次获取的指针是否相同,验证单例特性if (p1 == p2) {cout << "same" << endl;}return 0;
}

饿汉模式虽好,但其存在隐藏的问题,在于非静态对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。

  • 懒汉模式 在第一次调用 getInstance() 时才会实例化对象,适用于对资源敏感的场景,但需要处理线程安全问题。
  • 饿汉模式 在类加载时就创建对象,无需考虑线程安全问题,适用于资源不敏感且确保对象一定会被使用的场景。

条件变量与生产者-消费者模型

条件变量API与陷阱

条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值时,唤醒等待这个共享数据的线程。

基础API
  • pthread_cond_init函数,用于初始化条件变量
  • pthread_cond_destory函数,销毁条件变量
  • pthread_cond_broadcast函数,以广播的方式唤醒所有等待目标条件变量的线程
  • pthread_cond_wait函数,用于等待目标条件变量。该函数调用时需要传入 mutex参数(加锁的互斥锁) ,函数执行时,先把调用线程放入条件变量的请求队列,然后将互斥锁mutex解锁,当函数成功返回为0时,表示重新抢到了互斥锁,互斥锁会再次被锁上, 也就是说函数内部会有一次解锁和加锁操作.

使用pthread_cond_wait方式如下:

pthread_mutex_lock(&mutex)while(线程执行条件是否成立) {pthread_cond_wait(&cond, &mutex);
}pthread_mutex_unlock(&mutex);

pthread_cond_wait执行后的内部操作分为以下几步:

  • 将线程放在条件变量的请求队列后,内部解锁
  • 线程等待被pthread_cond_broadcast信号唤醒或者pthread_cond_signal信号唤醒,唤醒后去竞争锁
  • 若竞争到互斥锁,内部再次加锁
陷阱一

使用前要加锁,为什么要加锁?

多线程访问,为了避免资源竞争,所以要加锁,使得每个线程互斥的访问公有资源。

pthread_cond_wait内部为什么要解锁?

如果while或者if判断的时候,满足执行条件,线程便会调用pthread_cond_wait阻塞自己,此时它还在持有锁,如果他不解锁,那么其他线程将会无法访问公有资源。

具体到pthread_cond_wait的内部实现,当pthread_cond_wait被调用线程阻塞的时候,pthread_cond_wait会自动释放互斥锁。

为什么要把调用线程放入条件变量的请求队列后再解锁?

线程是并发执行的,如果在把调用线程A放在等待队列之前,就释放了互斥锁,这就意味着其他线程比如线程B可以获得互斥锁去访问公有资源,这时候线程A所等待的条件改变了,但是它没有被放在等待队列上,导致A忽略了等待条件被满足的信号。

倘若在线程A调用pthread_cond_wait开始,到把A放在等待队列的过程中,都持有互斥锁,其他线程无法得到互斥锁,就不能改变公有资源。

为什么最后还要加锁?

将线程放在条件变量的请求队列后,将其解锁,此时等待被唤醒,若成功竞争到互斥锁,再次加锁。

陷阱二

为什么判断线程执行的条件用while而不是if?

一般来说,在多线程资源竞争的时候,在一个使用资源的线程里面(消费者)判断资源是否可用,不可用,便调用pthread_cond_wait,在另一个线程里面(生产者)如果判断资源可用的话,则调用pthread_cond_signal发送一个资源可用信号。

在wait成功之后,资源就一定可以被使用么?答案是否定的,如果同时有两个或者两个以上的线程正在等待此资源,wait返回后,资源可能已经被使用了。

再具体点,有可能多个线程都在等待这个资源可用的信号,信号发出后只有一个资源可用,但是有A,B两个线程都在等待,B比较速度快,获得互斥锁,然后加锁,消耗资源,然后解锁,之后A获得互斥锁,但A回去发现资源已经被使用了,它便有两个选择,一个是去访问不存在的资源,另一个就是继续等待,那么继续等待下去的条件就是使用while,要不然使用if的话pthread_cond_wait返回后,就会顺序执行下去。

所以,在这种情况下,应该使用while而不是if:

while(resource == FALSE) {pthread_cond_wait(&cond, &mutex);
}

如果只有一个消费者,那么使用if是可以的

生产者-消费者模型

这里摘抄《Unix 环境高级编程》中第11章线程关于pthread_cond_wait的介绍中有一个生产者-消费者的例子P311,其中,process_msg相当于消费者,enqueue_msg相当于生产者,struct msg* workq作为缓冲队列。

生产者和消费者是互斥关系,两者对缓冲区访问互斥,同时生产者和消费者又是一个相互协作与同步的关系,只有生产者生产之后,消费者才能消费。

生产者-消费者模型的优点

1)解耦合:将生产者类和消费者类进行解耦,消除代码之间的依赖性,简化工作负载的管理。

2)复用:通过将生产者类和消费者类独立开来,那么可以对生产者类和消费者类进行独立的复用与扩展。

3)调整并发数:由于生产者和消费者的处理速度是不一样的,可以调整并发数,给予慢的一方多的并发数,来提高任务的处理速度。

4)异步:对于生产者和消费者来说能够各司其职,生产者只需要关心缓冲区是否还有数据,不需要等待消费者处理完;同样的对于消费者来说,也只需要关注缓冲区的内容,不需要关注生产者,通过异步的方式支持高并发,将一个耗时的流程拆成生产和消费两个阶段,这样生产者因为执行 put() 的时间比较短,而支持高并发。

5)支持分布式:生产者和消费者通过队列进行通讯,所以不需要运行在同一台机器上,在分布式环境中可以通过 redis 的 list 作为队列,而消费者只需要轮询队列中是否有数据。同时还能支持集群的伸缩性,当某台机器宕掉的时候,不会导致整个集群宕掉。

#include <pthread.h>// 消息结构体定义,用于在队列中传递消息
struct msg
{struct msg* m_next; // 指向下一个消息的指针// value.. // 这里可以存储消息的具体内容
};// 工作队列的全局变量,头指针指向队列中的第一个消息
struct msg* workq;
// 初始化条件变量,用于在队列有消息时通知消费者线程
pthread_cond_t qread = PTHREAD_COND_INITIALIZER;
// 初始化互斥锁,用于保护工作队列的线程安全
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;/*** 处理消息的函数* * 本函数旨在从一个消息队列中不断取出消息并处理之* 它通过线程互斥量(qlock)来保证消息队列的访问是线程安全的* 同时,它使用条件变量(qread)来等待新消息的到达*/
void process_msg() {// 指向当前正在处理的消息struct msg* mp;// 持续等待并处理消息for (;;) {// 加锁以保护共享资源pthread_mutex_lock(&qlock);// 如果队列为空,则解锁并等待新消息// 这里需要用while,而不是if,以防止虚假唤醒while (workq == nullptr) {pthread_cond_wait(&qread, &qlock);}// 取出队列头部的消息mp = workq;// 从队列中移除已取出的消息workq = mp->m_next;// 解锁以允许其他线程访问队列pthread_mutex_unlock(&qlock);// 此处应处理消息mp// 注意:具体的消息处理逻辑根据实际需求实现,此处省略}
}/*** 将消息加入队列的函数* @param mp 指向要加入队列的消息的指针*/
void enqueue_msg(struct msg* mp) {// 加锁保护工作队列pthread_mutex_lock(&qlock);// 将新消息加入队列头部mp->m_next = workq;workq = mp;// 解锁以允许其他线程操作队列pthread_mutex_unlock(&qlock);// 此时另一个线程可能在signal之前拿走了mp元素// 通过signal操作唤醒等待在qread上的线程pthread_cond_signal(&qread);// 如果在signal之前mp元素被拿走,workq仍然是NULL,需要继续等待
}

阻塞队列代码分析

阻塞队列类中封装了生产者-消费者模型,其中push成员是生产者,pop成员是消费者。

阻塞队列中,使用了循环数组实现了队列,作为两者共享缓冲区,当然了,队列也可以使用STL中的queue。

自定义队列

当队列为空时,从队列中获取元素的线程将会被挂起;当队列是满时,往队列里添加元素的线程将会挂起。

阻塞队列类中,有些代码比较简单,这里仅对push和pop成员进行详解。

/*************************************************************
*循环数组实现的阻塞队列,m_back = (m_back + 1) % m_max_size;  
*线程安全,每个操作前都要先加互斥锁,操作完后,再解锁
**************************************************************/
#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H#include "../lock/locker.h"
#include <stdlib.h>
#include <sys/time.h>template<class T>
class block_queue {
public:/*** 构造函数,初始化队列* @param max_size 队列的最大容量,默认为1000*/block_queue(int max_size = 1000) {// 检查最大容量是否合法if (max_size <= 0) {exit(-1);}m_max_size = max_size;// 分配存储空间m_array = new T[max_size];// 初始化队列大小为0m_size = 0;// 初始化队列前后指针m_front = -1;m_back = -1;}// 析构函数,负责释放队列所占用的资源
~block_queue() {// 加锁以确保线程安全m_mutex.lock();// 检查数组是否被分配,如果被分配则释放内存if (m_array != nullptr) {delete[] m_array;}// 解锁以释放互斥锁m_mutex.unlock();
}// 清空队列,重置队列状态
void clear() {// 加锁以确保线程安全m_mutex.lock();// 重置队列大小、前后指针,表示队列为空m_size = 0;m_front = -1;m_back = -1;// 解锁以释放互斥锁m_mutex.unlock();
}// 检查队列是否已满
bool full() {// 加锁以确保线程安全m_mutex.lock();// 如果队列大小等于最大容量,则队列已满if (m_size >= m_max_size) {// 解锁并返回队列已满的状态m_mutex.unlock();return true;}// 解锁并返回队列未满的状态m_mutex.unlock();return false;
}// 检查队列是否为空
bool empty() {// 加锁以确保线程安全m_mutex.lock();// 如果队列大小为0,则队列为空if(m_size == 0) {// 解锁并返回队列为空的状态m_mutex.unlock();return true;}// 解锁并返回队列不为空的状态m_mutex.unlock();return false;
}// 获取队列的前端元素
bool front(T &value) {// 加锁以确保线程安全m_mutex.lock();// 如果队列为空,则不能获取元素if (0 == m_size) {m_mutex.unlock();return false;}// 将前端元素的值复制到传入的引用value = m_array[m_front];// 解锁以释放互斥锁m_mutex.unlock();return true;
}/*** 获取队列的尾部元素值,并解锁* * 该函数尝试从队列中获取尾部元素的值。在获取元素值前后,会加锁和解锁以确保线程安全。* 如果队列为空,则返回false,并且不会修改传入的引用参数。* * @param value 将被设置为队列尾部元素值的引用,如果队列不为空。* @return 如果成功获取队列尾部元素值,则返回true;如果队列为空,则返回false。*/
bool back(T& value) {m_mutex.lock(); // 加锁以保护对共享资源的访问if (0 == m_size) { // 检查队列是否为空m_mutex.unlock(); // 如果队列为空,解锁并返回falsereturn false;}value = m_array[back]; // 将队列尾部元素值赋给valuem_mutex.unlock(); // 解锁以释放对共享资源的访问return true; // 返回true表示成功获取队列尾部元素值
}/*** 获取队列当前的元素数量,并解锁* * 该函数通过加锁保护对共享资源的访问,以获取当前队列中的元素数量,* 然后解锁并返回这个数量。这用于获取队列的大小,而不改变队列的状态。* * @return 队列当前的元素数量。*/
int size() {int tmp = 0; // 用于存储队列当前的元素数量m_mutex.lock(); // 加锁以保护对共享资源的访问tmp = m_size; // 将队列当前的元素数量赋给tmpm_mutex.unlock(); // 解锁以释放对共享资源的访问return tmp; // 返回队列当前的元素数量
}/*** 获取队列最大的元素数量,并解锁* * 该函数通过加锁保护对共享资源的访问,以获取队列被创建时所允许的最大元素数量,* 然后解锁并返回这个最大数量。这用于确定队列的最大容量,而不改变队列的状态。* * @return 队列最大的元素数量。*/
int max_size() {int tmp = 0; // 用于存储队列最大的元素数量m_mutex.lock(); // 加锁以保护对共享资源的访问tmp = m_max_size; // 将队列最大的元素数量赋给tmpm_mutex.unlock(); // 解锁以释放对共享资源的访问return tmp; // 返回队列最大的元素数量
}/*** 向队列中推送一个新元素* * @param item 要推送的元素* @return bool 推送成功返回true,失败返回false*/
bool push(const T &item) {// 加锁以确保线程安全m_mutex.lock(); // 如果队列已满,则解锁并返回falseif (m_size >= m_max_size) {// 唤醒所有等待的线程,以便它们可以检查队列状态m_cond.broadcast();m_mutex.unlock();return false;}// 计算新元素应插入的位置,并存储元素m_back = (m_back + 1) % m_max_size;m_array[m_back] = item;m_size ++;// 通知所有等待的线程队列状态已改变m_cond.broadcast();// 解锁以释放资源m_mutex.unlock();return true;}/*** 从队列中弹出一个元素* * @param item 弹出的元素将存储在此处* @param ms_timeout 等待队列非空的超时时间(以毫秒为单位)* @return bool 弹出成功返回true,失败返回false*/
bool pop(T &item, int ms_timeout) {// 初始化绝对超时时间struct timespec t = {0, 0};// 获取当前时间struct timeval now = {0, 0};gettimeofday(&now, nullptr);// 加锁以确保线程安全m_mutex.lock();// 如果队列为空且在超时时间内未变为非空,则解锁并返回falseif (m_size <= 0) {// 设置超时的绝对时间t.tv_sec = now.tv_sec + ms_timeout / 1000;t.tv_nsec = (ms_timeout % 1000) * 1000;// 在超时时间内等待队列变为非空if (!m_cond.timewait(m_mutex.get(), t)) {m_mutex.unlock();return false;}}// 如果队列仍然为空,则解锁并返回falseif(m_size <= 0) {m_mutex.unlock();return false;}// 计算下一个要弹出元素的位置,并获取该元素m_front = (m_front + 1) % m_max_size;item = m_array[m_front];m_size --;// 解锁以释放资源m_mutex.unlock();return true;}private:// 互斥锁,用于线程安全locker m_mutex;// 条件变量,用于线程间同步cond m_cond;// 队列存储数组T* m_array;// 当前队列大小int m_size;// 队列最大容量int m_max_size;// 队列首元素索引int m_front;// 队列末元素索引int m_back;
};
#endif

日志系统分为两部分,其一是单例模式与阻塞队列的定义,其二是日志类的定义与使用。

本篇将介绍日志类的定义与使用,具体的涉及到基础API,流程图与日志类定义,功能实现。

基础API,描述fputs,可变参数宏__VA_ARGS__,fflush

流程图与日志类定义,描述日志系统整体运行流程,介绍日志类的具体定义

功能实现,结合代码分析同步、异步写文件逻辑,分析超行、按天分文件和日志分级的具体实现

基础API

为了更好的源码阅读体验,这里对一些API用法进行介绍。

fputs
#include <stdio.h>
int fputs(const char* str, FILE* stream);
  • str,一个数组,包含了要写入的以空字符终止的字符序列。
  • stream,指向FILE对象的指针,该FILE对象标识了要被写入字符串的流。
可变参数宏__VA_ARGS__

__VA_ARGS__是一个可变参数的宏,定义时宏定义中参数列表的最后一个参数为省略号,在实际使用时会发现有时会加##,有时又不加。

//最简单的定义
#define my_print1(...)  printf(__VA_ARGS__)//搭配va_list的format使用
#define my_print2(format, ...) printf(format, __VA_ARGS__)  
#define my_print3(format, ...) printf(format, ##__VA_ARGS__)

__VA_ARGS__宏前面加上##的作用在于,当可变参数的个数为0时,这里printf参数列表中的的##会把前面多余的","去掉,否则会编译出错,建议使用后面这种,使得程序更加健壮。

fflush
#include <stdio.h>
int fflush(FILE *stream);

fflush()会强迫将缓冲区内的数据写回参数stream 指定的文件中,如果参数stream 为NULL,fflush()会将所有打开的文件数据更新。

在使用多个输出函数连续进行多次输出到控制台时,有可能下一个数据再上一个数据还没输出完毕,还在输出缓冲区中时,下一个printf就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出现输出错误。

在prinf()后加上fflush(stdout); 强制马上输出到控制台,可以避免出现上述错误。

流程图与日志类定义

流程图
  • 日志文件
    • 局部变量的懒汉模式获取实例
    • 生成日志文件,并判断同步和异步写入方式
  • 同步
    • 判断是否分文件
    • 直接格式化输出内容,将信息写入日志文件
  • 异步
    • 判断是否分文件
    • 格式化输出内容,将内容写入阻塞队列,创建一个写线程,从阻塞队列取出内容写入日志文件

日志类定义

通过局部变量的懒汉单例模式创建日志实例,对其进行初始化生成日志文件后,格式化输出内容,并根据不同的写入方式,完成对应逻辑,写入日志文件。

日志类包括但不限于如下方法,

  • 公有的实例获取方法
  • 初始化日志文件方法
  • 异步日志写入方法,内部调用私有异步方法
  • 内容格式化方法
  • 刷新缓冲区

#ifndef LOG_H
#define LOG_H#include <iostream>
#include <string>
#include "block_queue.h"using namespace std;/*** 日志类,用于记录程序运行时的日志信息*/
class Log{
public:/*** 获取日志类的单例实例* @return 返回日志类的单例指针*/static Log* get_instance() {static Log instance;return &instance;}/*** 日志刷新线程入口函数* @param args 线程参数,未使用* @return 返回nullptr*/static void* flush_log_thread(void* args) {Log::get_instance()->async_write_log();return nullptr;}/*** 初始化日志类* @param file_name 日志文件名* @param close_log 是否关闭日志* @param log_buf_size 日志缓冲区大小,默认为8192* @param split_lines 日志文件分割行数,默认为5000000* @param max_queue_size 日志队列最大大小,默认为0表示不限制* @return 返回初始化是否成功*/bool init(const char* file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);/*** 写入日志* @param level 日志级别* @param format 日志信息的格式字符串* @param ... 格式字符串中的参数*/void write_log(int level, const char* format, ...);/*** 刷新日志,将缓冲区内容写入文件*/void flush(void);private:Log();virtual ~Log();/*** 异步写入日志线程函数* @return 返回nullptr*/void* async_write_log() {string single_log;//从阻塞队列中取出一个日志string,写入文件while(m_log_queue->pop(single_log)) {m_mutex.lock();fputs(single_log.c_str(), m_fp);m_mutex.unlock();}}private:char dir_name[128]; //日志目录名char log_name[128]; //日志文件名int m_split_lines; //日志文件分割行数int m_log_buf_size; //日志缓冲区大小long long m_count; //日志计数int m_today; //当日志FILE* m_fp; //文件指针char* m_buf; //缓冲区block_queue<string> *m_log_queue; //阻塞队列,用于异步写日志bool m_is_async; //是否异步写日志locker m_mutex; //线程锁int m_close_log; //是否关闭日志
};// 定义DEBUG级别日志宏,当m_close_log为0时,记录DEBUG级别日志
#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
// 定义INFO级别日志宏,当m_close_log为0时,记录INFO级别日志
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
// 定义WARN级别日志宏,当m_close_log为0时,记录WARN级别日志
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
// 定义ERROR级别日志宏,当m_close_log为0时,记录ERROR级别日志
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}#endif

日志类中的方法都不会被其他程序直接调用,末尾的四个可变参数宏提供了其他程序的调用方法。

前述方法对日志等级进行分类,包括DEBUG,INFO,WARN和ERROR四种级别的日志。

功能实现

init函数实现日志创建、写入方式的判断。

write_log函数完成写入日志文件中的具体内容,主要实现日志分级、分文件、格式化输出内容。

生成日志文件 && 判断写入方式

通过单例模式获取唯一的日志类,调用init方法,初始化生成日志文件,服务器启动按当前时刻创建日志,前缀为时间,后缀为自定义log文件名,并记录创建日志的时间day和行数count。

写入方式通过初始化时是否设置队列大小(表示在队列中可以放几条数据)来判断,若队列大小为0,则为同步,否则为异步。

//异步需要设置阻塞队列长度,同步不需要设置
bool Log::init(const char* file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size) {// 如果设置了 max_queue_size,则将日志设置为异步模式if (max_queue_size >= 1) {m_is_async = true; // 设置异步标志为 truem_log_queue = new block_queue<string>(max_queue_size); // 创建日志队列,大小为 max_queue_sizepthread_t tid;// 创建一个异步线程,用于异步写日志,flush_log_thread 是回调函数pthread_create(&tid, nullptr, flush_log_thread, nullptr);}m_close_log = close_log; // 设置日志关闭标志m_log_buf_size = log_buf_size; // 设置日志缓冲区大小m_buf = new char[m_log_buf_size]; // 分配日志缓冲区memset(m_buf, '\0', m_log_buf_size); // 将缓冲区初始化为全零m_split_lines = split_lines; // 设置日志分割行数time_t t = time(nullptr); // 获取当前时间struct tm* sys_tm = localtime(&t); // 将时间转换为本地时间struct tm my_tm = *sys_tm; // 复制本地时间结构体const char* p = strrchr(file_name, '/'); // 查找文件名中最后一个 '/' 的位置char log_full_name[256] = {0}; // 日志文件的完整路径和文件名if (p == nullptr) {// 如果文件名中没有 '/',直接使用文件名创建日志文件名snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);} else {// 如果文件名中包含 '/',将目录和文件名分开strcpy(log_name, p + 1); // 提取文件名strncpy(dir_name, file_name, p - file_name + 1); // 提取目录名// 创建包含目录和日期的完整日志文件名snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);}m_today = my_tm.tm_mday; // 记录日志创建的当天日期m_fp = fopen(log_full_name, "a"); // 以追加模式打开日志文件if (m_fp == nullptr) {return false; // 如果文件打开失败,返回 false}return true; // 文件打开成功,返回 true
}
日志分级与分文件

日志分级的实现大同小异,一般的会提供五种级别,具体的,

  • Debug,调试代码时的输出,在系统实际运行时,一般不使用。
  • Warn,这种警告与调试时终端的warning类似,同样是调试代码时使用。
  • Info,报告系统当前的状态,当前执行的流程或接收的信息等。
  • Error和Fatal,输出系统的错误信息。

上述的使用方法仅仅是个人理解,在开发中具体如何选择等级因人而异。项目中给出了除Fatal外的四种分级,实际使用了Debug,Info和Error三种。

超行、按天分文件逻辑,具体的,

  • 日志写入前会判断当前day是否为创建日志的时间,行数是否超过最大行限制
    • 若为创建日志时间,写入日志,否则按当前时间创建新log,更新创建时间和行数
    • 若行数超过最大行限制,在当前日志的末尾加count/max_lines为后缀创建新log

将系统信息格式化后输出,具体为:格式化时间 + 格式化内容

void Log::write_log(int level, const char* format, ...) {struct timeval now = {0, 0};  // 定义一个时间结构体,用于存储当前时间gettimeofday(&now, nullptr);  // 获取当前时间,精确到微秒time_t t = now.tv_sec;  // 提取当前时间的秒部分struct tm *sys_tm = localtime(&t);  // 将秒部分转换为本地时间的tm结构struct tm my_tm = *sys_tm;  // 将sys_tm赋值给my_tmchar s[16] = {0};  // 定义一个字符数组s,用于存储日志级别字符串switch (level) {case 0:strcpy(s, "[debug]");  // 如果level为0,表示debug级别日志break;case 1:strcpy(s, "[info]:");  // 如果level为1,表示info级别日志break;case 2:strcpy(s, "[warn]:");  // 如果level为2,表示warn级别日志break;case 3:strcpy(s, "[erro]:");  // 如果level为3,表示error级别日志break;default:strcpy(s, "[info]:");  // 其他情况下,默认使用info级别日志break;}m_mutex.lock();  // 加锁,确保多线程环境下对共享资源的安全访问m_count++;  // 日志计数器加1// 检查是否需要新建日志文件if(m_today != my_tm.tm_mday || m_count & m_split_lines == 0) {char new_log[256] = {0};  // 定义一个字符数组,用于存储新日志文件名fflush(m_fp);  // 刷新文件缓冲区,将数据写入文件fclose(m_fp);  // 关闭当前日志文件char tail[16] = {0};  // 定义一个字符数组,用于存储日期信息// 格式化日期信息snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);if (m_today != my_tm.tm_mday) {// 如果是新的一天,则新建一个新的日志文件snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);m_today = my_tm.tm_mday;  // 更新当前日期m_count = 0;  // 重置日志计数器} else {// 如果是同一天,但日志数量达到了分割行数,则创建一个新文件snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);}m_fp = fopen(new_log, "a");  // 打开新日志文件,追加模式}m_mutex.unlock();  // 解锁va_list valst;  // 定义一个可变参数列表va_start(valst, format);  // 初始化可变参数列表string log_str;  // 定义一个字符串用于存储最终的日志信息m_mutex.lock();  // 再次加锁,确保安全写入日志内容// 写入具体的时间和日志级别内容int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);// 将可变参数列表格式化并写入缓冲区int m = vsnprintf(m_buf + n, m_log_buf_size - n - 1, format, valst);m_buf[n + m] = '\n';  // 添加换行符m_buf[n + m + 1] = '\0';  // 以空字符结束字符串log_str = m_buf;  // 将缓冲区内容转换为字符串m_mutex.unlock();  // 解锁if (m_is_async && !m_log_queue->full()) {// 如果是异步模式且日志队列未满,则将日志推入队列m_log_queue->push(log_str);} else {// 如果是同步模式或日志队列已满,则直接写入日志文件m_mutex.lock();fputs(log_str.c_str(), m_fp);  // 将日志字符串写入文件m_mutex.unlock();}va_end(valst);  // 结束可变参数列表}void Log::flush(void) {m_mutex.lock();  // 加锁fflush(m_fp);  // 刷新文件缓冲区,将数据写入文件m_mutex.unlock();  // 解锁
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/878670.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

file | 某文件夹【解耦合】下的文件查找功能实现及功能单元测试

文件查找工具 概要思路OS模块 --- 学习版os.getcwd()os.path.dirname(os.getcwd())os.path.dirname() 和 os.path.basename() OS模块 — 实战版单元测试解耦合 概要 梳理业务主逻辑&#xff1a; 查看存放被采集JSON数据的文件夹内的文件列表【所有 包含文件夹下的文件夹下的文…

【Anaconda】修改jupyter notebook默认打开的工作目录、jupyter notebook快捷键

jupyter notebook快捷键 针对单元格的颜色蓝色命令行模式绿色编辑模式 两种模式的切换编辑模式切换到命令行模式 >>> esc键命令行模式切换到编辑模式 >>> 鼠标左键或者直接按enter键1.标题的书写方式1:1.esc进入命令行模式2.按m键3.写内容4.运行单元格即可方…

SprinBoot+Vue健康管管理微信小程序的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue3.6 uniapp代码 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平…

LabVIEW水泵机组监控系统

介绍了一种基于LabVIEW的水泵机组智能监控系统。该系统结合先进的传感器和数据采集技术&#xff0c;实时监控水泵机组的运行状态&#xff0c;有效预防故障&#xff0c;提高运行效率。通过LabVIEW平台的集成开发环境&#xff0c;系统实现了高效的数据处理和友好的用户界面。 项…

SpringCloud-02 Consul服务注册与发现

Consul是一种用于服务发现、配置和分布式协调的开源工具。Consul提供了以下主要功能&#xff1a; 1.服务发现&#xff1a;Consul允许开发人员在微服务架构中注册和发现服务。它可以自动检测新添加的服务并为它们分配唯一的网络地址。 2.健康检查&#xff1a;Consul可以定期检查…

一篇文档教会你从JavaScript语法走进DOM,让你的网页动起来

目录 JavaScript与WebAPI WebAPI简介 DOM 获取元素 事件 事件三要素 常见的事件类型 获取修改元素属性 基本介绍和使用 案例1&#xff1a;实现文本框内数字计数 案例2&#xff1a;实现“全部选中”按钮触发时相应的效果&#xff08;worth trying for a freshman&…

turbovnc 服务端、客户端安装

turbovnc 可以方便地远程登录带界面的linux系统&#xff0c;比如xbuntu、kali等&#xff1b;远程windows11系统&#xff0c;经过亲身测试体验&#xff0c;感觉还是不如windows自带的rdp服务&#xff08;mstsc命令连接&#xff09;好用。 一、安装客户端 下载最新版本的客户端…

力扣面试经典算法150题:接雨水

接雨水 今天的题目是力扣面试经典算法150题中的困难难度数组题目&#xff1a;分发糖果。 题目链接&#xff1a;https://leetcode.cn/problems/trapping-rain-water/description/?envTypestudy-plan-v2&envIdtop-interview-150 题目描述 给定 n 个非负整数表示每个宽度为…

0904作业+思维导图

一、作业 &#xff08;将昨天的作业修改为标准模板类的&#xff09; 1、代码 #include <iostream> #include <stack> using namespace std; //队列模板类 template<typename T> class Queue { private:int max; //队列最大容量int num; //队列内…

pikachu文件包含漏洞靶场通关攻略

本地文件包含 首先&#xff0c;在靶场根目录下创建一个php文件&#xff0c;内容是phpinfo(); 其次&#xff0c;上传一个任意球星图片&#xff0c;会跳转到带有filename参数的php文件下 然后&#xff0c;将filename的参数改为可以访问到我们创建的php文件的地址 ../../../../…

TCP协议多进程多线程并发服务器

TCP多进程多线程并发服务器 1.多进程并发服务器 #include <myhead.h>#define SERPORT 6666 #define SERIP "192.168.0.136" #define BLACKLOG 10void hande(int a) {if(aSIGCHLD){while(waitpid(-1,NULL,WNOHANG)!-1);//回收僵尸进程} }int main(int argc, c…

【Grafana】Prometheus结合Grafana打造智能监控可视化平台

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Oracle 客户端 PL/SQL Developer 15.0.4 安装与使用

目录 官网下载与安装 切换中文与注册 连接Oracle数据库 tnsnames.ora 文件使用 Oracle 客户端 PL/SQL Developer 12.0.7 安装、数据导出、Oracle 执行/解释计划、for update。 官网下载与安装 1、官网&#xff1a;https://www.allroundautomations.com/products/pl-sql-d…

Redis的配置和启动+Redis Insight连接

一、安装 Redis的安装&#xff1a;从镜像站下载&#xff1a;索引 redis-local (huaweicloud.com)&#xff0c;然后将其传到Linux虚拟机中进行解压&#xff0c;解压之后需要下载gcc&#xff0c;因为Redis底层是用c写的&#xff0c;所以要编译一下生成redis文件&#xff0c;然后…

vite项目配置本地开发使用https访问

在Vite项目中启用HTTPS以安全地使用navigator.mediaDevices.getUserMedia() 引言 在现代Web开发中&#xff0c;保护用户隐私和数据安全是至关重要的。特别是在涉及到媒体捕获功能&#xff0c;如使用用户的摄像头或麦克风时&#xff0c;Web应用需要遵循严格的安全准则。naviga…

反向迭代器:reverse_iterator的实现

目录 前言 特点 注意事项 实现 构造函数 功能函数 在list与vector中的使用 vector list 前言 反向迭代器是一种在序列容器的末尾开始&#xff0c;并向前移动至序列开始处的迭代器。在C中&#xff0c;反向迭代器由标准库中的容器类提供&#xff0c;比如vector、list、d…

Qt 字符串的编码方式,以及反斜杠加3个数字是什么编码\344\275\240,如何生成

Qt 字符串的编码方式 问题 总所周知&#xff0c;Qt的ui文件在编译时&#xff0c;会自动生成一个ui_xxxxx.h的头文件&#xff0c;打开一看&#xff0c;其实就是将摆放的控件new出来以及布局的代码。 只要用Qt提供的uic.exe工具&#xff0c;自己也可以将ui文件输出为代码文件…

c# 笔记 winform添加右键菜单,获取文件大小 ,多条件排序OrderBy、ThenBy,list<double>截取前5个

Winform右键菜单‌ 要在C# Winform应用程序中添加右键菜单&#xff0c;‌你可以按照以下步骤操作&#xff1a;‌ 1.‌创建菜单项‌ 在Form的构造函数或加载事件中&#xff0c;‌创建ContextMenuStrip控件的实例&#xff0c;‌并为其添加菜单项。‌ 2.‌绑定到控件‌ 将Con…

c++ websocket简单讲解

只做简单讲解。 一.定义和原理 WebSocket 是从 HTML5 开始⽀持的⼀种⽹⻚端和服务端保持⻓连接的消息推送机制&#xff0c;传统的 web 程序都是属于 "⼀问⼀答" 的形式&#xff0c;即客⼾端给服务器发送了⼀个 HTTP 请求&#xff0c;服务器给客⼾端返回⼀个 HTTP 响…

Java 入门指南:Java 并发编程 —— 并发容器 PriorityBlockingQueue

BlockingQueue BlockingQueue 是Java并发包&#xff08;java.util.concurrent&#xff09;中提供的一个阻塞队列接口&#xff0c;它继承自 Queue 接口。 BlockingQueue 中的元素采用 FIFO 的原则&#xff0c;支持多线程环境并发访问&#xff0c;提供了阻塞读取和写入的操作&a…