日志
1. 为什么需要日志等级?
在实际生产中,程序输出的信息非常多,如果没有等级就会导致:
- 开发阶段找不到重点(调试信息太多)。
- 上线后也不好排查问题(没有区分严重错误和普通信息)。
因此,合理使用日志等级,能让我们:
- 快速定位错误。
- 过滤无用信息。
- 分环境(开发、测试、生产)灵活控制日志量。
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 文件
#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(主要)
#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 文件
#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 文件
#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;
}