目录
引言:
1.日志的基本概念
1.1.什么是日志?
1.2.我们为什么需要日志?
2.自己实现一个简易日志
2.1.日志的等级
2.2日志的格式
2.3.获取时间的方法
2.4.日志的主体实现
参数:
代码解析:
问题:写日志的时候,为什么也要保证线程安全?
一、避免数据竞争和不一致
二、确保日志的完整性和可读性
三、防止资源冲突和死锁
四、提高系统的稳定性和可靠性
3.日志的使用
3.1.代码解析:
3.2.实际效果
引言:
今天给大家带来的是用C++语言编写的一个简易日志系统。
1.日志的基本概念
1.1.什么是日志?
在当前的数字化时代,日志分析已经成为了云安全的重要组成部分,日志文件记录了系统、应用程序和网络的各种活动,通过分析这些日志,我们可以发现潜在的安全问题,预防和应对各种安全威胁
1.2.我们为什么需要日志?
在Linux系统下,日志的作用非常关键,它们记录了系统运行过程中的各种事件和信息,对于系统管理、故障排查、安全审计、性能分析和合规性记录等方面都具有重要作用。
记录系统事件: 日志文件记录了系统启动、运行和关闭过程中的各种事件,包括系统错误、警告、信息性和调试信息等。这些信息有助于管理员了解系统的整体运行状况,及时发现并解决问题。故障排查: 当系统或应用程序出现问题时,日志文件可以帮助管理员快速定位问题的根源。通过分析日志文件,管理员可以了解问题发生的时间、原因和影响范围,从而采取相应的措施进行修复。安全审计: 安全相关的日志文件记录了用户登录、权限变更、系统访问等安全事件。这些信息对于检测和防范未授权访问或安全威胁至关重要,有助于管理员及时发现并应对潜在的安全风险。性能监控: 应用程序和中间件的日志文件可以提供性能指标和资源使用情况的信息。通过分析这些信息,管理员可以优化系统和应用程序的性能,提高系统的运行效率。合规性记录: 在某些行业和法规要求下,日志文件作为合规性记录的一部分,用于证明系统操作的合法性和合规性。这些记录有助于企业满足相关的法律法规要求,避免潜在的法律风险。用户行为分析: 通过分析日志文件,管理员还可以了解用户在系统中的行为模式。这有助于管理员进行相应的管理和维护,确保系统的安全和稳定运行。2.自己实现一个简易日志
2.1.日志的等级
我们使用一个枚举成员来枚举日志等级,枚举的使用使得在代码中引用日志级别时,可以使用更具描述性的名称(如 Level::ERROR
),而不是直接使用数字(如 4),从而提高代码的可读性和可维护性。
下面我们来主要讲解每个枚举成员。
DEBUG = 1
:调试级别的日志。这通常用于开发过程中,记录详细的调试信息,帮助开发者定位和解决问题。这里明确地将 DEBUG
赋值为 1,意味着枚举值是从 1 开始的。INFO
:信息级别的日志。用于记录程序的正常运行信息,比如程序的启动和关闭、接收到的请求等。由于枚举值默认递增,INFO
的值会自动设为 2。WARNING
:警告级别的日志。用于记录潜在的有害情况,但不一定立即需要采取行动。其值会自动设为 3。ERROR
:错误级别的日志。用于记录程序运行时发生的错误,这些错误需要被关注,但程序可能仍然能够继续运行。其值会自动设为 4。FATAL
:致命级别的日志。表示程序遇到了无法恢复的错误,程序将无法正常继续运行。其值会自动设为 5。 // 1、日志是有等级的// 让枚举成员默认为整型,并且可以在创建时进行初始化enum Level{ DEBUG = 1, INFO, WARNING, ERROR, FATAL};
2.2日志的格式
日志等级 时间 代码所在的文件名/行数 日志的内容
并且参数是可变参数
2.3.获取时间的方法
我们可以封装一个GetTimeString的函数,方便我们使用,提高代码的可读性。
这个函数旨在获取当前时间并将其格式化为一个字符串。
代码解析:
获取当前时间:
time_t curr_time = time(nullptr);
这行代码使用 time
函数获取当前的系统时间(自1970年1月1日以来的秒数),并将其存储在 time_t
类型的变量 curr_time
中。time
函数的参数是 nullptr
,表示不需要将时间存储在提供的 time_t
对象中(因为我们已经有了 curr_time
来存储它)。
2.将时间转换为本地时间:
struct tm *format_time = localtime(&curr_time);
这行代码使用 localtime
函数将 curr_time
(UTC时间)转换为本地时间,并将结果存储在 struct tm
类型的指针 format_time
指向的结构体中。struct tm
是一个结构体,包含了年、月、日、小时、分钟、秒等信息。
3.错误检查:
if (format_time == nullptr)
return "None";
如果 localtime
函数返回 nullptr
,这通常意味着转换失败(尽管在实际应用中,这种情况非常罕见)。在这种情况下,函数会返回一个字符串 "None"。
4.格式化时间字符串:
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
format_time->tm_year + 1900,
format_time->tm_mon + 1,
format_time->tm_mday,
format_time->tm_hour,
format_time->tm_min,
format_time->tm_sec);
这段代码首先定义了一个字符数组 time_buffer
,用于存储格式化后的时间字符串。然后,使用 snprintf
函数将时间格式化为 "YYYY-MM-DD HH:MM:SS" 的形式,并存储在 time_buffer
中。注意,tm_year
是从1900年开始计数的,所以需要加1900来得到当前的年份;tm_mon
是从0开始计数的,所以需要加1来得到当前的月份。
代码:
std::string GetTimeString(){ time_t curr_time = time(nullptr); struct tm *format_time = localtime(&curr_time); if (format_time == nullptr) return "None"; char time_buffer[1024]; snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d", format_time->tm_year + 1900, format_time->tm_mon + 1, format_time->tm_mday, format_time->tm_hour, format_time->tm_min, format_time->tm_sec); return time_buffer;}
2.4.日志的主体实现
参数:
这个函数接受多个参数,包括文件名、行号、是否保存日志的标志、日志级别、格式化字符串以及可变数量的参数(用于格式化字符串)
代码解析:
使用了C的可变参数列表(varargs)功能来构建一个格式化字符串。
va_list 是一个用于访问可变参数列表的类型,
va_start 宏用于初始化这个列表,
vsnprintf 函数用于将格式化后的字符串写入到指定的缓冲区中,
而 va_end 宏则用于清理与可变参数列表相关的资源。
问题:写日志的时候,为什么也要保证线程安全?
写日志时保证线程安全是至关重要的,这主要基于以下几个原因:
一、避免数据竞争和不一致
在多线程环境中,多个线程可能会同时尝试写入日志。如果没有适当的同步机制,就可能出现数据竞争,导致日志记录不完整、混乱或丢失。例如,一个线程可能正在写入日志的一部分,而另一个线程突然插入其日志记录,从而造成日志内容的交错和混乱。
二、确保日志的完整性和可读性
日志是系统调试、监控和故障排查的重要工具。如果日志记录不完整或混乱,将严重影响其可读性和实用性。保证线程安全可以确保每个日志记录都是完整和独立的,从而便于后续的分析和排查。
三、防止资源冲突和死锁
在多线程写入日志时,如果没有正确的同步机制,还可能导致资源冲突和死锁问题。例如,两个线程可能同时尝试获取对日志文件的写入权限,从而造成资源冲突和阻塞。如果这种情况得不到妥善处理,甚至可能导致系统崩溃或死锁。
四、提高系统的稳定性和可靠性
保证日志记录的线程安全可以大大提高系统的稳定性和可靠性。在并发环境下,系统需要能够正确地处理和记录所有事件和状态变化。如果日志记录出现问题,将可能导致系统状态无法准确追踪和恢复,从而影响系统的整体性能和可靠性。
综上所述,写日志时保证线程安全是非常重要的。这不仅可以避免数据竞争和不一致,确保日志的完整性和可读性,还可以防止资源冲突和死锁问题,提高系统的稳定性和可靠性。因此,在多线程环境中进行日志记录时,必须采取适当的同步机制来确保线程安全。
void LogMessage(std::string filename, int line, bool ssave, int level, const char *format, ...){ std::string levelstr = LevelToString(level); std::string timestr = GetTimeString(); pid_t selfid = getpid(); char buffer[1024]; va_list arg; va_start(arg, format); vsnprintf(buffer, sizeof(buffer), format, arg); va_end(arg); std::string message = "[" + timestr + "]" + "[" + levelstr + "]" + "[" + std::to_string(selfid) + "]" + "[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer; LockGuard lockguard(&lock);//保证日志的线程安全 if (!issave) { std::cout << message; } else { SaveFile(logname, message); }}
3.日志的使用
3.1.代码解析:
do-while(0)结构:宏体被包裹在一个do { ... } while (0)结构中。
这是一种常见的技巧,用于确保宏在使用时能够正确地处理分号(;)和避免潜在的语法错误。
这种结构确保了无论宏体内部有多少语句,宏的使用都像是一个单独的语句一样。
##__VA_ARGS__:这是一个GCC扩展,用于处理可变数量的参数。
##操作符在这里的作用是,如果__VA_ARGS__为空(即没有提供额外的参数),则前面的逗号会被移除,避免语法错误。
#define LOG(level, format, ...) \ do \ { \ LogMessage(__FILE__, __LINE__, issave, level, format, ##__VA_ARGS__); \ } while (0)
所以我们最终在使用日志的时候,第一个参数传递的就是日志的等级,接着就是我们想要打印的可变参数。
3.2.实际效果
就像上面这样使用。正常运行的效果如下图: