四、日志系统:log文件夹下的log.h和log.cpp

本章是具体的日志系统类的介绍

1. 基础API

介绍一些API用法

(1)fputs

#include <stdio.h>
int fputs(const char *str, FILE *stream);
  • str:数组,包含要写入stream的字符序列,以空字符结尾

  • stream:指向FILE对象的指针,该FILE对象标志了要被写入字符串的流。

(2)#、##、__VA_ARGS__和##VA_ARGS


  • #用来把参数转换成字符串

#字符只能用于宏定义
#会把A的原本样式一字不差的输出出来

#define P(A) printf("%s:%d\n",#A,A);
//#define P(A) printf(#A"%d\n",A);//可以换成这样
int main(int argc, char **argv)
{

       int a = 1, b = 2;
       P(a);
       P(b);
       P(a+b);
}
/*输出为
a:1
b:2
a+b:3
*/

  • ##类似于字符间的粘合剂,会在宏定义中把两个语言符号组合成单个语言符号
#define P(A,n) printf(#A#n"=%d\n",A##n)
int main()
{
	int a0 = 0, a1 = 1;
	P(a, 0);
	P(a, 1);

	return 0;
}
/*//输出
a0=0
a1=1
*/
#define A(n) a##n
int main()
{
   int a0 = 0, a1 = 1;
   //A(0)会被编译器当成  a0  ,A(1)会被编译器当成   a1;##的作用就是胶合两个字符
   return 0;
}

  • __VA_ARGS__和##VA_ARGS
    定义时宏定义中参数列表的最后一个参数为省略号
#define my_printf1(...) printf(__VA_ARGS__)
#define my_printf2(format, ...) printf(format, __VA_ARGS__)
#define my_printf3(format, ...) printf(format, ##__VA_ARGS__)
int main()
{
    my_printf1("a = %d\n", 1);//正确,输出1,因为可以添加任意参数

    my_printf2("a = %d\n", 1);//正确,输出1,因为可以添加任意参数
    my_printf2("a\n");//错误,因为可变参数为0时,前边有一个逗号",",他至少需要两个以上的参数。

    my_printf3("a = %d\n", 1);//正确,输出1,因为可以添加任意参数
    my_printf3("a\n");//正确,__VA_ARGS__带上##,变成##__VA_ARGS__可以把可变参数的个数为0。

    return 0;
}

(3)fflush

#include <stdio.h>
int fflush(FILE *stream);

fflush()会强制把缓冲区内的数据写到参数stream指定的文件中,如果参数stream为NULL,fflush()会将所有打开的文件数据更新。

在使用多个输出函数连续进行多次输出到控制台时,有可能下一个数据再上一个数据还没输出完毕,还在输出缓冲区中时,下一个printf就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出现输出错误。

在prinf()后加上fflush(stdout); 强制马上输出到控制台,可以避免出现上述错误。


2. 流程图

  • 日志文件

    • 局部变量的懒汉模式获取实例

    • 生成日志文件,并判断同步和异步写入方式

  • 同步

    • 判断是否分文件

    • 直接格式化输出内容,将信息写入日志文件

  • 异步

    • 判断是否分文件

    • 格式化输出内容,将内容写入阻塞队列,创建写线程,从阻塞队列中取出内容写入日志文件。

初始化,生成日志文件
同步
需要分文件,创建新的日志文件
同步直接写入
异步
需要分文件,创建新的日志文件
写入阻塞队列
写线程异步写入
单例模式:获取日志实例 get_instance()
init函数
write_log
分文件判断
日志文件
格式化输出内容
write_log
分文件判断
格式化输出内容
flush_log_thread内部调用async_write_log

3.日志类定义

log.h

#ifndef LOG_H
#define LOG_H

#include <stdio.h>
#include <iostream>
#include <string>
#include <stdarg.h>
#include <pthread.h>
#include "block_queue.h"

class Log
{
public:
    //局部静态变量的线程安全懒汉模式,C++11之后不用加锁。
    static Log *getinstance()
    {
        static Log instance;
        return &instance;
    }

    //异步写日志公有方法,调用私有方法async_write_log
    static void *flush_log_thread(void *args)
    {
        Log::getinstance() -> async_write_log();
    }

    //初始化参数。可以选择的参数有:日志文件名称、日志缓冲区大小、最大行数、最长日志条队列
    bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);

    //将输出内容按照标准格式整理
    void write_log(int level, const char *format, ...);

    //强制刷新缓冲区
    void flush(void);

