一、日志项目的介绍
1.1 为什么要有日志系统
1、⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题,可以借助日志系统来打印⼀些⽇志帮助开发⼈员解决问题
为什么不直接printf打印在屏幕上呢??因为现实中没有谁能够一整天都盯着机房看系统运行信息,而且刷新可能很快根本看不过来
2、上线客户端的产品出现bug⽆法复现并解决,可以借助⽇志系统打印日志并上传到服务端帮助开发人员进行分析
为什么不允许使用调试器呢??因为使用gdb需要对程序的数据进行分析,有些时候是不被允许的,一些工程内的数据都是有隐私,有安全要求的!!
3、对于⼀些高频操作(如定时器、心跳包)在少量调试次数下可能无法触发我们想要的行为,通过断点的暂停⽅式,我们不得不重复操作几十次、上百次甚至更多,导致排查问题效率是非常低下,可以借助打印日志的方式查问题
4、在分布式、多线程/多进程代码中,出现bug比较难以定位,可以借助日志系统打印log帮助定位bug
日志可以快读定位bug出现在哪一个模块从而帮助程序员进行更好的分析
5、帮助首次接触项目代码的新开发人员理解代码的运行流程
1.2 日志系统的作用简述
日志:程序运行过程中所记录的程序运行状态(时间、错误原因、行号......)
作用:记录了程序运行状态信息,以便于程序员能够随时根据状态信息对系统的运行状态进行分析
一般来说日志都是作为一个小组件给其他业务打辅助用的,所以我们为了确保他能够更高效地开发,除了他能够正常使用之外,必要时候也要去阐述他的具体性能!!
1.3 需要实现的功能
1、⽀持多级别日志消息
区分各种信息的程度,比如调试、警告、致命...... 。同时要让程序在发布的时候不要输出调试的信息,而是只输出那些让我们程序出错的信息(设置输出限制,比如未发布的时候设置为调试级别,发布时设为错误级别即低于错误的都不输出)
2、⽀持同步日志和异步日志
同步就是将业务数据写入到数据库的操作由我的业务线程自己完成,而异步是我将数据放到内存里面,而写入的操作由一些专门负责工作的线程负责(因为如果都由我负责,那么万一写入有问题就会导致业务也做不了了)
3、支持可靠写入日志到控制台、文件以及滚动文件中
日志信息可以写到控制台、写到文件、滚动(切换)文件 也可以三个同时进行 想怎么输出由自己决定
滚动文件就是我们如果一直往一个文件里写入那么可能就会很大,所以当一个文件写到一定程度(可以按照文件大小,也可以按照日期来切换,然后设置一个定时任务每天清理3天以前的日志,只保留3天以内的日志文件)的时候切换下一个文件来进行写入
4、支持多线程程序并发写日志 (保证线程安全)
5、⽀持扩展不同的⽇志落地⽬标地
同时也支持扩展,比方说支持把日志信息写到数据库里,或者写到一个日志分析服务器里
1.4 开发环境&核心技术&环境搭建
开发环境(用到哪些软件):
• CentOS7
• vscode/vim
• g++/gdb
• Makefile
核心技术(会用到哪些前置知识):
• 类层次设计(继承和多态的应⽤)
• C++11(多线程、auto、智能指针、右值引⽤等)
• 双缓冲区
• ⽣产消费模型
• 多线程
• 设计模式(单例、工厂、代理、建造者等)
环境搭建(会用到哪个第三方库)
本项⽬不依赖其他任何第三⽅库,只需要安装好CentOS/Ubuntu+vscode/vim环境即可开发。
1.5 日志系统的技术实现
⽇志系统的技术实现主要包括三种类型:
1、利⽤printf、std::cout等输出函数将⽇志信息打印到控制台
2、对于⼤型商业化项⽬,为了⽅便排查问题,我们⼀般会将⽇志输出到⽂件或者是数据库系统⽅便查询和分析⽇志,主要分为同步⽇志和异步⽇志⽅式
1.5.1 同步写日志
同步⽇志是指当输出⽇志时,必须等待⽇志输出语句执⾏完毕后,才能执⾏后⾯的业务逻辑语句,**日志输出语句与程序的业务逻辑语句将在同⼀个线程运行。**每次调⽤⼀次打印⽇志API就对应⼀次系统调⽤write写⽇志⽂件。
在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:
• ⼀⽅⾯,⼤量的⽇志打印陷⼊等量的write系统调⽤,有⼀定系统开销.
• 另⼀⽅⾯,使得打印⽇志的进程附带了⼤量同步的磁盘IO,影响程序性能
1.5.2 异步写日志
异步⽇志是指在进⾏⽇志输出时,日志输出语句与业务逻辑语句并不是在同⼀个线程中运行,而是有专门的线程用于进行日志输出操作。业务线程只需要将⽇志放到⼀个内存缓冲区中不⽤等待即可继续执⾏后续业务逻辑(作为⽇志的⽣产者),⽽⽇志的落地操作交给单独的⽇志线程去完成(作为⽇志的消费者),这是⼀个典型的⽣产-消费模型。
这样做的好处是即使日志没有真的地完成输出也不会影响程序的主业务,可以提⾼程序的性能:
• 主线程调⽤日志打印接⼝成为非阻塞操作
• 同步的磁盘IO从主线程中剥离出来交给单独的线程完成
二、不定参函数
在初学C语⾔的时候,我们都⽤过printf函数进⾏打印。其中printf函数就是⼀个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进⾏数据的格式化。⽽这种不定参函数在实际的使⽤中也⾮常多⻅!!!
2.1 不定参宏函数
我们起初的风格可以这样写,但是这样写的话,我们每次写一些固定的变量的时候都需重复书写(比如 FILE__和__LINE)
cpp
#include<iostream>
using namespace std;
int main()
{
printf("[%s:%d] %s-%d\n", __FILE__, __LINE__, "hello",666);
return 0;
}
所以我们希望 __FILE__和__LINE__这俩无论任何一条日志都要打印的信息,给他设置到宏函数里面!
1、通过把宏参数列表中最后的参数写成省略号(...),使其可以接受数量可变的宏参数。
2、后边用不定参数宏__VA_ARGS__可以自动扩展
按理来说一定要带参数否则__VA_ARGS__无法扩展,最后替换时会留一个','导致错误,但是我们可以用##来避免这个问题
3、## __VA_ARGS__是为了确保当没有传入任何参数的时候,把最后面的','给去掉
##告诉编译器。如果我没有传任何参数给__VA_ARGS__,那么就把前面的','去掉
cpp
#include<iostream>
#define LOG(fmt, ...) printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
int main()
{
//printf("[%s:%d] %s-%d\n", __FILE__, __LINE__, "hello",666);
LOG("%s-%d", "hello", 666);
return 0;
}
运行结果:
使用了宏定义来实现日志功能,虽然这种方式可以达到目的,但宏定义在C++中不够类型安全,并且调试时不如函数调用方便。
2.2 C风格不定参函数
头文件strarg.h中定义了一组对象、方法使得我们可以使用不定参数。
- va_list ap:用于储存省略部分数据的对象类型
- va_start(format, ap):使得ap指向format后的不定参数列表,即不定参数列表中的第一个参数
- int tmp = va_arg(ap, int):将当前ap指向的值返回,并使ap指针按照type类型向后移动,va_arg中第二个参数类型名要与返回值类型相同(决定了向后移动几个字节)
- va_end(ap):完成清理工作,释放动态分配申请的用于存储参数的内存
cpp
#include <iostream>
#include <cstdarg>
void printNum(int n, ...) {
va_list al;
va_start(al, n);//让al指向n参数之后的第⼀个可变参数
for (int i = 0; i < n; i++) {
int num = va_arg(al, int);//从可变参数中取出⼀个整形参数,然后向后移动int个字节
std::cout << num << std::endl;
}
va_end(al);//清空可变参数列表--其实是将al置空
}
int main()
{
printNum(3, 11,22,33);
printNum(5, 44,55,66,77,88);
return 0;
}
注意:虽然是不定参,但是也要遵守规定,比如第一个数字就是专门用来确定后面有多少个参数的!如果乱写的话可能导致未定义的行为,因为va_arg
会尝试读取超出传入参数数量的内存。
如果我们的va_argc传的类型不匹配呢??那这必然导致我们读到的数据是错的!!!
所以这也是为什么printf有格式化字符串,就是为了告诉编译器接下来要从后面读几个字节的数据,应该当做什么类型去做处理!!
vasprintf 是一个C库函数,它允许通过可变参数列表创建格式化字符串,并将其存储在动态分配的内存中。这个函数的行为类似于printf,但它不会将结果输出到标准输出,而是将格式化后的字符串存储在一个字符指针变量中。
char**strp:一级指针的地址,会在动态分配的内存中给我们的格式化字符串分配足够的空间
const char*fmt:带格式化的字符串
va_list ap:从ap里面一个个取参数进行解析,然后将组织好的字符串放到我们预先申请的空间里
注意因为这个空间是由OS帮我们申请的,所以最后我们一定要记得free!!
成功的话会返回对应的字节数,失败的话会返回-1
cpp
#define _GNU_SOURCE
#include<iostream>
#include<cstdarg>
#include<stdlib.h>
void myprintf(const char* fmt, ...)
{
va_list ap;
va_start(ap, fmt);
char*res; //不需要我们自己申请
int ret= vasprintf(&res,fmt,ap);
if(ret!=-1)
{
printf(res);
free(res);
}
va_end(ap);
}
int main()
{
myprintf("hello %s", "world");
return 0;
}
2.3 C++风格不定参函数
cpp使用的是模版参数包
sizeof ...(args)是固定写法
因为递归到最后并没有空函数的xprintf,因此我们还得定义一个没有函数的xprintf
cpp
#include <iostream>
#include <cstdarg>
#include <memory>
#include <functional>
void xprintf() {
std::cout << std::endl;
}
template<typename T, typename ...Args>
void xprintf(const T &value, Args &&...args) {
std::cout << value << " ";
if ((sizeof ...(args)) > 0) {
xprintf(std::forward<Args>(args)...);
}else {
xprintf();
}
}
int main()
{
xprintf("hello");
xprintf("hello", 666);
xprintf("hello", "world", 666);
return 0;
}