【tag-002】Linux C++ 网络编程之日志系统

本章节开始搭建我们自己的工程日志

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. 结语

至此,完成日志功能;
下一节继续添加配置文件的读取功能

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值