private:
    Log();
    virtual ~Log();

    //异步写日志方法
    void *async_write_log()
    {
        std::string single_log;
        
        //从阻塞队列取出日志string,存入string_log,再写入文件
        while(m_log_queue->pop(single_log))
        {
            m_mutex.lock();
            fputs(single_log.c_str(), m_fp);
            m_mutex.unlock();
        }
    }

private:
    //路径名称
    char dir_name[128];
    //log文件名称
    char log_name[128];
    //日志最大行数
    int m_split_lines;
    //日志缓冲区大小
    int m_log_buf_size;
    //日志行数
    long long m_count;
    //由于按天分类,记录当前哪天
    int m_today;
    //打开log文件的指针
    FILE *m_fp;
    //要输出的内容
    char *m_buf;
    //阻塞队列
    block_queue<std::string> *m_log_queue;
    //是否同步标志位
    bool m_is_async;
    //锁
    locker m_mutex;
    //关闭日志
    int m_close_log;
};
//这四个宏定义在其他文件中使用,主要用于不同类型的日志输出
#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}

/* //上边的宏定义这么写可能更简洁
//定义一个基础宏。
#define LOG_BASE(n, format, ...) \
        if(m_close_log == 0) \
        { \
            Log::get_instance()->write_log(n, format, ##__VA_ARGS__); \
            Log::get_instance()->flush();\
        }
//这四个宏定义在其他文件中使用,主要用于不同类型的日志输出
#define LOG_DEBUG(format, ...) LOG_BASE(0, format, ...)
#define LOG_INFO(format, ...) LOG_BASE(1, format, ...)
#define LOG_WARN(format, ...) LOG_BASE(2, format, ...)
#define LOG_ERROR(format, ...) LOG_BASE(3, format, ...)
*/
#endif

4.日志类实现

