cover_image

快看移动端日志库

客户端超能立方组 快看技术
2022年01月27日 08:15

线AndroidiOSKKLog

  • ⽇志不全,容易丢失
  • 占⽤空间⼤
  • 写入性能低效
  • 数据不安全
  • Android、iOS平台差异

  • 数据安全:数据不可以被其他任何三⽅窥探,破解
  • 数据完整性:任意时刻上传或者从本地磁盘拉取出的⽇志⽂件都可以完整的被解析处理
  • 空间占⽤少:尽量较少冗余数据占⽤的空间,在相同⼤⼩⽇志⽂件下,尽可能多的存储⽇志内容
  • 获取便利性:除了App⽤户反馈上报,⽤户也可以很容易的找到⽇志⽂件发送给开发⼈员,避免应⽤遇到⽆法启动问题时,⽆法上传⽇志的问题
  • ⾼性能:数据经过压缩、加密、存储快
  • 平台差异:Android、iOS使用一套代码
  • ⽇志中可能会涉及到⼀些⽤户隐私数据,例如imei等,需要确保这些隐私数据不被别的应⽤获取就必须满⾜以下其中⾄少⼀点
  • ⽇志⽂件放在应⽤私有⽬录,可以确保别的应⽤⽆法读取⽇志⽂件,但是⽤户也⽆法获取这个⽂件,⽆法⼿动发送⽇志⽂件
  • ⽇志⽂件放在公有⽬录,那就要求对⽇志⽂件压缩加密,这样会严重降低压缩率基于以上两种⽅式的缺点
    我们取⼀个折中⽅案:
    • ⽇志放在公有⽬录(仅Android)
    • 对写⼊临时⽂件的每条⽇志内容做简单加密,当临时⽂件写满时,还原⽇志内容,并将这个临时⽂件压缩加密存⼊最终的⽇志⽂件中

图片
  • KKLogger 对外暴露接⼝,只有基本的⽅法,和系统Log⽅法⼀致,外部使⽤⽅式为KKLogger.with("biz").i("tag","content")
  • LoggerManager为管理类,管理writer⽅式,上传、以及是否需要Hook Native的⽇志,使⽤时可以根据是debug还是release选择不同的writer
  • Writer/Upload/Hooker等,这些是负责专⻔的写操作和上传等逻辑操作
  • 日志存储是c写的代码,真正负责日志的写入工作
:
图片

  •  
    • mmap
  • meta
    • version: 
    • string pool: tag使string pool,tag使1232768
    • time_ofset: 8使4time_ofset
    • ()

  • ⽇志级别: Error, Warning, Info, Debug
  • 业务类型: ⽤于区分不同的业务线或⼤的业务模块(如图⽚,⽹络)
  • tag: ⽇志tag
  • content: ⽇志内容
  • pid: 当前进程号
  • tid: 当前线程号
  • timestamp: ⽇志发⽣的时间戳
{
"level": "D",
"tag": "Test",
"biz": "Image",
"content": "load staic image: http://xxxx/index.png",
"pid": 123,
"tid": 456,
"time": 123123
}
level, tag, biz, contentpid, tid, timestamp

图片
  • level: ⼀个字节
  • pid/tid: 默认pid, tid分别占两个字节, 考虑到相同pid/tid连续打⽇志的概率⾮常⾼,如果两条相邻⽇志的pid/tid相同,则使⽤0表示新⽇志的pid/tid, 共占⽤两个字节。
  • time: 相对于time_ofset的时间偏移,4个字节
  • biz: 业务类型在string pool在string - pool中的索引,如果索引值<=127, 则使⽤1个字节,否则使⽤两个字节表示,且最⾼位置1,表示是两个字节的⻓度
  • tag: log tag在string pool中的索引,编码规则同biz
  • content:日志内容
  • len: 内容⻓度,⻓度<=127时1个字节,否则两个字节,最⻓不超过32766字节, 超过则分成两条⽇志这⾥不清楚字节的分配占⽤,可以了解⼀下以下知识:Int8,Int16,Int32,Int64,后⾯的数字就代表这个数据类型占据的空间。
  • Int8 等于Byte, 占1个字节. -128~127
  • Int16 意思是16位整数(16bit integer),相当于short 占2个字节 -32768 ~ 32767
  • Int32 意思是32位整数(32bit integer), 相当于 int 占4个字节 -2147483648 ~ 2147483647
  • Int64 意思是64位整数(64bit interger), 相当于 long 占8个字节 -9223372036854775808 ~9223372036854775807
  • WORD 等于 unsigned short 0 ~ 65535

线

线线线.

图片
:
  • ⾸先获取到原始信息,然后⾃动补全tid、pid以及time
  • 写⼊bufer时会序列化计算每个位置的字节数,从meta中找到元信息,也就是len、version、time_ofset、string_pool等数据
  • 接着将补全后的数据简单加密后存储到临时⽂件,也就是mmap的⽂件
  • 如果此时临时⽂件不⾜以写⼊这条数据,会将临时⽂件还原,然后压缩、aes加密,写⼊log_fle⽂件中

