安全高效可视化的日志解决方案

背景

一条工单引发的日志需求
我们的业务收到了一个信息泄漏的工单,漏洞是明文打印日志。

我们当时使用的是某网络库自带的日志系统(以下简称 A 系统),会直接将明文写入到了日志文件当中,这样就有信息泄漏的风险。
我们搜集了当前使用 A 系统的痛点:

  1. 明文打印,造成隐私泄漏;
  2. 关键日志丢失;
  3. 无法方便的打印当前的堆栈,线程等信息;
  4. 不支持各种格式化输出。

为了解决这个问题,我们设计并实现了 TinyLog。
TinyLog 能支持日志的加密和压缩,减少日志文件大小并确保隐私不会泄漏;能够支持各种格式化输出,且比较方便的打印出当前的堆栈,线程等信息;我们在设计的时候也考虑到了日志丢失的问题,在 crash 的场景,也能够确保当前的 crash 堆栈能够被记录下来;此外我们还提供了图形化展示日志的接口,能够将日志图形化的展示出来,方便开发同学快速的定位到问题。

TinyLog 的总体设计

作为一个日志组件,安全高效是我们的一个核心需求。安全包括两点,日志不丢失以及隐私不泄漏;高效就是日志组件不能影响应用的性能。我们在设计 TinyLog 的时候也充分考虑到了这两点,以下是 TinyLog 的总体设计图:
客户端加密,服务端解密。服务端提供明文下载以及图形化展示日志的功能。客户端来保证打印日志的高效性,服务端来保证安全性。
在这里插入图片描述

1.TinyLog SDK

如何才能支持多种格式化输出,并能保持良好的性能呢?
以下是 TinyLog SDK 的结构图:
我们将 TinyLog SDK 分为三层,分别是输入层处理层输出层。其中处理层包含了很多日志的配置项,能够满足各种场景的格式化输出;另外,在输出层,我们将加密,压缩以及文件写入的功能下沉到 c++,保证了良好的性能。
在这里插入图片描述

输入层:
输入层为对外提供的接口,主要提供了初始化方法以及日志打印的方法。我们参考了系统打印日志的接口,并在其基础上进行扩充,降低业务方的接入成本。以下为 TinyLog 和系统日志打印的接口对比,以 info 等级为例:
在这里插入图片描述

系统原有的两个日志打印的接口,我们将其扩充为4个,并在其基础上进行了增强。如系统接口 i(String tag,String msg),第二个参数只能是字符串对象,而我们将其增强为 i(String tag,T obj),第二个参数可以是任何对象,我们会匹配对应的格式化方法,将其进行格式化处理。比如第二个参数是 json 对象,我们就会使用 JsonFormatter 将 json 对象进行格式化,若输入的对象在 TinyLog 内部未匹配到任何 Formatter ,则会直接调用该对象的 toString 方法。

处理层:
在这一层,日志会被分发给 Logger,每个 Logger 都包含一个 LogConfig(配置信息)和 Printer(输出), Logger 会根据 LogConfig 将日志封装成为一个日志对象,然后传递给 Printer 输出。LogConfig 的配置信息包括日志等级过滤,是否打印线程,是否打印边框等,以及各种格式化方法。
我们将格式化抽象成了一个 Formatter 接口,并默认实现了各种场景的格式化方法,具体如下表格所示,业务方也可根据需要自己实现 Formatter 接口。

格式化备注
DefaultBorderFormatter边框格式化
DefaultJsonArrayFormatterJson数组格式化
DefaultJsonFormatterJson对象格式化
DefaultStackTraceFormatter堆栈格式化
DefaultThreadFormatter线程格式化
DefaultThrowableFormatter异常信息格式化
DefaultXmlFormatterxml格式化

输出层:
我们将输出抽象成了 Printer 接口,TinyLog 默认实现了 ConsolePrinter 和 FilePrinter,分别将日志输出到控制台和文件当中。若输出到文件,则可以将日志上传到服务端。业务方也可以自定义 Printer,将日志输出到任何地方。

让日志的输出更加友好

TinyLog 能够很轻松的打印当前的线程信息,堆栈信息以及常见的对象,并默认实现了各种格式化方法,包括边框,json,线程,堆栈等。
例如,以下是打印一个 json 对象,分别使用系统 Log 和 TinyLog 的对比:
在这里插入图片描述

防止日志丢失

