033 日志

日志

1. 为什么需要日志等级?

在实际生产中,程序输出的信息非常多,如果没有等级就会导致:

  • 开发阶段找不到重点(调试信息太多)。
  • 上线后也不好排查问题(没有区分严重错误和普通信息)。

因此,合理使用日志等级,能让我们:

  1. 快速定位错误。
  2. 过滤无用信息。
  3. 分环境(开发、测试、生产)灵活控制日志量。

2. Linux 常见日志等级

syslog 标准 为例(这是 Linux 内核和很多守护进程默认遵循的):

等级名 数值(优先级) 典型含义
EMERG 0 系统不可用,比如内核崩溃(panic)
ALERT 1 必须立刻采取措施,比如磁盘坏块
CRIT 2 严重错误,可能导致程序崩溃
ERR (ERROR) 3 一般错误,需要修复
WARNING 4 警告信息,可能有潜在风险
NOTICE 5 正常但需要注意的事件
INFO 6 普通运行信息
DEBUG 7 调试信息,开发阶段最详细
  • 数值越小,级别越高,越重要。
  • 在生产环境中,一般只保留 WARNING 及以上等级,避免刷盘压力和磁盘占用。

然而在实际操作当中,我们大多只考虑下面几种日志等级信息:

  • Info:常规消息。
  • Warning:报警信息。
  • Error:比较严重了,可能需要立即处理。
  • Fatal:致命的。
  • Debug:调试信息。

无论是写到文件、控制台,还是系统日志,一个完整的日志条目 通常包含:

部分 示例 说明
时间戳(必须) 2025-01-01 11:11:11 发生时间
日志等级(必须) INFO 该条日志的重要性
内容(必须) Connection established from 192.168.225.225.0 实际信息
主机名 server-01 哪台机器
程序名/模块名 nginx 哪个程序
进程 ID [pid=1234] 哪个进程
线程 ID [tid=5678] 哪个线程(多线程时可选)
上下文(可选) 文件名、行号、函数名 开启调试时很重要

3. 日志的实现

下面演示之前的 有名管道通信的服务端-客户端模型 Demo 进行的日志化:

1. comm.hpp 文件

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
#pragma once

#include <iostream>
#include <string>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "Log.hpp"
using namespace std;

#define FIFO_FILE "./myfifo"
#define MODE 0664

// enum
// {
// FIFO_CREATE_ERR = 1, // 这是创建管道文件失败的错误码
// FIFO_DELETE_ERR = 2, // 这是删除管道文件失败的错误码
// FIFO_OPEN_ERR // 这是打开管道文件失败的错误码(枚举会自动赋值为3)
// };

