一、介绍
随着业务的不断扩张,移动端的日志也会不断增多。
当用户达到一定量级之后,某些用户的Bug却无法通过之前的跟踪定位方式来进行解决。
这时候我们需要一个移动端的日志收集工具。
Logan是美团移动端底层的基础日志库,可以在本地存储各种类型的日志,在需要时可以对数据进行回捞和分析。
Logan地址:https://github.com/Meituan-Dianping/Logan
二、原理
Logan通过Native方式来实现日志底层的核心逻辑,也就是C编写底层库。这样做不光能解决Java GC问题,还做到了一份代码运行在Android和iOS两个平台上。后续还开源了web和flutter平台。
收集日志时,在C层实现流式的压缩和加密数据,可以减少CPU峰值,使程序运行更加顺滑。而且先压缩再加密的方式压缩率比较高,整体效率较高。加密方式为AES。
Logan对日志协议数据进行格式化处理,针对大日志的分片处理,引入了MMAP机制解决了日志丢失问题,使用AES进行日志加密确保日志安全性。 为了节约用户手机空间大小,日志文件默认只保留最近7天的日志,过期会自动删除。在设备上Logan将日志保存在应用自己的缓存目录中。(mmap: 是一种内存映射文件的方法,它将一个文件映射到进程的地址空间中,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上)
三、API使用
Logan的jar和so文件很小,jar包 32K,so文件150k(armeabi、armeabi-v7a、arm64-v8a)
初始化: 参数:mmap文件地址、日志文件地址、日志文件过期天数、日志文件最大值、SD卡最小值(小于则不写入日志)、AES加密key和随机值。
LoganConfig config = new LoganConfig.Builder()
.setCachePath(getApplicationContext().getFilesDir().getAbsolutePath())
.setPath(getApplicationContext().getExternalFilesDir(null)
.getAbsolutePath() + File.separator + "logan_v1")
.setEncryptKey16("0123456789012345".getBytes())
.setEncryptIV16("0123456789012345".getBytes())
.setMaxFile(10)
.setDay(3)
.build();
Logan.init(config);
日志写入:
Logan.w("test logan", 2);
第一个参数写入的日志字符串,第二个参数是日志类型,可以自定义,大于1即可
日志强行保存到文件:
Logan.f();
日志上传:
String url = "https://xxxxx";
Logan.s(url, loganTodaysDate(), "testAppId", "testUnionid", "testdDviceId", "testBuildVersion", "testAppVersion", new SendLogCallback() {
@Override
public void onLogSendCompleted(int statusCode, byte[] data) {
final String resultData = data != null ? new String(data) : "";
Log.d(TAG, "upload result, httpCode: " + statusCode + ", details: "
+ resultData);
}
});
自定义上传方式:RealSendLogRunnable
Logan.s(date, RealSendLogRunnable);
获取所有日志文件信息:
//map键值对:key: 日志文件日期 value: 日志文件大小
Map<String, Long> map = Logan.getAllFilesInfo();
监听Logan操作状态:
Logan.setOnLoganProtocolStatus(new OnLoganProtocolStatus() {
@Override
public void loganProtocolStatus(String cmd, int code) {
}
});
监听Logan的状态:包括初始化,打开文件和写入文件的状态
四、源码解析
java层:
初始化时,会启动一个线程LoganThread,传入设置的参数。所以可以在主线程和非主线程调用写日志的方法。
这个LoganThread启动后,就会一直不停的运行,采用多线程的wait/notify的机制,操作的类型有写入和发送等。
上传日志文件的默认方式:采用 HttpsURLConnection 的连接,post方法; 也可以 自定义上传文件。上传的文件是采用日期为单位,每天一个日志文件,上传时传入日期参数数组。
上传日志文件前,会主动调用flush。
写日志时,首先会删除过期的文件,然后判断SD卡可用空间是否大于50M,否则不写入。
当天写入的日志文件如果达到最大值,就不会再写入。
写入和发送日志,都使用了 并发队列 ConcurrentLinkedQueue(非阻塞式)写入队列,并发最大数为500、并发队列,没有限制最大数(Integer.MAX_VALUE)
native层:
native层 压缩的块大小为5k,超过5K就会主动写入(flush)日志文件中。主动调用flush,也会直接写入文件。所以,解密的时候,不能一次解密,需要循环的读取文件流,然后再去解密和解压缩。
native层 加密的方式是AES,加密需要填充,然后在加密和压缩,不调用flush,就不会对尾部进行填充和压缩。
这样就会导致一个问题:当APP写入日志,还未调用flush时,然后异常退出或者被kill进程,就会导致这部分日志解析失败。
这个问题特地咨询了美图的Logan技术人员,暂时没法比解决,只能想办法在APP退出前,调用flush。或者优化native层的源代码。
日志协议格式:
{"c":"clogan header","f":1,"l":1538034504703,"n":"clogan","i":1,"m":true}
key | type | description |
c | string | 日志内容 |
f | int | 日志类型 |
l | long | 时间戳 |
n | string | 线程名称 |
i | int | 上传的线程id |
m | boolean | 是不是主线程 |
五、性能
经过本地真机测试:
批量写10万条,写完立即flush,内存监控图:
Vivo X20 系统8.1:
小米9 系统9.0:
CPU监控图:
批量写入期间,界面不卡顿,还可以操作。
六、结论
1、Logan的日志收集功能,总体还是比较优秀:
a. API简单:初始化,写日志,写入文件等
b. 日志格式简洁:json格式,可以定义日志类型
c. 性能强大:大批量写入日志,消耗的内存不多,IO操作很少,也不影响手机正常操作
d. 跨平台:android、ios、web
e. 开源:可以修改源码
2、美中不足的就是app crash前的日志无法解析,需要:
a. 在native层优化(加上gzip的end标记) -- 优先这种方式
b. 优化解析日志文件的方法。