如何防止日志丢失是一个非常重要的问题,我们最初使用 A 系统的时候,经常有同事抱怨关键信息丢失。因此在设计 TinyLog 的时候,我们也考虑到了这个问题。
(1)使用 mmap
我们分析了 A 系统,发现其日志打印的流程如下图左边所示:
这样的结构就会有日志丢失的风险,因为使用了内存缓存,一旦发生 crash,缓存当中的日志就丢失了。
怎样才能防止日志丢失呢?最好的办法就是打印每条日志的时候立刻写进入文件,可是这样就会频繁的触发 IO,存在性能的问题。经过调研之后,我们使用 mmap 的方式,既能够防止日志丢失,又能保证性能,如下图右边所示。
在这里插入图片描述

为什么使用 mmap 性能又好,还不会造成数据丢失呢?以下是标准 IO 和 mmap 在写场景的对比图,标准 IO 下,当我们需要将写入的数据从用户空间拷贝到内核空间,然后系统会定期的将数据写入磁盘;而使用 mmap,用户空间和内核空间通过映射同一个普通文件实现内存共享,我们在用户空间内写入的数据相当于直接写入到了内核空间,减少了一次内存拷贝,性能得到提升。而且一旦数据写入到了内核空间,此时 app 即使发生了 crash 也不会造成数据的丢失,仍然能够由系统保证写入到磁盘上。
在这里插入图片描述

以下是我做的一个对比实验,把50 byte 的日志分别写入内存,mmap和文件 10w 次,以下是实验结果:

机器写入内存写入mmap写入文件
三星(s20)16ms32ms815ms
iphone 6s4.9ms4.3ms987ms

可以看出,mmap 在性能上接近于内存。

(2)异常捕获
Android 版本 sdk 我们在 java 层做了全局的异常捕获,当 TinyLog 监测到异常发生时,会立刻将当前的异常信息堆栈写入到日志当中,当日志写完之后才释放异常。以此确保当 crash 发生时,能够将 crash 的堆栈信息保留下来。
ios 版本 sdk 需要依赖 bugly 或其他崩溃采集组件,在其 crash 回调函数中调用 TinyLog 接口写入 crash 堆栈。

安全兼顾性能

加密算法通常分为对称加密和非对称加密,以下为各自的特点:

分类特点速度
对称加密加密和解密的密钥是一样的
非对称加密公钥加密,私钥解密

对于日志 sdk 这个场景,若选用对称加密,则必须要将密钥放到 sdk 当中,才能够完成日志文件的加密,这样会有密钥泄漏的风险;
若选用非对称加密,公钥存入 sdk,私钥存在服务端,能够保证安全,但是性能不太好。
因此我们综合了一下,使用了混合加密的方法,既能保证安全又能兼顾性能,具体的流程如下图所示:
在这里插入图片描述

  1. 客户端和服务端先要约定一套公钥和私钥;
  2. 当客户端创建日志块时,随机生成一串字符串 key,此时使用公钥加密随机生成的 key 得到 key’,将 key’ 记录到日志文件当中,然后 key 当作对称加密的密钥来加密日志;
  3. 服务端在解密时,需要先将 key’ 从日志文件当中读出,然后使用私钥解密出 key,再使用对称加密的方式解密日志得到明文。

虽然非对称加密方式比较慢,但在混合加密的场景只有在创建日志块的时候才会使用一次,而真正加密日志的方法使用的是性能较好的对称加密,每个日志块的密钥均为随机生成,而真正记录在日志文件当中的密钥为加密过后的密文。因此在性能上接近对称加密,而安全性又接近于非对称加密。
以下为加密 1w 条 100 byte 日志的对比实验,使用的是三星 s20:

分类时间
非对称加密1070ms
对称加密33ms
混合加密50ms
图形化显示日志接口

如何让日志更加清晰和直观的展示?
目前我们团队的开发同学使用 fancylog 的插件来查看日志,fancylog 是一个能够可视化显示日志的插件,其原理是根据配置好的正则表达式规则,将给定的日志文件图形化的显示出来,能够极大的提高查看日志的效率。

既然是通过正则表达式来匹配,那我们何不将一些通用的方法抽象出来形成接口,然后为这些接口提供对应的正则表达式,这样只要使用这个接口打印出来的日志,就能够被 fancylog 解析出来,而无需自己配置了。
以下为 TinyLog 当中提供的图形化显示日志的接口:

