• TinyWebServer学习笔记-log


    为什么服务器要有一个日志系统?

    1. 故障排查和调试: 在服务器运行期间,可能会发生各种问题和故障,例如程序崩溃、性能下降、异常请求等。日志记录了服务器的运行状态、错误信息和各种操作,这些日志可以用来快速定位和排查问题,帮助开发人员更容易地找到问题的根本原因,从而更快地修复bug和提高服务器稳定性。

    2. 性能监测: 通过日志系统可以记录服务器的性能数据,包括请求处理时间、吞吐量、内存使用情况、CPU利用率等等。这些数据有助于监控服务器的性能,发现潜在的性能瓶颈,以便进行性能优化。

    3. 安全性: 日志可以记录系统的安全事件,如登录失败尝试、异常访问等。通过分析这些日志,可以检测和防止潜在的安全威胁,提高服务器的安全性。

    4. 法律合规性: 某些行业和法规要求服务器必须记录和保留特定类型的操作日志,以便进行审计和合规性检查。缺乏合规性的日志记录可能会导致法律责任。

    5. 运营和分析: 运营团队可以通过分析日志数据来了解用户行为、产品使用情况和趋势。这有助于改进产品、优化用户体验和制定业务策略。

    6. 历史记录: 日志还可以用作历史记录,以跟踪系统的状态和操作历史。这对于了解系统的演化和历史情况非常有用。

     实现过程

    为了实现异步写入,利用循环数组实现阻塞队列。

    1. template <class T>
    2. class block_queue
    3. {
    4. public:
    5. block_queue(int max_size = 1000)
    6. {
    7. if (max_size <= 0)
    8. {
    9. exit(-1);
    10. }
    11. m_max_size = max_size;
    12. m_array = new T[max_size];
    13. m_size = 0;
    14. m_front = -1;
    15. m_back = -1;
    16. }
    17. void clear()
    18. {
    19. m_mutex.lock();
    20. m_size = 0;
    21. m_front = -1;
    22. m_back = -1;
    23. m_mutex.unlock();
    24. }
    25. ~block_queue()
    26. {
    27. m_mutex.lock();
    28. if (m_array != NULL)
    29. delete [] m_array;
    30. m_mutex.unlock();
    31. }
    32. //判断队列是否满了
    33. bool full()
    34. {
    35. m_mutex.lock();
    36. if (m_size >= m_max_size)
    37. {
    38. m_mutex.unlock();
    39. return true;
    40. }
    41. m_mutex.unlock();
    42. return false;
    43. }
    44. //判断队列是否为空
    45. bool empty()
    46. {
    47. m_mutex.lock();
    48. if (0 == m_size)
    49. {
    50. m_mutex.unlock();
    51. return true;
    52. }
    53. m_mutex.unlock();
    54. return false;
    55. }
    56. //返回队首元素
    57. bool front(T &value)
    58. {
    59. m_mutex.lock();
    60. if (0 == m_size)
    61. {
    62. m_mutex.unlock();
    63. return false;
    64. }
    65. value = m_array[m_front];
    66. m_mutex.unlock();
    67. return true;
    68. }
    69. //返回队尾元素
    70. bool back(T &value)
    71. {
    72. m_mutex.lock();
    73. if (0 == m_size)
    74. {
    75. m_mutex.unlock();
    76. return false;
    77. }
    78. value = m_array[m_back];
    79. m_mutex.unlock();
    80. return true;
    81. }
    82. int size()
    83. {
    84. int tmp = 0;
    85. m_mutex.lock();
    86. tmp = m_size;
    87. m_mutex.unlock();
    88. return tmp;
    89. }
    90. int max_size()
    91. {
    92. int tmp = 0;
    93. m_mutex.lock();
    94. tmp = m_max_size;
    95. m_mutex.unlock();
    96. return tmp;
    97. }
    98. //往队列添加元素,需要将所有使用队列的线程先唤醒
    99. //当有元素push进队列,相当于生产者生产了一个元素
    100. //若当前没有线程等待条件变量,则唤醒无意义
    101. bool push(const T &item)
    102. {
    103. m_mutex.lock();
    104. if (m_size >= m_max_size)
    105. {
    106. m_cond.broadcast();
    107. m_mutex.unlock();
    108. return false;
    109. }
    110. m_back = (m_back + 1) % m_max_size;
    111. m_array[m_back] = item;
    112. m_size++;
    113. m_cond.broadcast();
    114. m_mutex.unlock();
    115. return true;
    116. }
    117. //pop时,如果当前队列没有元素,将会等待条件变量
    118. bool pop(T &item)
    119. {
    120. m_mutex.lock();
    121. while (m_size <= 0)
    122. {
    123. if (!m_cond.wait(m_mutex.get()))
    124. {
    125. m_mutex.unlock();
    126. return false;
    127. }
    128. }
    129. /*m_front = (m_front + 1) % m_max_size;
    130. item = m_array[m_front];*/
    131. //这里我认为应该先得到值,然后更新m_front下标
    132. item=m_array[m_front];
    133. m_front=(m_frong+1)% m_max_size;
    134. m_size--;
    135. m_mutex.unlock();
    136. return true;
    137. }
    138. //增加了超时处理
    139. bool pop(T &item, int ms_timeout)
    140. {
    141. struct timespec t = {0, 0};
    142. struct timeval now = {0, 0};
    143. gettimeofday(&now, NULL);
    144. m_mutex.lock();
    145. if (m_size <= 0)
    146. {
    147. t.tv_sec = now.tv_sec + ms_timeout / 1000;
    148. t.tv_nsec = (ms_timeout % 1000) * 1000;
    149. if (!m_cond.timewait(m_mutex.get(), t))
    150. {
    151. m_mutex.unlock();
    152. return false;
    153. }
    154. }
    155. if (m_size <= 0)
    156. {
    157. m_mutex.unlock();
    158. return false;
    159. }
    160. m_front = (m_front + 1) % m_max_size;
    161. item = m_array[m_front];
    162. m_size--;
    163. m_mutex.unlock();
    164. return true;
    165. }
    166. private:
    167. locker m_mutex; //互斥锁
    168. cond m_cond; //条件变量
    169. T *m_array; //存放日志的数组
    170. int m_size; //已经用的空间
    171. int m_max_size; //最大容量
    172. int m_front; //数组头的下标
    173. int m_back; //数组尾的下标
    174. };

    解释下timespec结构体和timeval结构体,timespec结构体,分为秒和微秒两个部分,timeval结构体now,分为秒和纳秒两个部分。在这个自定义队列中,当队列为空时,从队列中获取元素的线程将会被挂起;当队列是满时,往队列里添加元素的线程将会挂起。

    1. class Log
    2. {
    3. public:
    4. //C++11以后,使用局部变量懒汉不用加锁
    5. static Log *get_instance()
    6. {
    7. static Log instance;
    8. return &instance;
    9. }
    10. static void *flush_log_thread(void *args)
    11. {
    12. Log::get_instance()->async_write_log();
    13. }
    14. //可选择的参数有日志文件、日志缓冲区大小、最大行数以及最长日志条队列
    15. bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);
    16. void write_log(int level, const char *format, ...);
    17. void flush(void);
    18. private:
    19. Log();
    20. virtual ~Log();
    21. void *async_write_log()
    22. {
    23. string single_log;
    24. //从阻塞队列中取出一个日志string,写入文件
    25. while (m_log_queue->pop(single_log))
    26. {
    27. m_mutex.lock();
    28. fputs(single_log.c_str(), m_fp);
    29. m_mutex.unlock();
    30. }
    31. }
    32. private:
    33. char dir_name[128]; //路径名
    34. char log_name[128]; //log文件名
    35. int m_split_lines; //日志最大行数
    36. int m_log_buf_size; //日志缓冲区大小
    37. long long m_count; //日志行数记录
    38. int m_today; //因为按天分类,记录当前时间是那一天
    39. FILE *m_fp; //打开log的文件指针
    40. char *m_buf; //缓冲区
    41. block_queue<string> *m_log_queue; //阻塞队列
    42. bool m_is_async; //是否同步标志位
    43. locker m_mutex; //互斥锁
    44. int m_close_log; //关闭日志
    45. };
    46. #define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
    47. #define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
    48. #define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
    49. #define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}

    本项目中,使用单例模式创建日志系统,记录服务器的运行状态、错误信息和访问数据。能够按天分类,超行分类。可以选择同步和异步写入两种方式。

    同步打开对应的文件写入日志;异步则采取生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的日志push进队列,写线程从队列中读取内容,写入日志。

    在这个项目的日志系统中,如果设置了阻塞队列的长度,则代表选择了异步写入日志;如果没有设置,则代表同步写入日志。

    为什么要日志分级?

    1. 信息过滤: 在大型应用程序中,产生的日志可能非常庞大。通过分级,可以根据需要选择性地查看日志。例如,开发人员可能只对错误和警告感兴趣,而不关心调试信息。

    2. 故障排查: 当应用程序出现故障或错误时,日志分级可以帮助开发人员快速定位问题。错误日志可以提供关于发生了什么错误的详细信息,而调试日志则可以提供更多上下文,帮助解决问题。

    3. 性能监测: 分级日志还可以用于监测应用程序的性能。通过记录某些操作的耗时信息,开发人员可以识别性能瓶颈,并进行优化。

    4. 审核和合规性: 在某些情况下,应用程序需要记录特定事件或行为,以满足合规性要求或进行审核。通过使用不同级别的日志,可以轻松地识别和检索这些信息。

    5. 容易维护: 通过使用分级日志,开发人员可以更容易地维护应用程序的日志记录。不同级别的日志通常被写入不同的文件或存储位置,这使得查找和清理日志变得更加简单。

    常见的日志级别包括:

    • DEBUG(调试): 用于记录详细的调试信息,通常只在开发和测试阶段启用。

    • INFO(信息): 用于记录应用程序的重要事件和状态信息,例如启动、关闭、用户登录等。

    • WARNING(警告): 用于记录可能需要关注但不一定是错误的事件,例如配置警告或不寻常的操作。

    • ERROR(错误): 用于记录应用程序的错误事件,例如异常、未处理的异常、无法连接到数据库等。

    • CRITICAL(关键): 用于记录严重错误,可能导致应用程序无法正常运行的情况,例如关键服务崩溃。

    本项目中,包括DEBUG,INFO,WARN,ERROR四个级别。

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

     在init函数中完成路径名,文件名等私有数据成员的设置。

    1. if (p == NULL)
    2. {
    3. 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);
    4. }
    5. else
    6. {
    7. strcpy(log_name, p + 1);
    8. strncpy(dir_name, file_name, p - file_name + 1);
    9. 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);
    10. }

    这段代码判断file_name是是否包含'\',如果不包含,也就是没有包含路径信息,那么就用当前日期和file_name来构建日志文件名。p为空则代表file_name不包含路径信息,只包含文件名,那么生成最终文件名:年_月_日_文件名。如果p不为空,那么找到最后一个路径分隔符,计算出文件名所在文件夹的长度,并将路径复制到dir_name中,文件名复制到log_name中,最终构建的路径是文件路径+时间+文件名。也就是根据是否包含路径信息,生成不同格式的日志文件名。

    接下来是分级和分片函数:

    1. void Log::write_log(int level, const char *format, ...)
    2. {
    3. struct timeval now = {0, 0};
    4. gettimeofday(&now, NULL);
    5. time_t t = now.tv_sec;
    6. struct tm *sys_tm = localtime(&t);
    7. struct tm my_tm = *sys_tm;
    8. char s[16] = {0};
    9. switch (level)
    10. {
    11. case 0:
    12. strcpy(s, "[debug]:");
    13. break;
    14. case 1:
    15. strcpy(s, "[info]:");
    16. break;
    17. case 2:
    18. strcpy(s, "[warn]:");
    19. break;
    20. case 3:
    21. strcpy(s, "[erro]:");
    22. break;
    23. default:
    24. strcpy(s, "[info]:");
    25. break;
    26. }
    27. //写入一个log,对m_count++, m_split_lines最大行数
    28. m_mutex.lock();
    29. m_count++;
    30. if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //everyday log
    31. {
    32. char new_log[256] = {0};
    33. fflush(m_fp);
    34. fclose(m_fp);
    35. char tail[16] = {0};
    36. snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);
    37. if (m_today != my_tm.tm_mday)
    38. {
    39. snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);
    40. m_today = my_tm.tm_mday;
    41. m_count = 0;
    42. }
    43. else
    44. {
    45. snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);
    46. }
    47. m_fp = fopen(new_log, "a");
    48. }
    49. m_mutex.unlock();
    50. va_list valst;
    51. va_start(valst, format);
    52. string log_str;
    53. m_mutex.lock();
    54. //写入的具体时间内容格式
    55. int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",
    56. my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
    57. my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);
    58. int m = vsnprintf(m_buf + n, m_log_buf_size - n - 1, format, valst);
    59. m_buf[n + m] = '\n';
    60. m_buf[n + m + 1] = '\0';
    61. log_str = m_buf;
    62. m_mutex.unlock();
    63. if (m_is_async && !m_log_queue->full())
    64. {
    65. m_log_queue->push(log_str);
    66. }
    67. else
    68. {
    69. m_mutex.lock();
    70. fputs(log_str.c_str(), m_fp);
    71. m_mutex.unlock();
    72. }
    73. va_end(valst);
    74. }

    思路如下:

            1、获得今天的日期时间,如果日志的日期不是今天则创建新的日志文件,然后向新的文件写入。

            2、如果是今天的日期时间,但是超过了文件的最大行,那么就进行分片,在文件结尾加入分片的ID,比如***_log_file1,***_log_file2这样。然后写入文件。

            3、如果是今天的日期,并且没有超过最大文件行,就写入对应的文件。

    准备好要写入的文件后,格式化时间和内容,放到缓冲数组中,然后判断日志系统是异步写入还是同步写入,如果是异步则将需要写的日志内容加入到阻塞队列中,否则直接调用fputs函数将输入文件指针对应的文件中。

  • 相关阅读:
    1407. 排名靠前的旅行者
    剑指 Offer 68 - II. 二叉树的最近公共祖先
    机器学习基础:大数定律与中心极限定理
    热乎的过万字GameFramework讲解笔记文档
    vue中使用接口(搜狐接口)获取访客IP地址
    暑期JAVA学习(40.1)UDP通信——广播、组播
    【AI绘画接口】Midjourney是什么?Midjourney有官方接口吗?
    【信号去噪】基于遗传算法优化VMD实现信号去噪附matlab代码
    Linux的七种运行级别
    高效数据管理:Java助力实现Excel数据验证
  • 原文地址:https://blog.csdn.net/pan_1214_/article/details/133522957