TinywebServer代码详解– 日志系统-下(10)

该blog内容转自:最新版Web服务器项目详解 - 10 日志系统(下)

该blog对上述内容进行补充(在本人的角度)

结合此前记录的blog一起学习:牛客WebServer项目实战(点击跳转)

结合此前记录的blog一起学习:多线程与线程同步(点击跳转)

原项目地址(点击跳转)

博主添加注释后项目地址(点击跳转)



一、基础知识

1.本文内容

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

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

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

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

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



2.基础API

(1)fputs

fputs 函数的原型定义在 <cstdio>(C++ 中)或 <stdio.h>(C 中)头文件中

1
int fputs(const char *str, FILE *stream);

参数:

  • const char *str:指向一个以空字符终止的字符串的指针,该字符串将被写入
  • FILE *stream:指向 FILE 对象的指针,这个 FILE 对象代表一个已打开的文件流,该字符串将被写入到这个文件流中

返回值:

  • 成功:返回一个非负值。
  • 失败:返回 EOF,表示发生了一个错误

(2)可变参数宏__VA_ARGS__

在 C 和 C++ 中,__VA_ARGS__ 是一个预处理宏,它允许宏定义接收和使用可变数量的参数。__VA_ARGS__ 使得宏可以灵活处理不定数量的参数,非常适合用于编写如日志、错误处理等需要接受多个参数的功能

使用 __VA_ARGS__ 的宏定义通常遵循以下格式:

1
#define 宏名(参数列表, ...) 替换列表

其中,... 表示可变部分的开始,__VA_ARGS__ 用于在宏的替换部分引用所有的可变参数

下面是一个使用 __VA_ARGS__ 的示例,定义一个简单的宏,用于打印信息到标准输出:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

#define PRINTF(fmt, ...) printf(fmt, __VA_ARGS__)

int main() {
PRINTF("Hello, world!\n");
PRINTF("Number: %d\n", 42);
PRINTF("Float and integer: %.2f and %d\n", 3.14159, 7);
return 0;
}

PRINTF 被定义为接受一个格式化字符串和任意数量的后续参数,它将这些参数传递给 printf 函数

可以看到 PRINTF 在没有可变参数时(如 “Hello, world!\n”)和有一个或多个可变参数时(如 “%d” 和 “%.2f and %d”)都能正常工作

__VA_ARGS__宏前面加上##

##操作符通常用于连接两个令牌,但在与__VA_ARGS__结合使用时,它的行为稍有不同。当使用##__VA_ARGS__时,如果__VA_ARGS__是空的,那么##操作符将会消除它前面的逗号,避免语法错误。这在创建接受可选参数的宏时非常有用

实例

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

#define PRINTF(fmt, ...) printf(fmt, ##__VA_ARGS__)

int main() {
PRINTF("Hello, world!\n");
PRINTF("Number: %d\n", 42);
PRINTF("Float and integer: %.2f and %d\n", 3.14159, 7);
return 0;
}

如果调用PRINTF时没有提供除fmt以外的其他参数(如"Hello, world!\n"),##操作符会移除fmt后面的逗号,使得调用变为printf("Hello, world!\n")

如果提供了额外的参数,##操作符不会影响它们的使用


(3)fflush

fflush 函数定义在 <stdio.h>(C)或 <cstdio>(C++)头文件中,主要用途是将缓冲区内的数据强制写入与流相关联的文件中,或者刷新(清空)输出缓冲区

1
int fflush(FILE *stream);

参数:

  • **stream**:指向 FILE 对象的指针,该 FILE 对象表示一个打开的文件流。如果 streamNULL,则刷新所有输出流的缓冲区

返回值:

  • 成功:如果刷新成功,返回 0
  • 错误:如果发生错误,返回 EOF(通常是 -1),并设置错误指示器

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

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




二、流程图与日志类定义

1.流程图

日志文件

  • 局部变量的懒汉模式获取实例
  • 生成日志文件,并判断同步和异步写入方式

同步

  • 判断是否分文件
  • 直接格式化输出内容,将信息写入日志文件

异步

  • 判断是否分文件
  • 格式化输出内容,将内容写入阻塞队列,创建一个写线程,从阻塞队列取出内容写入日志文件

image-20240702160840732



2.日志类定义

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

log/log.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#pragma once
#include <stdio.h>
#include <iostream>
#include <string>
#include <stdarg.h>
#include <pthread.h>
#include "block_queue.h"

using namespace std;

