033 日志

143 阅读6分钟

日志

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

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

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

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

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

2. Linux 常见日志等级

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

等级名数值(优先级)典型含义
EMERG0系统不可用,比如内核崩溃(panic)
ALERT1必须立刻采取措施,比如磁盘坏块
CRIT2严重错误,可能导致程序崩溃
ERR (ERROR)3一般错误,需要修复
WARNING4警告信息,可能有潜在风险
NOTICE5正常但需要注意的事件
INFO6普通运行信息
DEBUG7调试信息,开发阶段最详细
  • 数值越小,级别越高,越重要。
  • 在生产环境中,一般只保留 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;
}

5. 运行示例

日志插件 Demo 展示