本章节开始搭建我们自己的工程日志
1. 向终端打印消息
在写代码过程中为了方便调试,经常需要向终端打印一些调试信息或者错误消息。
1.1 宏定义头文件
在 include 目录新建 ngx_macro.h 文件,专门用于保存宏定义
//规定一条日志最大长度
#define MAX_ERR_LEN 2048
//返回拷贝数据后的终点位置
#define ngx_cpymem(dst,src,n) (((u_char *)memcpy(dst,src,n)) + (n))
//最大的32位无符号数:十进制是4294967295
#define NGX_MAX_UINT32_VALUE (uint32_t) 0xffffffff
#define NGX_INT64_LEN (sizeof("-9223372036854775808") - 1)
1.2 常用函数头文件
在 include 目录新建 ngx_func.h 文件,通用的函数声明放在这个文件中
//日志,终端打印函数
void ngx_log_stderr(int err, const char* fmt, ...);
//核心函数,用于拼接字符串,支持类似 (”这是字符串%d与%s的拼接“,1,"字符串2") 的字符串拼接
unsigned char *ngx_vslprintf(unsigned char *buf, unsigned char *last,const char *fmt,va_list args);
unsigned char *ngx_log_errno(unsigned char *buf, unsigned char *last,int err);
//对函数 ngx_vslprintf 的新的包装
unsigned char *ngx_slprintf(unsigned char *buf, unsigned char *last, const char *fmt, ...);
1.3 函数实现
我们将 log 打印的函数和拼接字符串相关的函数的定义放在不同的文件中,在 src 目录新建 ngx_log.cxx 用于实现 log 打印相关的函数;新建 ngx_printf.cxx 用于实现字符串拼接相关的函数。
1.3.1 ngx_log.cxx
void ngx_log_stderr(int err, const char* fmt, ...)
{
va_list args;
u_char errstr[MAX_ERR_LEN + 1];
u_char *p,*last;
memset(errstr,0,sizeof(errstr));
last = errstr + MAX_ERR_LEN;
p = ngx_cpymem(errstr, "Jokey_Chan: ", 11);
va_start(args,fmt);
//字符串拼接,定义在 ngx_printf.cxx
p = ngx_vslprintf(p,last,fmt,args);
va_end(args);
//非零表示代码中逻辑发生错误
if(err)
{
//在这里将错误写入文件
p = ngx_log_errno(p,last,err);
}
//超过长度,作截断处理
if(p>=(last-1))
{
p = (last-1)-1;
}
*p++ = '\n';
write(STDERR_FILENO,errstr,p-errstr);
return;
}
/**
* @brief 给一段内存,一个错误编号,我要组合出一个字符串,形如: (错误编号: 错误原因),放到给的这段内存中去
* 这个函数我改造的比较多,和原始的nginx代码多有不同
*
* @param buf : 是个内存,要往这里保存数据
* @param last : 放的数据不要超过这里
* @param err : 错误编号,我们是要取得这个错误编号对应的错误字符串,保存到buffer中
* @return unsigned char* : 指向字符串末尾的指针
*/
unsigned char *ngx_log_errno(unsigned char *buf, unsigned char *last,int err)
{
char *perrorInfo = strerror(err);
size_t len = strlen(perrorInfo);
char leftStr[10] = {0};
sprintf(leftStr,"(%d: ",err);
size_t lLen = strlen(leftStr);
char rightStr[] = ")";
size_t rLen = strlen(rightStr);
if((buf+lLen+len+rLen)<last)
{
buf = ngx_cpymem(buf,leftStr,lLen);
buf = ngx_cpymem(buf,perrorInfo,len);
buf = ngx_cpymem(buf,rightStr,rLen);
}
return buf;
}
1.3.2 ngx_printf.cxx
static u_char * ngx_sprintf_num(u_char *buf, u_char *last, uint64_t ui64, u_char zero, uintptr_t hexadecimal, uintptr_t width)
{
u_char *p, temp[NGX_INT64_LEN + 1];
size_t len;
uint32_t ui32;
static u_char hex[] = "0123456789abcdef";
static u_char HEX[] = "0123456789ABCDEF";
p = temp + NGX_INT64_LEN;
if (hexadecimal == 0)
{
if (ui64 <= (uint64_t) NGX_MAX_UINT32_VALUE)
{
ui32 = (uint32_t) ui64;
do
{
*--p = (u_char) (ui32 % 10 + '0');
}
while (ui32 /= 10);
}
else
{
do
{
*--p = (u_char) (ui64 % 10 + '0');
} while (ui64 /= 10);
}
}
else if (hexadecimal == 1)
do
{
*--p = hex[(uint32_t) (ui64 & 0xf)];
} while (ui64 >>= 4);
}
else
{
do
{
*--p = HEX[(uint32_t) (ui64 & 0xf)];
} while (ui64 >>= 4);
}
len = (temp + NGX_INT64_LEN) - p;
while (len++ < width && buf < last)
{
*buf++ = zero;
}
len = (temp + NGX_INT64_LEN) - p;
if((buf + len) >= last)
{
len = last - buf;
}
return ngx_cpymem(buf, p, len);
}
u_char *ngx_vslprintf(u_char *buf, u_char *last,const char *fmt,va_list args)
{
u_char zero;
uintptr_t width,sign,hex,frac_width,scale,n;
int64_t i64; //保存%d对应的可变参
uint64_t ui64; //保存%ud对应的可变参,临时作为%f可变参的整数部分也是可以的
u_char *p; //保存%s对应的可变参
double f; //保存%f对应的可变参
uint64_t frac; //%f可变参数,根据%.2f等,取得小数部分的2位后的内容;
while(*fmt && buf<last)
{
if('%' == *fmt)
{
zero = (u_char)((*++fmt) == '0' ? '0' : ' ');
width = 0;
sign = 1;
hex = 0;
frac_width = 0;
i64 = 0;
ui64 = 0;
while(*fmt >='0' && *fmt <= '9')
{
width = width *10 + (*fmt++ - '0');
}
for(;;)
{
switch(*fmt)
{
case 'u':
sign = 0;
fmt++;
continue;
case 'X':
hex = 2; //十六进制大写
sign = 0;
fmt++;
continue;
case 'x':
hex = 1; //十六进制小写
sign = 0;
fmt++;
continue;
case '.':
fmt++;
while(*fmt >= '0' && *fmt <= '9')
{
frac_width = frac_width * 10 + (*fmt++ - '0');
}
break;
default:
break;
}
break;
}
switch(*fmt)
{
case '%':
*buf++ = '%';
fmt++;
continue;
case 'd':
if(sign)//无符号类型
{
i64 = (int64_t)va_arg(args,int);
}
else
{
ui64 = (uint64_t)va_arg(args,int);
}
break;
case 's': //字符串
p = va_arg(args,u_char*);
while(*p && buf<last)
{
*buf++ = *p++;
}
fmt++;
continue;
case 'p':
i64 = (int64_t)va_arg(args,pid_t);
sign = 1;
break;
case 'f':
f = va_arg(args,double);
if(f<0)
{
*buf++ = '-';
f = -f;
}
ui64 = (int64_t)f;
frac = 0;
if(frac_width)
{
scale = 1;
for(n=frac_width;n;--n)
{
scale *= 10;
}
//取出小数部分
frac = (uint64_t)((f - (double)ui64)*scale + 0.5);
if(frac == scale)//需要进位了
{
ui64++;
frac = 0;
}
}
//正整数部分
buf = ngx_sprintf_num(buf, last, ui64, zero, 0, width);
if(frac_width)
{
if(buf<last)
{
*buf++ = '.';
}
buf = ngx_sprintf_num(buf, last, frac, '0', 0, frac_width); //frac这里是小数部分
}
fmt++;
continue;
default:
*buf++ = *fmt++;
continue;
}
if(sign) //有符号数
{
if (i64 < 0) //这可能是和%d格式对应的要显示的数字
{
*buf++ = '-'; //小于0,自然要把负号先显示出来
ui64 = (uint64_t) -i64; //变成无符号数(正数)
}
else //显示正数
{
ui64 = (uint64_t) i64;
}
}
buf = ngx_sprintf_num(buf, last, ui64, zero, hex, width);
fmt++;
}
else
{
*buf++ = *fmt++;
}
}
return buf;
}
/**
* 对于 nginx 自定义的数据结构进行标准格式化输出,就像 printf,vprintf 一样,我们顺道学习写这类函数到底内部是怎么实现的
* 该函数只不过相当于针对ngx_vslprintf()函数包装了一下
*/
unsigned char *ngx_slprintf(unsigned char *buf, unsigned char *last, const char *fmt, ...)
{
va_list args;
u_char *p;
va_start(args, fmt); //使args指向起始的参数
p = ngx_vslprintf(buf, last, fmt, args);
va_end(args); //释放args
return p;
}
1.4 编译、运行、测试
在 main 函数加入测试代码
int main(int argc, char* const* argv)
{
for(int i = 1;i<=5;++i)
{
ngx_log_stderr(0, "这是第 %d 条终端测试 log!");
}
return 0;
}
编译运行程序
cd ./build
cmake ..
make
cd ../bin
./nginx_sim
至此完成了向终端打印日志的功能
2. 向日志文件中输出日志
我们暂时将输出目录默认在 bin/log 中。
2.1. 建立日志等级、全局变量头文件
在本工程中,建立 0-8 共 9 个日志等级,将代表等级的宏定义放在 ngx_macro.h 文件中,在之后的代码中,默认等级设置为 NGX_LOG_NOTICE, 所有大于设置等级的日志都不会输出。
//控制台错误【stderr】:最高级别日志,日志的内容不再写入log参数指定的文件,而是会直接将日志输出到标准错误设备比如控制台屏幕
#define NGX_LOG_STDERR 0
#define NGX_LOG_EMERG 1 //紧急 【emerg】
#define NGX_LOG_ALERT 2 //警戒 【alert】
#define NGX_LOG_CRIT 3 //严重 【crit】
#define NGX_LOG_ERR 4 //错误 【error】:属于常用级别
#define NGX_LOG_WARN 5 //警告 【warn】:属于常用级别
#define NGX_LOG_NOTICE 6 //注意 【notice】
#define NGX_LOG_INFO 7 //信息 【info】
#define NGX_LOG_DEBUG 8 //调试 【debug】:最低级别
//定义日志存放的路径和文件名
#define NGX_ERROR_LOG_PATH "logs/error1.log"
在 include 目录新建 ngx_global.h 头文件用于保存整个工程的全局变量,首先要添加的保存日志等级和文件描述符的结构体 ngx_log_t
typedef struct log_t
{
int log_level; // log 等级,分为0-8共9个等级
char fd; // 日至文件描述符
log_t():log_level(6),fd(-1){}
}ngx_log_t;
extern ngx_log_t ngx_log;
2.2 日志初始化
初始化的作用在于从配置文件中读取 log 目录以及日志等级;本章节暂时不涉及配置文件的读取,暂时使用默认的目录与等级。
在 ngx_func.h 文件中声明初始化函数
void ngx_log_init();
在 ngx_log.cxx 中定义
ngx_log_t ngx_log;
void ngx_log_init()
{
u_char *plogname = NULL;
/*这里要从配置文件读取目录*/
if(NULL == plogname)
{
//"logs/error.log" ,logs目录需要提前建立出来
plogname = (u_char *) NGX_ERROR_LOG_PATH;
}
//缺省日志等级为6【注意】
ngx_log.log_level = NGX_LOG_NOTICE;
//只写打开|追加到末尾|文件不存在则创建【这个需要跟第三参数指定文件访问权限】
//mode = 0644:文件访问权限, 6: 110 , 4: 100
ngx_log.fd = open((const char *)plogname,O_WRONLY|O_APPEND|O_CREAT,0644);
//如果有错误,则直接定位到 标准错误上去
if (ngx_log.fd == -1)
{
ngx_log_stderr(errno,"[alert] could not open error log file: open() \"%s\" failed", plogname);
ngx_log.fd = STDERR_FILENO; //直接定位到标准错误去了
}
return;
}
2.3 写日志函数实现
在 ngx_func.h 文件中声明初始化函数
void ngx_log_error_core(int level, int err, const char *fmt, ...);
在 ngx_log.cxx 中定义
//等级错误
static u_char err_levels[][20] =
{
{"stderr"}, //0:控制台错误
{"emerg"}, //1:紧急
{"alert"}, //2:警戒
{"crit"}, //3:严重
{"error"}, //4:错误
{"warn"}, //5:警告
{"notice"}, //6:注意
{"info"}, //7:信息
{"debug"} //8:调试
};
/**
* 日过定向为标准错误,则直接往屏幕上写日志【比如日志文件打不开,则会直接定位到标准错误,此时日志就打印到屏幕上,参考ngx_log_init()】
* level:一个等级数字,我们把日志分成一些等级,以方便管理、显示、过滤等等,如果这个等级数字比配置文件中的等级数字"LogLevel"大,那么该条信息不被写到日志文件中
* err:是个错误代码,如果不是0,就应该转换成显示对应的错误信息,一起写到日志文件中,
* ngx_log_error_core(5,8,"这个XXX工作的有问题,显示的结果是=%s","YYYY");
*/
void ngx_log_error_core(int level, int err, const char *fmt, ...)
{
u_char *last;
u_char errStr[MAX_ERR_LEN + 1];
memset(errStr,0,sizeof(errStr));
last = errStr + MAX_ERR_LEN;
struct timeval tv;
struct tm tm;
time_t sec;
u_char *p; //指向当前要拷贝数据到其中的内存位置
va_list args;
memset(&tv,0,sizeof(struct timeval));
memset(&tm,0,sizeof(struct tm));
//获取当前时间,返回自1970-01-01 00:00:00到现在经历的秒数【第二个参数是时区,一般不关心】
gettimeofday(&tv,NULL);
sec = tv.tv_sec;
//把参数1的time_t转换为本地时间,保存到参数2中去,带_r的是线程安全的版本,尽量使用
localtime_r(&sec,&tm);
//需要作一下调整
tm.tm_mon++;
tm.tm_year += 1900;
//组装出时间字符串
u_char strCurrTime[40] = {0};
ngx_slprintf(strCurrTime, (u_char*)-1, "%4d-%02d-%02d %02d:%02d:%02d",tm.tm_year,tm.tm_mon,tm.tm_mday,tm.tm_hour,tm.tm_min,tm.tm_sec);
p = ngx_cpymem(errStr,strCurrTime,strlen((const char*)strCurrTime));
//日志等级
p = ngx_slprintf(p,last," [%s] ",err_levels[level]);
//进程号
p = ngx_slprintf(p,last," [%p] ",ngx_pid);
va_start(args,fmt);
p = ngx_vslprintf(p,last,fmt,args);
va_end(args);
//错误码不为0,表示发生错误
if(err)
{
p = ngx_log_errno(p,last,err);
}
if(p > (last - 1))
{
p = (last - 1) - 1;
}
*p++ = '\n';
ssize_t n;
while(1)
{
if(level > ngx_log.log_level)
{
break;
}
n = write(ngx_log.fd,errStr,p-errStr);
if(-1 == n)
{
if(errno == ENOSPC)
{
//这里表示磁盘没有空间
}
else
{
//这是有其他错误,那么我考虑把这个错误显示到标准错误设备吧;
if(ngx_log.fd != STDERR_FILENO) //当前是定位到文件的,则条件成立
{
n = write(STDERR_FILENO,errStr,p - errStr);
}
}
}
break;
}
return;
}
2.4 编译、运行、测试
在 main 函数加入测试代码;在 bin/logs 目录下可以看到日志文件。
pid_t ngx_pid; //当前进程ID
int main(int argc, char* const* argv)
{
ngx_pid = getpid();
//日志初始化(创建/打开日志文件)
ngx_log_init();
for(int i = 1;i<=5;++i)
{
ngx_log_stderr(0, "这是第 %d 条终端测试 log!",i);
ngx_log_error_core(NGX_LOG_WARN, 0, "这是第 %d 条测试 log!",i);
}
return 0;
}
3. 结语
至此,完成日志功能;
下一节继续添加配置文件的读取功能;