类别接口备注
场景loadScene(String sceneName)加载场景
loadSceneSuccess(String sceneName)加载成功
loadSceneFailed(String sceneName)加载失败
操作switchFront(String msg)切前台
switchBack(String msg)切后台
click(String msg)点击事件
异常exception(String msg,Exception exception)出现异常
crash(String msg,Exception exception)crash
过程processStart(String msg)开始
processing(String msg)进行中
processEnd(String msg)结束
其他event(String event)自定义event

当我们的日志上传到日志网站之后就能够自动的图形化展示了,如下图所示
在这里插入图片描述

非侵入式的打印日志

有同事提出,目前 TinyLog 所提供的图形化展示接口,是一种侵入式的结构化日志实现方式,业务方需要在代码中显示的调用 fancylog,才能够实现日志的结构化展示。既然这样我们何不使用注解的方式,实现无侵入式的打印日志呢。
因此,我们设计并实现了 FancyLogger 组件,我们自定义了一些日志的注解,并针对被注解标记的方法增加切面,当对应的方法执行完成时,便触发日志打印的逻辑。
以下为 FancyLogger 当中包含的注解:

注解备注
ActivityCycleActivity生命周期,类注解
FragmentCycleFragment生命周期,类注解
Click点击事件,方法注解
Crashcrash,方法注解
Event自定义事件,方法注解
ExceptionLog异常,方法注解
LoadScene加载场景,方法注解
ProcessLog过程方法注解
Switch切前后台,方法注解

FancyLogger 当中提供了两种注解,分别是方法注解和类注解,其中类注解为 ActivityCycle 和 FragmentCycle 能够自动的打印 Activity 和 Fragment 的生命周期。
以下分别为方法注解和类注解的示例。
方法注解
如下所示,在 testEvent 方法上面添加 Click 注解

@Click(msg = "testEvent")
public void testEvent(){
    Log.i("测试","执行了testEvent");
}

当我们执行 testEvent 方法时,会有如下打印,其中第一条日志为执行 testEvent 方法内部的日志,第二条日志为 FancyLogger 打印的日志。
在这里插入图片描述

类注解
如下所示,在 MainActivity 类上增加 ActivityCycle 注解

@ActivityCycle(tag="MainActivity")
public class MainActivity extends AppCompatActivity {
    ...
}

当 Activity 执行了对应的生命周期的方法时,会有如下日志打印:
在这里插入图片描述

TinyLog 的性能测试

以下为瞬间写入 10w 条 50byte 日志信息在各个不同维度的数据,其中内存,cpu峰值均使用 PerfDog 进行测试。
时间

机器不压缩不加密压缩加密
三星s20269.2ms276.6ms
iphone11604.9ms815.8ms

日志文件大小

机器不压缩不加密压缩加密
三星s2012.3MB851KB
iphone1110.3MB904KB

内存占用

以下场景使用 TinyLog demo 进行测试。
三星s20

场景不压缩不加密压缩加密
打开应用60MB63MB
初始化 TinyLog63MB66MB
写入10w条日志峰值83MB91MB
写入10w条之后的内存67MB70MB

iphone11

场景不压缩不加密压缩加密
打开应用38MB38MB
初始化 TinyLog38.2MB38.3MB
写入10w条日志峰值38.7MB39.8MB
写入10w条之后的内存38.6MB39MB

cpu 峰值

机器不压缩不加密压缩加密
三星s2018%18%
iphone1110%10%

2.TinyLog 服务端

日志多维度搜索

TinyLog 上传日志时候,支持自定义 tag。例如,用户反馈的场景可以增加 type 为 user,crash 场景可以增加 type 为 crash。我们除了可以按照日志上传时间进行搜索之外,还可以根据自定义的 tag 进行搜索。这样我们就可以根据不同的 tag 更加精准的定位到日志。

在这里插入图片描述

一键图形化展示日志

日志上传到服务端之后,点击在线查看,便可以将日志图形化展示出来。如下图所示,我们通过右边的图形就可以快速的定位到问题。点击右边红色的菱形模块,就能够定位到左边日志。

在这里插入图片描述

3.本地解压解密工具

如果我们遇到一些特殊场景,不方便上传日志,TinyLog 提供了本地解密工具,我们只需要将日志导出,用本地解密工具进行解密。本地解密工具只是将大部分解密的步骤放在本地,核心的非对称解密步骤仍然放在服务端,不会造成密钥的泄漏。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值