相关系统函数

  • strrchr(const char *s, int c);
    从后往前找(从右往左)在s中搜索c,找到就返回c在s中第一次出现的位置,找不到返回NULL。

  • int snprintf(char *str, size_t size, const char *format, ...);
    将可变个参数(…)按照format格式化成字符串,然后将其复制到str中。
    (1) 如果格式化后的字符串长度 < size,则将此字符串全部复制到str中,并给其后添加一个字符串结束符(‘\0’);
    (2) 如果格式化后的字符串长度 >= size,则只将其中的(size-1)个字符复制到str中,并给其后添加一个字符串结束符(‘\0’)

    返回值为欲写入的字符串长度 。
    例如:

    i = snprintf(a, 13, "%012d", 12345);  // 第 1 种情况
    printf("i = %lu, a = %s\n", i, a);    // 输出:i = 12, a = 000000012345
    

  • char* strcpy(char* strDestination, const char* strSource);
    strcpy() 函数用于对字符串进行复制(拷贝)。
    • strDestination:目的字符串。
    • strSource:源字符串

  • char *strncpy(char *dest, const char *src, size_t n)
    • dest – 指向用于存储复制内容的目标字符串。
    • src – 要复制的字符串。
    • n – 要从源字符串中复制的字符数。

  • va_list、va_start、vsprintf
    • #include <stdarg.h>
    • va_list 是一个字符指针,在代码中可以理解为指向当前参数的一个指针
    • void va_start(va_list ap, last_arg); 用于初始化ap,将ap指针指向参数列表中的第一个参数
    • void va_end ( va_list ap ); //回收ap指针
    • int vsprintf(char *string, char *format, va_list ap);//将ap(通常是字符串) 按format格式写入字符串string中
    • 例如:
    void test(const char *format, ...)
    {
    	va_list valst;
            va_start(valst, format);
    	char m_s[128] = {0};
    	int m = vsnprintf(m_s, 127, format, valst);
    	printf("%s\n", m_s);
            va_end(valst);
    }
    int main()
    {
    	int a = 10, b = 20;
    	char format[] = "%d_%d";
    	test(format, 10, 10);
    	return 0;
    }```
    
    输出为: 10_10

log.cpp

#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdarg.h>
#include "log.h"
#include <pthread.h>

Log::Log()
{
    m_count = 0;//行数0
    m_is_async = false;//默认不异步
}

Log::~Log()
{
    if (m_fp != NULL)
    {
        //关闭打开的文件
        fclose(m_fp);
    }
}
//Log::init执行的是初始化的功能。
//异步需要设置阻塞队列长度,同步不用
bool Log::init(const char *file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size)
{
    //如果设置了max_queue_size,则认为设置是异步
    if (max_queue_size >= 1)
    {
        m_is_async = true;
        m_log_queue = new block_queue<std::string>(max_queue_size);
        pthread_t tid;
        //flush_log_thread为回调函数,这里表示创建线程异步写日志
        pthread_create(&tid, NULL, flush_log_thread, NULL);
    }
    m_close_log = close_log;
    m_log_buf_size = log_buf_size;
    m_buf = new char[m_log_buf_size];
    memset(m_buf, '\0', m_log_buf_size);
    m_split_lines = split_lines;

    //获取当前时间
    time_t t = time(NULL);
    struct tm *sys_tm = localtime(&t);
    struct tm my_tm = *sys_tm;

    //strrchr(const char *s, int c);//从后往前找(从右往左)在s中搜索c,找到就返回c在s中第一次出现的位置,找不到返回NULL
    //相当于只提取出文件名,把路径忽略掉
    const char *p = strrchr(filename, '/')char log_full_name[256] = {0};

    if(p == NULL)
    {
        //int snprintf(char *str, size_t size, const char *format, ...);
        //输出来形式就是“年_月_日_文件名”
        snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);
    }
    else
    {
        //把log的名称提取出来
        strcpy(log_name, p + 1);
        //把路径提取出来
        strncpy(dir_name, file_name, p - file_name + 1);
        //创建完整的文件名
        snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);
    }

    m_today = my_tm.tm_mday;

    m_fp = fopen(log_full_name, "a");
    if (m_fp == NULL)
    {
        //打开文件失败
        return false;
    }

    return true;
}

//Log::write_log执行的是把对于的log写入文件的功能,这个功能通过定义在log.h文件中的LOG_XXX等宏调用。
void Log::write_log(int level, const char *format, ...)
{
    struct timecal now = {0, 0};
    //获取时间
    gettimeofday(&now, NULL)
    time_t t = now.tv_sec;
    struct tm *sys_tm = localtime(&t);
    struct tm my_tm = *sys_tm;
    char s[16] = {0};

    switch(level)
    {
        case 0:
        {
            strcpy(s, "[debug]:");
            break;
        }
        case 1:
        {
            strcpy(s, "[info]:");
            break;
        }
        case 2:
        {
            strcpy(s, "[warn]:");
            break;
        }
        case 3:
        {
            strcpy(s, "[erro]:");
            break;
        }
        default:
        {
            strcpy(s, "[info]:");
            break;
        }
    }

    //写入一个log

    //新建一个log文件
    m_mutex.lock();
    ++m_count;
    //按天数存放log文件,如果实例中的天数与当前天数不一致,或者行数超过能最大行数,则新建立文件
    if(m_today != my_tm.tm_dat || m_count % m_split_lines == 0)
    {
        char new_log[256] = {0};
        //刷新文件流
        fflush(m_fp);
        fclose(m_fp);
        char tail[16] = {0};
        snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_day);

        //新的一天
        if(m_today != my_tm.tm_mday)
        {
            snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);
            m_today = my_tm.tm_mday;
            m_count = 0;
        }
        else//超行
        {
            snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);
        }
        //打开新的文件
        m_fp = fopen(new_log, "a");
    }
    m_mutex.unlock();

    //用于可变参数列表的一组函数和定义
    va_list valst;
    va_start(valst, format);
    std::string log_str;

    m_mutex.lock();
    int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",
                     my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
                     my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);
    //按照format格式把valst写入m_buf + n指向的字符串
    int m = vsnprintf(m_buf + n, m_log_buf_size - 1, format, valst);
    m_buf[n + m] = '\n';
    m_buf[n + m + 1] = '\0';
    log_str = m_buf;

    m_mutex.unlock();
    
    //判断是同步输出还是异步输出,异步输出要压入队列,如果队列满了,就采用同步直接输出
    if(m_is_async && !m_log_queue->full())
    {
        m_log_queue->push(log_str);
    }
    else
    {
        m_mutex.lock();
        fputs(log_str.c_str(), m_fp);
        m_mutex.unlock();
    }
    va_end(valst);
}

void Log::flush(void)
{
    m_mutex.lock();
    //强制刷新写入流缓冲区
    fflush(m_fp);
    m_mutex.unlock();
}
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