文章目录
什么是日志
日志(log)是一种记录和存储系统运行状态,事件,和操作历史的文件和数据库,这些记录通常以时间顺序排列且详细记录系统中发生的各种活动;
日志的类型常见有以下几种:
-
系统日志
记录操作系统级别的事件,如启动,关机,错误和警告等;
-
应用日志:
记录应用程序运行过程中的事件,如用户操作,错误,状态变更等;
-
安全日志
记录系统和应用程序的安全相关事件,如登录尝试,权限变更等;
日志的作用为以下:
-
问题诊断和故障排查
日志可以帮助系统管理员和开发人员诊断和解决问题;
-
系统监控和维护
通过分析日志,管理员可以监控系统的运行状态并且识别潜在的问题和异常;
-
安全审计和合规性
日志记录系统和应用程序的各种安全事件以帮助进行安全审计,同时确保系统符合安全和法规要求;
-
性能分析和优化
日志数据可以用于分析系统和应用程序的性能,并间接提升系统效率和响应速度;
-
历史记录和追踪
日志提供了系统和应用程序运行过程中的详细历史记录,可以用于追踪和回溯系统的操作和变更;
日志记录通常包含以下基本信息:
-
时间戳
记录事件发生的具体事件;
-
事件类型
记录事件的类型,如错误,警告,信息等;
-
事件来源
知名事件是由哪个系统组件或者是应用程序生成的;
-
事件描述
详细描述事件的内容和相关信息;
本文主要简单设计一个简单应用日志;
应用日志的设计要点
应用日志专门记录应用程序正在运行过程中发生的各种事件,错误,状态变化等信息;
应用日志可以帮助跟踪程序的执行过程并进行诊断从而了解具体问题;
应用日志的日志级别主要如下:
-
DEBUG
详细的信息,一般用于诊断问题,经常用于开发过程;
-
INFO
普通操作信息,用于记录应用程序的正常运行情况;
-
WARNING
警告信息,标明看那会出现问题的情况,但程序仍可以正常运行;
-
ERROR
错误信息,标明程序中的某些功能出现问题需要引起注意;
-
CRITICAL/FATAL
严重错误(致命)信息,标明程序可能无法继续运行,需要立即解决;
日志的统一格式有助于日志的解析和分析;
常见的格式包括JSON
,XML
或简单的纯文本格式;
如:
bash
2024-07-15 10:00:00 [INFO] [module_name] - This is an info message.
2024-07-15 10:01:00 [ERROR] [module_name] - An error occurred.
日志的存储通常有以下几种:
-
文件
将日志记录到文件中,方便查看和归档;
-
数据块
将日志存储到数据库中,便于查询和分析;
-
日志管理系统
使用专门的日志管理工具,如
ELK
堆栈,进行集中化管理和分析;
简单应用日志插件设计编码
主要思路为封装一个为log
的类;
在类中实现根据错误编码产生的日志并使用对应的方式打印至显示器或写入文件中;
文件为:
-
log.hpp
该文件主要封装日志插件并实现;
-
makefile
自动化构建过程文件;
-
test.cc
测试文件;
大概框架及所需头文件与定义
cpp
#pragma once
#include <fcntl.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <iostream>
#define INFO 0 // 常规信息
#define DEBUG 1 // debug信息
#define WARNING 2 // 警告信息
#define ERROR 3 // 错误信息
#define FATAL 4 // 致命错误信息
#define SCREEN 1 // 向显示器文件写入
#define ONEFILE 2 // 向单个文件进行写入
#define CLASSIFY 3 // 分类将日志进行写入
#define LOG_BUFFER_SIZE 1024 // 日志所需大小
#define LogFile "log.txt" // 默认日志文件文件名
class Log {
public:
Log() { // 构造函数 用于初始化默认打印方式以及设置默认路径
printMethod = SCREEN;
path = "./log/";
}
~Log() { ; } // 析构函数 未使用
private:
int printMethod; // 打印风格
std::string path; // 默认路径
};
构造函数用于初始化默认打印风格以及初始化默认路径;
利用 #define
定义出可能需要使用的常量同时包含所需头文件;
日志信息的输出
此处使用的日志输出格式为:
bash
[loglevel][time] logmassage
日志的格式主要为 : 默认部分 + 自定义部分;
默认部分一般为 日志等级 与 日志时间 ;
-
日志等级
日志等级需要以字符串的形式进行显示,则可以使用
switch() case:
对日志等级进行返回;cppconst std::string levelToString(int level) { // 以字符串形式获取日志等级 switch (level) { case INFO: return "INFO"; case DEBUG: return "DEBUG"; case WARNING: return "WARNING"; case ERROR: return "ERROR"; case FATAL: return "FATAL"; default: return "NONE"; } }
需要注意此处返回的是
std::string
类型,若是需要打印字符串或是当做字符串使用需要调用其string::c_str()
; -
日志时间
日志时间一般采用时间戳的方式获取;
常见的系统调用接口有
time()
系统调用接口与gettimeofday()
系统调用接口,此处使用time()
;bashNAME time - get time in seconds SYNOPSIS #include <time.h> time_t time(time_t *t); DESCRIPTION time() returns the time as the number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). If t is non-NULL, the return value is also stored in the memory pointed to by t. RETURN VALUE On success, the value of time in seconds since the Epoch is returned. On error, ((time_t) -1) is returned, and errno is set appropriately.
调用
time()
系统调用接口将会返回time_t
类型,实际打印的是未格式化的时间戳;若是需要格式化需要调用
localtime()
接口将时间戳进行实例化;bashNAME asctime, ctime, gmtime, localtime, mktime, asctime_r, ctime_r, gmtime_r, localtime_r - transform date and time to broken-down time or ASCII SYNOPSIS #include <time.h> struct tm *localtime(const time_t *timep);
其会返回一个
struct tm
类型的结构体,其结构体构造如下:cppstruct tm { int tm_sec; /* seconds */ int tm_min; /* minutes */ int tm_hour; /* hours */ int tm_mday; /* day of the month */ int tm_mon; /* month */ int tm_year; /* year */ int tm_wday; /* day of the week */ int tm_yday; /* day in the year */ int tm_isdst; /* daylight saving time */ };
可根据需要获取结构中数据;
可用
printf
打印进行debug
显示数据是否正确:cppprintf("%d-%d-%d %d:%d:%d\n", ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
在
time()
系统调用接口中介绍了时间戳的开始时间,为了显示正确时间需要加上对应的数值;
自定义部分需要使用可变参数列表即...
;
需要调用vsnprintf()
来获取用户自定义的内容:
bash
NAME
printf, fprintf, sprintf, snprintf, vprintf, vfprintf, vsprintf, vsnprintf - formatted output conversion
SYNOPSIS
#include <stdarg.h>
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
snprintf(), vsnprintf():
_BSD_SOURCE || _XOPEN_SOURCE >= 500 || _ISOC99_SOURCE || _POSIX_C_SOURCE >= 200112L;
or cc -std=c99
DESCRIPTION
The functions in the printf() family produce output according to a format as described below. The functions printf() and
vprintf() write output to stdout, the standard output stream; fprintf() and vfprintf() write output to the given output stream;
sprintf(), snprintf(), vsprintf() and vsnprintf() write to the character string str.
在使用可变参数的时候需要定义va_list
变量来表示可变参数;
并且使用va_start()
来初始化一个va_list
类型的变量,这个变量用来访问传递给函数的可变参数;
结束使用后需要用va_end()
结束对可变参数的访问;
cpp
va_list s;
va_start(s, format);
char custombuffer[LOG_BUFFER_SIZE];
vsnprintf(custombuffer, sizeof(custombuffer), format, s);
va_end(s);
当获取结束默认部分与自定义部分后调用snprintf()
将两个信息组合成一条信息,并根据接下来的需求输出日志信息;
-
输出日志信息代码
cppvoid logmessage(int level, const char* format, ...) { // 获取时间 time_t currentTime = time(nullptr); struct tm* ctime = localtime(¤tTime); // 格式: 默认部分 + 自定义部分 // 默认部分 char defaultbuffer[LOG_BUFFER_SIZE]; snprintf(defaultbuffer, sizeof(defaultbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // 自定义部分 va_list s; va_start(s, format); char custombuffer[LOG_BUFFER_SIZE]; vsnprintf(custombuffer, sizeof(custombuffer), format, s); va_end(s); // 组合 char logtxt[LOG_BUFFER_SIZE * 2]; snprintf(logtxt, sizeof(logtxt), "%s %s\n", defaultbuffer, custombuffer); // 组合完毕后可直接打印也可直接写到文件中 // std::cout << logtxt << std::endl; - 暂时打印至显示器中; printLog(level, logtxt); }
日志的输出方式
可根据已定义的日志输出方式使用switch()case:
分类进行输出;
cpp
void Enable(int method) { printMethod = method; } // 更改日志输出方式
Enable()
函数用来根据用户需求更改日志的打印方式;
cpp
void printLog(int level, const std::string& logtxt) { // 不同方式输出日志
switch (printMethod) {
case SCREEN:
/* code */
std::cout << logtxt << std::endl;
break;
case ONEFILE:
printOneFile(LogFile, logtxt);
break;
case CLASSIFY:
printClassify(level, logtxt);
break;
default:
break;
}
}
printLog()
通过使用switch()case:
对日志信息输出进行控制,输出至屏幕时直接std::cout
打印至屏幕即可;
-
将日志信息写入至同一日志文件
log.txt
思路为:
打开文件,将日志信息进行写入最后关闭文件;
cppvoid printOneFile(const std::string& logname, const std::string& logtxt) { // 单个文件输出日志 std::string _logname = path + logname; int fd = open(_logname.c_str(), O_CREAT | O_APPEND | O_WRONLY, 0666); if (fd < 0) { std::cerr << "open fail" << std::endl; return; } write(fd, logtxt.c_str(), logtxt.size()); close(fd); }
-
将日志信息进行分类归档
思路与写进同一文件思路相同;
创建一个
std::string
对象为默认的文件名,并调用levelToString()
对文件名进行修改;最后调用
printOneFile()
函数即可;cppvoid printClassify(int level, const std::string& logtxt) { // 分类形式输出日志 std::string filename = LogFile; filename += "."; filename += levelToString(level); printOneFile(filename, logtxt); }
|-------------------------------------------------------|
| 需要注意,尽量在makefile
中使用mkdir
命令来优先创建log
目录,否则会打开文件失败; |
operator()重载
调用过麻烦的话可将void logmessage(int level, const char\* format, ...)
函数封装为void operator()(int level, const char* format,...)
以减少调用繁琐;
cpp
void operator()(int level, const char* format,
...) { // 重载()操作符并将原本接口进行封装
// 获取时间
time_t currentTime = time(nullptr);
struct tm* ctime = localtime(¤tTime);
// printf("%d-%d-%d %d:%d:%d\n", ctime->tm_year + 1900, ctime->tm_mon + 1,
// ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// //打印 debug
// 格式: 默认部分 + 自定义部分
// 默认部分
char defaultbuffer[LOG_BUFFER_SIZE];
snprintf(defaultbuffer, sizeof(defaultbuffer),
"[%s][%d-%02d-%02d %02d:%02d:%02d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// 自定义部分
va_list s;
va_start(s, format);
char custombuffer[LOG_BUFFER_SIZE];
vsnprintf(custombuffer, sizeof(custombuffer), format, s);
va_end(s);
// 组合
char logtxt[LOG_BUFFER_SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", defaultbuffer, custombuffer);
// 组合完毕后可直接打印也可直接写到文件中
// std::cout << logtxt << std::endl;
printLog(level, logtxt);
}
完整代码(供参考):