会了JNI,我们来看看Logan库

前言

随着业务的发展,日志是必不可少的环节之一,越来越多的日志库如雨后春笋般崭露头角。这里就有很多日志库从快、准、稳这三个维度的追求,将触手从java层蔓延到了Linux层。这里就市面上运用了mmap技术的开源日志库(腾讯Mars中的一环XLog\美团点评的logan)进行一些个人总结,如有不对的地方,还请指正。
抱着从简到杂,从易到复的原则,这里首先简述美团点评的Logan库,希望对广大希望快速拥有日志能力的开发者们早日脱离日志坑。

Logan在初始化如下:
Logan初始化.png
接着以炒菜的形式来表述吧:
1、切菜、备油(这里我们可以看到必须的几要素如下)

(1)、mmap文件的缓存路径;(2)、每个文件的最大文件大小,默认10M;(3)、保存日志的缓存天数,默认是7天;(4)、ASE加密的key;(5)、ASE加密的IV;(6)、需要SD卡最小的额外存储,默认是50M。
1LoganConfig.png
1-1初始化参数.png

2、准备工具
LoganControlCenter使用一个基于链接节点的无界线程安全队列的ConcurrentLinkedQueue来处理消费事件。同样我们还是准备工具:
重点如下:(1)、最大队列数,默认500个;(2)、自定义线程LogainThread;(3)、日期格式
队列处理中心参数.png
这个队列处理中心担负了两个主要责任:一个是分发处理日志事件、还有一个就是负责把之前参数全部缓存起来在合适的时机传给JNI层。
这些都有了,我们思考,如果我们要写一个日志库,配置、参数都有了是不是就差写文件了,就好比菜和工具都齐了,我们现在是不要要有菜谱做细节啊,那好,我们接下来看美团点评的同学如何做菜下酒。

3、JNI层交互方法汇总

前言说到这是一个基于Linux层面的一种内存映射技术,这技术会在后面统一讲解,这里暂时先梳理Logan库的思路和逻辑。我已经迫不及待的在找菜谱了,直接搜关键字Native,因为上一章节提到JNI交互的语法格式需要一个native字段,这里我们直接抓住重点,看到clogan_init、clogan_open、clogan_debug、clogan_write、clogan_flush
CLoganProtocol中维护的native方法.png
那既然我们找到菜谱了,我们看看究竟每一步是干嘛的,第一步clogan_init:简述就是初始化缓存路径、目录路径,秘钥,如果mmap文件缓存目录初始化失败了,这里会延用内存作为缓存的姿势继续保存文件。因为逻辑较多,删除了部分判断逻辑用省略号代替。
/**
 * Logan初始化
 * @param cachedirs 缓存路径
 * @param pathdirs  目录路径
 * @param max_file  日志文件最大值
 */