(Android)

Android使fe_lock, 
:
  • 获取⽂件锁涉及内核调⽤,频繁的操作可能会有⼀定的性能消耗, 测试10w次lock+unlock, ⼤约耗时100-200ms, 是可接受的范围
  • 由于每个进程中的⽇志信息是在独⽴线程中异步写⼊的,因此并不能绝对保证多进程的⽇志时序正确。

使gzip(使zlib)

使aes

#ifndef KKLOG_LOG_CORE_H
#define KKLOG_LOG_CORE_H

#include <map>
/**
 * 
 * @param aesKey
 * @param aesIV
 * @param tempFileSize
 * @param MAX_LOG_FILE_SIZE
 * @param MAX_LOG_FILE_SIZE
 * @return
 */

int init(const char* aesKey, const char* aesIV, uint32_t tempFileSize, uint32_t MAX_LOG_FILE_SIZE, const char* logDir);

/**
 * 
 * @param level
 * @param bizType
 * @param tag
 * @param tid
 * @param timestamp 
 * @return 0
 */

int writeLog(int level, const char* bizType, const char* tag, const char* content, uint32_t tid, long timestamp);

/**
 * 
 * @return 0
 */

int flush();

/**
 * Log File
 * @return 
 */

const char* assembleLogFile();

/**
 * Log File, 
 * @param logDir
 * @return 
 */

bool assembleLogFile(const char* targetPath);

bool clearLog(const char* logDir);
#endif //KKLOG_LOG_CORE_H
使writeLog,fush,assembleLogFile,

KKLogcpu使103ms
  • 常规:

图片
  • kklog:

图片
GCkklog下CPU使
CPU
  • 常规
图片
  • kklog:
图片
cpu使12%kklogcpu使2%~3%

  • log_fle.lock: ⽤于多进程间⽇志同步锁,不需要关注
  • log_fle.mmap: 临时⽇志⽂件,⽤于提升写⽇志的性能,以及⽇志压缩率
  • log_fle.meta: ⽇志相关的⼀些meta信息,如字符串表,时间偏移等
  • log_fle_1: ⽇志⽂件1
  • log_fle_2: ⽇志⽂件2
  • log_fle.log: 聚合了log_fle_1, log_fle_2, log_fle.meta,log_fle.mmap的完整⽇志,⽤于上传到服务端,研发可以从服务端下载完整⽇志⽂件
    图片
    image

  • ⾸先读取meta信息,将version、time_ofset、string_pool读取出来
  • 然后将剩余所有数据读取出来接着解密、解压
  • 最后根据写⼊时的顺序按照字节数读取,顺序分别为1字节的level、2字节的pid、2字节的tid、4字节的time等等
  • 这样每次读取⼀条log信息,然后就是遍历所有读取所有内容
private static Tuple<LogItem, Boolean> nextLogInner(InputStream logStream, MetaData metaData) throws IOException {
    if (logStream == null || logStream.available() <= 0) {
        return null;
    }

    LogItem item = new LogItem();
    item.level = logStream.read();
    int pid = IOUtils.readShort(logStream);
    if (pid == 0) {
        item.pid = LogItem.lastPid;
        item.tid = LogItem.lastTid;
    } else {
        item.pid = pid;
        item.tid = IOUtils.readShort(logStream);
        LogItem.lastPid = item.pid;
        LogItem.lastTid = item.tid;
    }
    item.time = metaData.getTimeOffSet() + IOUtils.readLong32(logStream);
    int bizIndex = IOUtils.readVarientShort(logStream);
    int tagIndex = IOUtils.readVarientShort(logStream);
    item.biz = metaData.getStringTable().get(bizIndex);
    item.tag = metaData.getStringTable().get(tagIndex);
    Tuple<String, Boolean>
 content = IOUtils.readString(logStream);
    item.content = content.getFirst();
    return new Tuple<>(item, content.getSecond());
}

  • 先看是否有完整⽇志⽂件,如果找到了直接解析⽇志⽂件,和上⾯⼀样解析
  • 如果没有完整⽇志⽂件,则分别解析不同⽂件meta、mmap以及log_fle_1、log_fle_2等
  • log_fle_1、log_fle_2和上⾯步骤1是⼀样的解法
  • mmap⽂件解析不同之处在于不需要解压,不过因为mmap⽂件是做了简单加密的,所以查看时需要解密还原回来

使

kklog [-b biz] [-t tag] [-l Level] [-p pid] [-tid tid] [-o outputPath] logPath
   -b biz: filter log by biz
   -t tag: filter log by tag
   -l Level: filter log by level, options: D/I/W/E
   -p pid: filter log by pid
   -tid tid: filter log by tid
   -o outputPath: output parsed result to outputPath
logPathlog_fe_1, log_fe_2, log_fe.meta, log_fe.mmap.biz/tagkklog -t Activity.*, Activitytagkklog -o outputPath logPath 
图片

id
图片
继续滑动看下一个
快看技术
向上滑动看下一个