/********************日志类***********************/
class Log
{
public:
// 静态接口,获取日志类实例
// C++11以后,使用局部变量懒汉不用加锁
static Log *get_instance();
// 异步写日志公有方法,调用私有方法async_write_log
static void *flush_log_thread(void *args);

//可选择的参数有日志文件、日志缓冲区大小、最大行数以及最长日志条队列
bool init(const char *file_name, int close_log, int log_buf_size = 8192,
int split_lines = 5000000, int max_queue_size = 0);
// 将输出内容按照标准格式整理
void write_log(int level, const char *format, ...);
// 强制刷新缓冲区
void flush(void);

private:
// 私有化构造函数
Log();
virtual ~Log();
// 异步写日志方法
void *async_write_log();

private:
// 路径名
char dir_name[128];
// log文件名
char log_name[128];
// 日志最大行数
int m_split_lines;
// 日志缓冲区大小
int m_log_buf_size;

// 日志行数记录
long long m_count;
// 因为按天分类,记录当前时间是那一天
int m_today;
// 打开log的文件指针
FILE *m_fp;
// 要输出的内容
char *m_buf;
// 阻塞队列
block_queue<string> *m_log_queue;
// 是否同步标志位
bool m_is_async;
// 同步类
locker m_mutex;
// 关闭日志
int m_close_log;
};
/********************日志类***********************/


/******************使用宏进行日志输出****************/
// 日志等级进行分类,包括DEBUG,INFO,WARN和ERROR四种级别的日志
// 先判断是否将日志关闭,后,调用类的函数write_log向日志文件写入日志信息,最后对写缓冲区进行刷新
#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}
/******************使用宏进行日志输出****************/

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

前述方法对日志等级进行分类,包括DEBUGINFOWARNERROR四种级别的日志



2.基础函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdarg.h>
#include "log.h"
#include <pthread.h>
using namespace std;

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

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



/*
* func:静态接口函数
* note:获取唯一的日志实例对象
*/
Log* Log::get_instance()
{
static Log instance;
return &instance;
}


/*
* func:异步写日志公有方法,调用私有方法async_write_log
*/
void * Log::flush_log_thread(void *args)
{
Log::get_instance()->async_write_log();
}


/*
* func:私有的异步写日志方法
*/
void *Log::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();
}
}



void Log::flush(void)
{
m_mutex.lock();
//强制刷新写入流缓冲区
fflush(m_fp);
m_mutex.unlock();
}


3.功能实现

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/*
* func:初始化日志信息
* note:可选择的参数有日志文件、日志缓冲区大小、最大行数以及最长日志条队列
* 1.创建日志文件(命名方式:当前年份_当前月份_当前日_日志文件名)
* 2.创建写缓冲区
* 3.异步,还需要设置阻塞队列,且创建一个写线程,从阻塞队列中pop日志信息写入到日志文件中
*/
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;
// string类型日志的循环队列
m_log_queue = new block_queue<string>(max_queue_size);
pthread_t tid;
// flush_log_thread为回调函数,这里表示创建线程异步写日志
pthread_create(&tid, NULL, flush_log_thread, NULL);
}

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;

// 获取当前时间,获取自 1970 年 1 月 1 日以来的秒数
time_t t = time(NULL);
// 转换时间为本地时间格式
struct tm *sys_tm = localtime(&t);
struct tm my_tm = *sys_tm;

// 从后往前找到第一个/的位置---file_name可能传入的是路径,如:LOG/log_today
const char *p = strrchr(file_name, '/');
char log_full_name[256] = {0};

if (p == NULL)
{
// file_name直接是日志文件
// 向log_full_name数组中写入,当前的"当前年份_当前月份_当前日_日志文件名"
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
{
// log_name为日志文件名
strcpy(log_name, p + 1);
// 日志文件的上层目录的路径信息
strncpy(dir_name, file_name, p - file_name + 1);
// 向log_full_name数组中写入,当前的"目录+当前年份_当前月份_当前日期_日志文件名"
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 == NULL)
{
return false;
}

return true;
}

(2)日志分级&按天区分日志文件

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

Debug,调试代码时的输出,在系统实际运行时,一般不使用。

Warn,这种警告与调试时终端的warning类似,同样是调试代码时使用。

Info,报告系统当前的状态,当前执行的流程或接收的信息等。

ErrorFatal,输出系统的错误信息

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

日志写入前会判断当前day是否为创建日志的时间,行数是否超过最大行限制

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/*
* func:写日志文件
* note:1.若为同步,直接将日志信息写入日志文件
* 2.若为异步,则将日志信息push阻塞队列,init函数中创建了写子线程,用于从阻塞队列中pop日志信息,异步写入日志文件
*/
void Log::write_log(int level, const char *format, ...)
{
struct timeval now = {0, 0};
// 获取当前精确的时间信息
gettimeofday(&now, NULL);
// 提取从1970年1月1日 00:00:00 UTC 至今的秒数
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++;

// 日志不是今天或写入的日志行数是最大行的倍数
if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //everyday log
{

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);

//如果是时间不是今天,则创建今天的日志,更新m_today和m_count
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
{
//超过了最大行,在之前的日志名基础上加后缀, m_count/m_split_lines
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;
//将传入的format参数赋值给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);

// 传入的日志信息,已经按照format赋值给valst
// 将日志信息写入m_buf
int m = vsnprintf(m_buf + n, m_log_buf_size - 1, format, valst);
m_buf[n + m] = '\n';
m_buf[n + m + 1] = '\0';
log_str = m_buf;

m_mutex.unlock();

//若m_is_async为true表示异步,false为同步
//若异步,则将日志信息加入阻塞队列,同步则加锁向文件中写
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);
}