int
clogan_init(const char *cache_dirs, const char *path_dirs, int max_file, const char *encrypt_key16,
            const char *encrypt_iv16) {
    .........
    if (max_file > 0) {
        max_file_len = max_file;
    } else {
        max_file_len = LOGAN_LOGFILE_MAXLENGTH;
    }
    ...........
    //初始化秘钥的KEY和IV
    aes_init_key_iv(encrypt_key16, encrypt_iv16);
    makedir_clogan(cache_path); //创建保存mmap文件的目录
    strcat(cache_path, LOGAN_CACHE_FILE);
    ..........
    char *dirs = (char *) malloc(total); //缓存文件目录
    if (NULL != dirs) {
        _dir_path = dirs; //日志写入的文件目录
    } else {
        ......
    }
   ......
    if (isAddDivede)
        strcat(dirs, LOGAN_DIVIDE_SYMBOL);
    makedir_clogan(_dir_path); //创建缓存目录,如果初始化失败,注意释放_dir_path

    int flag = LOGAN_MMAP_FAIL;
    if (NULL == _logan_buffer) {
        if (NULL == _cache_buffer_buffer) {
            flag = open_mmap_file_clogan(cache_path, &_logan_buffer, &_cache_buffer_buffer);
        } else {
            flag = LOGAN_MMAP_MEMORY;
        }
    } else {
        flag = LOGAN_MMAP_MMAP;
    }
    .............
    if (is_init_ok) {
        if (NULL == logan_model) {
            logan_model = malloc(sizeof(cLogan_model));
            if (NULL != logan_model) { //堆非空判断 , 如果为null , 就失败
                memset(logan_model, 0, sizeof(cLogan_model));
            } else {
                is_init_ok = 0;
                printf_clogan("clogan_init > malloc memory fail for logan_model\n");
                back = CLOGAN_INIT_FAIL_NOMALLOC;
                return back;
            }
        }
        if (flag == LOGAN_MMAP_MMAP) //MMAP的缓存模式,从缓存的MMAP中读取数据
            read_mmap_data_clogan(_dir_path);
        printf_clogan("clogan_init > logan init success\n");
    } else {
        printf_clogan("clogan_open > logan init fail\n");
        // 初始化失败,删除所有路径
       .........
    return back;
}

接下来是clogan_open

FILE *file_temp = fopen(logan_model->file_path, "ab+");
long longBytes = ftell(file_temp);
logan_model->file_len = longBytes;
// 在之前init的时候会判断mmap缓存目录是否有缓存,如果有,下面在open的时候会把缓存数据写入防止丢失,这里主要告诉我们:1、如果之前有剩下的菜没有烧的,这次也放进去,就是有缓存我们也写到日志中;2、初始化我们的logan_model结构体(后面会贴出解释,存放文件数据的精髓类,里面包含了文件大小,压缩等信息);3、对于是否是mmap,这里还做了目录创建等处理
int clogan_open(const char *pathname) {
    .........
    if (NULL != logan_model) { //回写到日志中
        if (logan_model->total_len > LOGAN_WRITEPROTOCOL_HEAER_LENGTH) {
            clogan_flush();
        }
      ..........
    } else {
        logan_model = malloc(sizeof(cLogan_model));
        // 初始化Logan_model结构体
        ............
    }
    char *temp = NULL;

    size_t file_path_len = strlen(_dir_path) + strlen(pathname) + 1;
    char *temp_file = malloc(file_path_len); // 日志文件路径
    if (NULL != temp_file) {
        memset(temp_file, 0, file_path_len);
        temp = temp_file;
        memcpy(temp, _dir_path, strlen(_dir_path));
        temp += strlen(_dir_path);
        memcpy(temp, pathname, strlen(pathname)); //创建文件路径
        logan_model->file_path = temp_file;

        if (!init_file_clogan(logan_model)) {  //初始化文件IO和文件大小
            is_open_ok = 0;
            back = CLOGAN_OPEN_FAIL_IO;
            return back;
        }

        if (init_zlib_clogan(logan_model) != Z_OK) { //初始化zlib压缩
            is_open_ok = 0;
            back = CLOGAN_OPEN_FAIL_ZLIB;
            return back;
        }

        logan_model->buffer_point = _logan_buffer;

        if (buffer_type == LOGAN_MMAP_MMAP) {  //如果是MMAP,缓存文件目录和文件名称
            cJSON *root = NULL;
            Json_map_logan *map = NULL;
            root = cJSON_CreateObject();
            map = create_json_map_logan();
            char *back_data = NULL;
            if (NULL != root) {
                if (NULL != map) {
                    add_item_number_clogan(map, LOGAN_VERSION_KEY, CLOGAN_VERSION_NUMBER);
                    add_item_string_clogan(map, LOGAN_PATH_KEY, pathname);
                    inflate_json_by_map_clogan(root, map);
                    back_data = cJSON_PrintUnformatted(root);
                }
                cJSON_Delete(root);
                if (NULL != back_data) {
                    add_mmap_header_clogan(back_data, logan_model);
                    free(back_data);
                } else {
                    logan_model->total_point = _logan_buffer;
                    logan_model->total_len = 0;
                }
            } else {
                logan_model->total_point = _logan_buffer;
                logan_model->total_len = 0;
            }

            logan_model->last_point = logan_model->total_point + LOGAN_MMAP_TOTALLEN;

            if (NULL != map) {
                delete_json_map_clogan(map);
            }
        } else {
            logan_model->total_point = _logan_buffer;
            logan_model->total_len = 0;
            logan_model->last_point = logan_model->total_point + LOGAN_MMAP_TOTALLEN;
        }
        restore_last_position_clogan(logan_model);
        init_encrypt_key_clogan(logan_model);
        logan_model->is_ok = 1;
        is_open_ok = 1;
    } else {
        is_open_ok = 0;
        back = CLOGAN_OPEN_FAIL_MALLOC;
        printf_clogan("clogan_open > malloc memory fail\n");
    }
    ..........
    return back;
}
typedef struct logan_model_struct {
    int total_len; //数据长度
    char *file_path; //文件路径
    int is_malloc_zlib;
    z_stream *strm;
    int zlib_type; //压缩类型
    char remain_data[16]; //剩余空间
    int remain_data_len; //剩余空间长度

    int is_ready_gzip; //是否可以gzip

    int file_stream_type; //文件流类型
    FILE *file; //文件流

    long file_len; //文件大小

    unsigned char *buffer_point; //缓存的指针 (不变)
    unsigned char *last_point; //最后写入位置的指针
    unsigned char *total_point; //总数的指针 (可能变) , 给c看,低字节
    unsigned char *content_lent_point;//协议内容长度指针 , 给java看,高字节
    int content_len; //内容的大小

    unsigned char aes_iv[16]; //aes_iv
    int is_ok;

} cLogan_model;

接下来就是精髓了,就是菜谱告诉我们,第一步我们要放油初始化文件目录参数,第二步告诉我们调用open方法开火,第三步就是放菜啦clogan_write,这里都是写入缓存的,具体随我们来看美团点评的同学如何来玩。

/**
 @brief 写入数据 按照顺序和类型传值(强调、强调、强调)
 @param flag 日志类型 (int)
 @param log 日志内容 (char*)
 @param local_time 日志发生的本地时间,形如1502100065601 (long long)
 @param thread_name 线程名称 (char*)
 @param thread_id 线程id (long long) 为了兼容JAVA
 @param ismain 是否为主线程,0为是主线程,1位非主线程 (int)
 */
int
clogan_write(int flag, char *log, long long local_time, char *thread_name, long long thread_id,
             int is_main) {
    int back = CLOGAN_WRITE_FAIL_HEADER;
    if (!is_init_ok || NULL == logan_model || !is_open_ok) {
        back = CLOGAN_WRITE_FAIL_HEADER;
        return back;
    }
    //如果文件大小超过10M
    if (is_file_exist_clogan(logan_model->file_path)) {
        if (logan_model->file_len > max_file_len) {
            printf_clogan("clogan_write > beyond max file , can't write log\n");
            back = CLOAGN_WRITE_FAIL_MAXFILE;
            return back;
        }
    } else {
        if (logan_model->file_stream_type == LOGAN_FILE_OPEN) {
            fclose(logan_model->file);
            logan_model->file_stream_type = LOGAN_FILE_CLOSE;
        }
        if (NULL != _dir_path) {
            if (!is_file_exist_clogan(_dir_path)) {
                makedir_clogan(_dir_path);
            }
            init_file_clogan(logan_model);
            printf_clogan("clogan_write > create log file , restore open file stream \n");
        }
    }

    //判断MMAP文件是否存在,如果被删除,用内存缓存
    if (buffer_type == LOGAN_MMAP_MMAP && !is_file_exist_clogan(_mmap_file_path)) {
        if (NULL != _cache_buffer_buffer) {
            buffer_type = LOGAN_MMAP_MEMORY;
            buffer_length = LOGAN_MEMORY_LENGTH;

            printf_clogan("clogan_write > change to memory buffer");

            _logan_buffer = _cache_buffer_buffer;
            logan_model->total_point = _logan_buffer;
            logan_model->total_len = 0;
            logan_model->content_len = 0;
            logan_model->remain_data_len = 0;

            if (logan_model->zlib_type == LOGAN_ZLIB_INIT) {
                clogan_zlib_delete_stream(logan_model); //关闭已开的流
            }

            logan_model->last_point = logan_model->total_point + LOGAN_MMAP_TOTALLEN;
            restore_last_position_clogan(logan_model);
            init_zlib_clogan(logan_model);
            init_encrypt_key_clogan(logan_model);
            logan_model->is_ok = 1;
        } else {
            buffer_type = LOGAN_MMAP_FAIL;
            is_init_ok = 0;
            is_open_ok = 0;
            _logan_buffer = NULL;
        }
    }

    Construct_Data_cLogan *data = construct_json_data_clogan(log, flag, local_time, thread_name,
                                                             thread_id, is_main);
    if (NULL != data) {
        clogan_write_section(data->data, data->data_len);
        construct_data_delete_clogan(data);
        back = CLOGAN_WRITE_SUCCESS;
    } else {
        back = CLOGAN_WRITE_FAIL_MALLOC;
    }
    return back;
}

上面这块东西代码块不大我就不省略了,别看这里代码块不多,但却是重中之重,首先当头便是文件写入策略,1、如果当日只会有一个文件记录,且大于10M则拒绝写入,这里笔者认为应该是根据实际情况做了一层妥协,其实很多业务方的数据都会丢进来,那么美团在业务回捞的时候,用户肯定会很懵逼,怎么就反馈了一个页面打不开的问题,我就10M流量没了,这里我们是否可以实现多业务多文件或者多类别的区分,这样在回捞的时候也可以侧重点来进行回捞,也就同时也避免了单文件10M数据拒绝写入的尴尬境地。2、这里的写入在非特殊情况下只是写入缓存和内存,读者们要在这里注意下。3、clogan_write_section关注这个方法,非常重要,这里是在正常情况下,非接入方干预前提下的缓存写入用户真实文件的时机,具体逻辑如下:

void clogan_write2(char *data, int length) {
    if (NULL != logan_model && logan_model->is_ok) {
        clogan_zlib_compress(logan_model, data, length);
        update_length_clogan(logan_model); //有数据操作,要更新数据长度到缓存中
        int is_gzip_end = 0;

        if (!logan_model->file_len ||
            logan_model->content_len >= LOGAN_MAX_GZIP_UTIL) { //是否一个压缩单元结束
            clogan_zlib_end_compress(logan_model);
            is_gzip_end = 1;
            update_length_clogan(logan_model);
        }

        int isWrite = 0;
        if (!logan_model->file_len && is_gzip_end) { //如果是个空文件、第一条日志写入
            isWrite = 1;
            printf_clogan("clogan_write2 > write type empty file \n");
        } else if (buffer_type == LOGAN_MMAP_MEMORY && is_gzip_end) { //直接写入文件
            isWrite = 1;
            printf_clogan("clogan_write2 > write type memory \n");
        } else if (buffer_type == LOGAN_MMAP_MMAP &&
                   logan_model->total_len >=
                   buffer_length / LOGAN_WRITEPROTOCOL_DEVIDE_VALUE) { //如果是MMAP 且 文件长度已经超过三分之一
            isWrite = 1;
            printf_clogan("clogan_write2 > write type MMAP \n");
        }
        if (isWrite) { //写入
            write_flush_clogan();
        } else if (is_gzip_end) { //如果是mmap类型,不回写IO,初始化下一步
            logan_model->content_len = 0;
            logan_model->remain_data_len = 0;
            init_zlib_clogan(logan_model);
            restore_last_position_clogan(logan_model);
            init_encrypt_key_clogan(logan_model);
        }
    }
}

总结一下:1、如果是空文件的头文件,直接写入文件;2、如果是内存缓存且压缩结束了,直接写入文件;3、如果是mmap缓存,且文件长度超过缓存的三分之一,直接写入文件;
这时候差不多菜谱就读的差不多了,那细心的读者就会有一个问题了,说我就喜欢吃8分熟的牛排,我写了30条日志,并不满足上述的3个条件,或者我就要在APP退出之前把所有缓存数据写入文件,我就喜欢这样不写进去我心里不踏实。那这里顺理成章,一个经过通用的SDK库肯定具备完善的API,菜谱肯定是能满足大多数人的需要的,那就是clogan_flush,接下来我们来梳理下:

int clogan_flush(void) {
    int back = CLOGAN_FLUSH_FAIL_INIT;
    if (!is_init_ok || NULL == logan_model) {
        return back;
    }
    write_flush_clogan();
    back = CLOGAN_FLUSH_SUCCESS;
    printf_clogan(" clogan_flush > write flush\n");
    return back;
}
void write_flush_clogan() {
    if (logan_model->zlib_type == LOGAN_ZLIB_ING) {
        clogan_zlib_end_compress(logan_model);
        update_length_clogan(logan_model);
    }
    if (logan_model->total_len > LOGAN_WRITEPROTOCOL_HEAER_LENGTH) {
        unsigned char *point = logan_model->total_point;
        point += LOGAN_MMAP_TOTALLEN;
        write_dest_clogan(point, sizeof(char), logan_model->total_len, logan_model);
        printf_clogan("write_flush_clogan > logan total len : %d \n", logan_model->total_len);
        clear_clogan(logan_model);
    }
}
//文件写入磁盘、更新文件大小
void write_dest_clogan(void *point, size_t size, size_t length, cLogan_model *loganModel) {
    if (!is_file_exist_clogan(loganModel->file_path)) { //如果文件被删除,再创建一个文件
        if (logan_model->file_stream_type == LOGAN_FILE_OPEN) {
            fclose(logan_model->file);
            logan_model->file_stream_type = LOGAN_FILE_CLOSE;
        }
        if (NULL != _dir_path) {
            if (!is_file_exist_clogan(_dir_path)) {
                makedir_clogan(_dir_path);
            }
            init_file_clogan(logan_model);
            printf_clogan("clogan_write > create log file , restore open file stream \n");
        }
    }
    if (CLOGAN_EMPTY_FILE == loganModel->file_len) { //如果是空文件插入一行CLogan的头文件
        insert_header_file_clogan(loganModel);
    }
    fwrite(point, sizeof(char), logan_model->total_len, logan_model->file);//写入到文件中
    fflush(logan_model->file);
    loganModel->file_len += loganModel->total_len; //修改文件大小
}

那这里就是写入磁盘、更新文件数据model.好了,这盘菜出锅了,接下来会为大家简述下微信的Mars XLOG流程,那个比这个更全面些,但是魔改起来需要对C和底层有更高阶的认识。期望大家可以做出比我更美味的菜,如有错误请指出,谢谢食客们的光临。

你可能感兴趣的:(会了JNI,我们来看看Logan库)