class Init
{
public:
Init()
{
int n = mkfifo(FIFO_FILE, MODE); // 创建管道文件

if (n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}

~Init()
{
int m = unlink(FIFO_FILE); // 删除管道文件

if (m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};

2. Log.hpp(主要)

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#pragma once

#include <iostream>
#include <string>
#include <stdlib.h> // exit, perror
#include <unistd.h> // read, write, close
#include <sys/types.h> // open, close, read, write, lseek
#include <sys/stat.h> // mkdir
#include <fcntl.h> // open, O_RDONLY, O_WRONLY, O_CREAT, O_APPEND
#include <errno.h> // errno
#include <sys/time.h> // gettimeofday, struct timeval
#include <ctime> // localtime_r, struct tm
using namespace std;

// 管道错误码
enum FIFO_ERROR_CODE
{
FIFO_CREATE_ERR = 1, // 这是创建管道文件失败的错误码
FIFO_DELETE_ERR = 2, // 这是删除管道文件失败的错误码
FIFO_OPEN_ERR // 这是打开管道文件失败的错误码(枚举会自动赋值为3)
};

// 日志等级
enum Log_Level
{
Fatal, // 最严重级别
Error, // 严重错误
Warning, // 警告
Debug, // 调试信息
Info // 普通信息
};

class Log
{
int enable = 1; // 是否启用日志
int classification = 1; // 是否分类
string log_path = "./log.txt"; // 日志存放路径
int console_out = 1; // 是否输出到终端

// 日志等级转换成字符串
string level_to_string(int level)
{
switch (level)
{
case Fatal:
return "Fatal";
case Error:
return "Error";
case Warning:
return "Warning";
case Debug:
return "Debug";
case Info:
return "Info";
default:
return "None";
}
}

// 获取当前计算机的时间,返回格式:YYYY-MM-DD HH:MM:SS.UUUUUU (含微秒)
string get_current_time()
{
struct timeval tv; // timeval:包含秒和微秒
gettimeofday(&tv, nullptr); // 系统调用:获取当前时间(精确到微秒)

struct tm t; // tm:分解时间,转格式(年、月、日、时、分、秒)
localtime_r(&tv.tv_sec, &t); // 把秒转换成年月日时分秒(本地时区)

char buffer[64]; // 定义字符数组作为格式化输出的缓冲区

snprintf(buffer, sizeof(buffer),
"%04d-%02d-%02d %02d:%02d:%02d.%06ld",
t.tm_year + 1900, // 年:tm_year 从 1900 开始计数
t.tm_mon + 1, // 月:tm_mon 从 0 开始,0 表示 1 月
t.tm_mday, // 日
t.tm_hour, // 时
t.tm_min, // 分
t.tm_sec, // 秒
tv.tv_usec); // 微秒部分,取自 gettimeofday

return string(buffer); // 转换成 string 返回
}

public:
Log() = default; // 使用默认构造
Log(int enable, int classification, string log_path, int console_out)
: enable(enable),
classification(classification),
log_path(log_path),
console_out(console_out)
{

}

// 重载函数调用运算符
void operator()(int level, const string& content)
{
if (enable == 0)
{
return; // 日志未启用
}

string level_str = "[" + level_to_string(level) + "] ";
string log_message;

if (classification == 1)
{
log_message = level_str + "[" + get_current_time() + "] " + content + "\n";
}
else if (classification == 0)
{
log_message = "[" + get_current_time() + "] " + content + "\n";
}
else
{
printf("传入的分类参数错误!\n"); // 分类未启用
return;
}

if (console_out == 1)
{
cout << log_message;
}

log_to_file(level, log_message);
}

private:
// 文件路径的后缀处理函数:当按照日志等级分类存储并且文件路径是 "./log.txt" 这种有文件扩展名时的处理方法
string Suffix_processing(int level, string log_path)
{
string Path;
if (log_path.back() == '/') // 如果是一个目录的路径,比如 "./log/",则最终文件名为 "log_等级名.txt"
{
Path = log_path + "log_" + level_to_string(level) + ".txt";
}
else // 如果是一个文件路径,比如 "./log.txt",则最终文件名为 "log_等级名.txt"
{
size_t pos = log_path.find_last_of('.'); // 从后往前找到第一个 '.' 的位置,即最后一次出现的 '.' 的位置
if (pos != string::npos)
{
string left = log_path.substr(0, pos); // 去掉后缀,即我所需要的有效的前部分路径
string right = log_path.substr(pos); // 保留后缀,即有效的文件扩展名
Path = left + "_" + level_to_string(level) + right; // 组合成新的文件名
}
else // 如果没有文件扩展名(比如 "./log"),则直接在文件名后面加上 "_等级名.txt"
{
Path = log_path + "_" + level_to_string(level) + ".txt";
}
}

return Path;
}

// 核心写文件函数
void log_to_file(int level, const string& log_content)
{
string Path;

if (classification == 1)
{
Path = Suffix_processing(level, log_path); // 按照日志等级分类存储
}
else if (classification == 0)
{
Path = log_path; // 不分类直接使用传入的 log_path
}

// 追加写入,文件不存在则创建,权限 0644
int fd = open(Path.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd < 0)
{
perror("");
exit(FIFO_OPEN_ERR);
}

write(fd, log_content.c_str(), log_content.size());
close(fd);
}
};

3. Client.cc 文件

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
#include "comm.hpp"

int main()
{
Log log(1, 1, "./log.txt", 1);

int fd = open(FIFO_FILE, O_WRONLY); // 以只写方式打开管道文件
if(fd < 0)
{
perror("open");
log(Error, "open FIFO_FILE failed"); // 使用日志记录错误
exit(FIFO_OPEN_ERR);
}

string str; // 定义消息字符串
while(true)
{
cout << "请输入要发送的消息:";
getline(cin, str); // 读取用户输入的消息(一整行)

write(fd, str.c_str(), str.size()); // 向管道文件写入消息,str.c_str()是 string 转换为 C 风格字符串 的方法
log(Info, "发送的消息: " + str); // 记录发送内容
}

close(fd); // 关闭管道文件
return 0;
}

4. Server.cc 文件

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
#include "comm.hpp"

int main()
{
Log log(1, 0, "./log.txt", 1);

int fd = open(FIFO_FILE, O_RDONLY); // 以只读方式打开管道
if(fd < 0)
{
perror("open");
log(Error, "打开文件失败!");
exit(FIFO_OPEN_ERR);
}

while(true)
{
char buf[1024] = {0};
int x = read(fd, buf, sizeof(buf)); // 读取管道数据,x 为读取的字节数
if(x > 0)
{
buf[x] = 0; // 将完整的字节流转换为字符串,并添加结束符‘\0’
cout << "客户端说:" << buf << endl;
log(Debug, "收到消息: " + (string)buf); // 记录接收内容
}
}

close(fd); // 关闭管道

return 0;
}

5. 运行示例

日志插件 Demo 展示