033 日志
🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022
文章目录
- 日志
- 1. 为什么需要日志等级?
- 2. Linux 常见日志等级
- 3. 日志的实现
- 1. comm.hpp 文件
- 2. Log.hpp(主要)
- 3. Client.cc 文件
- 4. Server.cc 文件
- 5. 运行示例
- 共勉
日志
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); // 微秒部分,取自 gettimeofdayreturn 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}// 追加写入,文件不存在则创建,权限 0644int 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;
}
5. 运行示例
日志插件 Demo 展示
共勉