TinyWebserver的复现与改进(7):日志系统

本项目中,使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,为了简单,将使用异步写入的方式。(后续再添加同步写入)

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

系统流程

image-20240825112035668

为了确保对同一日志文件的统一访问,我们采用get_instance()方法来获取唯一的日志文件指针pget_instance()作为Log类的静态成员函数,它的作用是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

  1. 在程序的入口点main函数中,我们需要首先对Log类进行初始化。
  2. 每当有新的信息需要记录到日志文件中时,系统会执行一个文件有效性检查。如果发现当前没有可用的日志文件、现有日志文件已达到存储上限,或者上一个日志文件不是在当前日期创建的,系统将自动生成一个新的日志文件。
  3. 随后,主线程将负责将信息进行格式化,并将信息添加到阻塞队列中。完成这一操作后,主线程便可以继续执行其他任务,而无需等待日志写入操作的完成。
  4. 与此同时,一旦阻塞队列中有待处理的日志记录任务,专门的写线程(一个子进程)将被唤醒。它会调用async_write_log()函数,将队列中的日志信息异步写入到磁盘中。

在这里,循环队列实际上是一个连续数组+2个移动指针组成的数据结构

循环队列 的图像结果

日志类定义

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

  • 公有的实例获取方法
  • 初始化日志文件方法
  • 异步日志写入方法,内部调用私有异步方法
  • 内容格式化方法
  • 刷新缓冲区
class Log
{
public:
    // 返回log对象的指针
    static Log *get_instance()
    {
        /* 
            instance只能初始化一次,所以即便调用 get_instance() 函数多次,
            代码static Log instance;只执行一次,即 instance 的地址不会被改变 
        */
        static Log instance;
        return &instance;
    }

    static void *flush_log_thread(void* arg)
    {
        Log::get_instance()->async_write_log();
    }

    bool init(const char *file_name, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);
    void write_log(int level, const char* format, ...);
    void flush();

private:
    long long m_count;
    int m_today;
    int m_log_buf_size;
    bool m_is_async;
    char *m_buf;
    int m_split_lines;
    char log_name[128]; // log文件名
    char dir_name[128]; // 路径名
    locker m_mutex;
    FILE *m_fp;         //打开log的文件指针
    block_queue<string> *m_log_queue; //阻塞队列

private:
    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();
        }
    }

    Log()
    {
        m_count = 0;
        m_is_async = false;
    }

    ~Log()
    {
        if(m_fp != NULL)
        {
            fclose(m_fp);
        }
    }
};

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

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

#define LOG_DEBUG(format, ...)  Log::get_instance()->write_log(0, format, ##__VA_ARGS__)
#define LOG_INFO(format, ...)  Log::get_instance()->write_log(0, format, ##__VA_ARGS__)
#define LOG_WARN(format, ...)  Log::get_instance()->write_log(0, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...)  Log::get_instance()->write_log(0, format, ##__VA_ARGS__)

日志分文件判断

  • 日志写入前会判断当前day是否为创建日志的时间,行数是否超过最大行限制
    • 若为创建日志时间,写入日志,否则按当前时间创建新log,更新创建时间和行数
    • 若行数超过最大行限制,在当前日志的末尾加count/max_lines为后缀创建新log
// 一天一个log,或者当文件行数满了,就新开一个log
if(m_today != my_tm.tm_mday || m_count % m_split_lines == 0)
{
    char new_log[256] = {0};
    // 刷新由 fopen 打开的输出流(如文件或控制台)的缓冲区。当使用 fflush 刷新流时,所有缓冲中的数据将被写入到流所关联的文件或设备中。
    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");
}

异步写入日志

void Log::write_log(int level, const char* format, ...)
{
    /*
        struct timeval {
            time_t      tv_sec;     // 秒
            suseconds_t  tv_usec;    // 微秒
        }
    */
   struct timeval now = {0,0};
    // 获取当前的日期和时间, 存储在now中
   gettimeofday(&now, NULL);
   time_t t = now.tv_sec;
   // 将秒数转换为本地时间表示形式
   struct tm *sys_tm = localtime(&t);
   struct tm my_tm = *sys_tm;
   char s[16] = {0};
   switch(level)
   {
        case 0:
            strcpy(s, "[debug]:");
            break;
        case 1:
            strcpy(s, "[info]:");
            break;
        case 2:
            strcpy(s, "[warn]:");
            break;
        case 3:
            strcpy(s, "[erro]:");
            break;
        default:
            strcpy(s, "[info]:");
            break;
   }

   // 写入一个log, 对 m_count++, m_split_lines最大行数
   m_mutex.lock();
   m_count++;

   // 一天一个log,或者当文件行数满了,就新开一个log
   if(m_today != my_tm.tm_mday || m_count % m_split_lines == 0)
   {
        char new_log[256] = {0};
        // 刷新由 fopen 打开的输出流(如文件或控制台)的缓冲区。当使用 fflush 刷新流时,所有缓冲中的数据将被写入到流所关联的文件或设备中。
        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-1, format, valst);
    m_buf[n+m] = '\n';
    // 加 \0 表示前面的字符组成一句话
    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);
}

系列文章

GitHub - yzfzzz/MyWebServer: Linux高并发服务器项目,参考了TinyWebServer,将在此基础上进行性能改进与功能增加。为方便读者学习,附带详细注释和博客!

TinyWebserver的复现与改进(1):服务器环境的搭建与测试-CSDN博客

TinyWebserver的复现与改进(2):项目的整体框架-CSDN博客

TinyWebserver的复现与改进(3):线程同步机制类封装及线程池实现-CSDN博客

TinyWebserver的复现与改进(4):主线程的具体实现-CSDN博客

TinyWebserver的复现与改进(5):HTTP报文的解析与响应-CSDN博客

TinyWebserver的复现与改进(6):定时器处理非活动连接-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落叶随峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值