引子
有没有一个日志库除了可以打印字符串,还能打印列表、Map、异常,并自动添加 tag?
- 有!比如 Timber,Logger
有没有一个日志库可以动态地追加 N 种日志处理方式?比如输出到 logcat 的同时写文件,上传服务器?
- 有!比如 Timber,Logger
有没有一种日志库可以打印一切对象以及实现柔性生产线?
- 没有!
什么是柔性生产线?比如下图:
在该日志处理链路中,所有的日志都会被输出到 logcat,但 protobuf 日志是原样输出,其他日志会经过美化再输出。并且只有 protobuf 日志会被持久化并批量上传服务器。
上图的日志处理逻辑是串行的,即上一个处理逻辑的输出是下一个的输入。而 Timber 和 Logger 是并行的:
并行的处理方式,使得每个日志处理逻辑的输入都是原始日志,每个日志处理器的逻辑都得从头开始写,相同的日志处理逻辑无法复用。
除了串行日志处理外,柔性生产线的第二个特点是日志处理逻辑动态可拔插。比如可以轻松地在批量上传结点前插入日志压缩或加密。
为什么需要柔性生产线?
因为客户端日志其实存在多种多样的需求。
以日志输出目的地分类:
- 输出到控制台
- 输出到文件
- 输出到数据库
- 输出到服务器
以日志内容加工方式分类:
- 美化日志
- 日志加密
- 日志压缩
- 追加信息
以开发易用性分类:
- tag自动生成
- 一次性tag
- 打印异常
- 支持 format arguments
- 打印列表、Map
不同业务场景中,日志处理需求会以不同形式任意组合。柔性生产线是最灵活的组合方式。
这一篇会从如何实现柔性生产线以及日志库的易用性两个方面展开。
前情提要
为了实现柔性生产线式的日志处理,我写了EasyLog。
上两篇介绍了 EasyLog 的第一个版本的设计思路,在此做一个简短的介绍。
每个日志处理逻辑被抽象为拦截器:
// 日志拦截器
interface Interceptor {
// 日志处理逻辑
fun log(tag: String, message: String, priority: Int, chain: Chain, vararg args: Any)
// 是否开启当前拦截器
fun enable():Boolean
}
把日志输出到 logcat 的实现如下:
class LogcatInterceptor : Interceptor<String>() {
override fun log(tag: String, message: String, priority: Int, chain: Chain, vararg args: Any) {
// 如果该拦截器开启,则输出到logcat,否则直接传递给下一个拦截器
if (enable()) Log.d("test", message)
else chain.proceed(tag, message, priority, args)
}
}
这样的实现对于某个独立的日志需求,看上去非常脱裤子放屁。但这套接口的价值是多个日志处理逻辑的串联组合。责任链在其中扮演着重要角色:
// 责任链
class Chain(
// 持有一组拦截器
private val interceptors: List<Interceptor>,
// 当前拦截器索引
private val index: Int = 0
) {
// 将日志请求在链上传递
fun proceed(tag: String, message: String, priority: Int, vararg args: Any) {
// 用一条新的链包裹链上的下一个拦截器
val next = Chain(interceptors, index + 1)
// 获取链上当前的拦截器
val interceptor = interceptors.getOrNull(index)
// 执行当前拦截器逻辑,并传入新建的链
interceptor?.log(tag, message, priority, next, *args)
}
}
所有日志拦截器被责任链持有,日志沿链向后传递通过索引值+1实现。
EasyLog是日志库的入口类,通过其addInterceptor()方法可动态地添加拦截器:
EasyLog.apply {
addInterceptor(LogcatInterceptor())
addInterceptor(FileInterceptor())
}
上述代码添加了 logcat 以及文件拦截器,这样通过EasyLog.i("test log")输出的日志就会被输出到 logcat 并持久化到文件。
日志库的灵活性表现在“可动态化插入拦截器”:
EasyLog.apply {
addInterceptor(LogcatInterceptor())
addInterceptor(GzipInterceptor())
addInterceptor(EncryptInterceptor())
addInterceptor(FileInterceptor())
}
这条责任链会先将日志输出到logcat再进行Gzip压缩并加密后最终持久化到文件。
日志的每一个处理步骤都被内聚到一个单独类中,使得它可无副作用地被插入或摘除(不会影响到其他日志处理逻辑),这增加了程序的弹性和健壮性。
柔性生产线
EasyLog 在上线了数个版本之后迎来了第一个挑战。因为它的接口设计限制了日志的类型只能是 String:
interface Interceptor {
// 其中的 message 只能是 String 类型
fun log(tag: String, message: String, priority: Int, chain: Chain, vararg args: Any)
fun enable():Boolean
}
实际使用中日志可能是任何类型,比如客户端和服务器约定了上报日志的结构体(通常用 protobuf 定义,为简单起见用 data class 示意):
// 广告加载成功
data class AdLoadSuccessEvent (
val slotId: String,
val eventName: String,
val eventId: String,
val duration: Long
)
// 广告加载失败
data class AdLoadFail (
val slotId: String,
val eventName: String,
val eventId: String,
val cause: String,
)
如果日志库能够直接打印这样的结构体该多好(柔性生产线应该可以打印一切):
EasyLog.i(AdLoadFail(...))
总结一下,柔性生产线的特点:
- 串联日志处理,后续结点可以享受到前序结点的处理结果。
- 动态拔插日志处理逻辑。
- 可处理任何日志对象。
为了实现打印一切,拦截器接口应该设计成和类型无关:
// 处理泛型数据的日志拦截器
interface Interceptor<T> {
// 日志处理逻辑
fun log(tag: String, message: T, priority: Int, chain: Chain, vararg args: Any)
// 是否启动当前拦截器
fun enable():Boolean
}
为了将处理不同数据类型的拦截器串联在一起,需要重构一下责任链:
class Chain(
// 责任链持有 star 投影的拦截器
private val interceptors: List<Interceptor<*>>,
private val index: Int = 0
) {
// 责任链向后传递 Any 类型的日志
fun proceed(tag: String, message: Any, priority: Int, vararg args: Any) {
val next = Chain(interceptors, index + 1)
try {
// 将 star 投影的拦截器强转为 Any 类型
(interceptors.getOrNull(index) as? Interceptor<Any>)?.log(tag, message, priority, next, *args)
} catch (e: Exception) {
}
}
}
责任链持有的拦截器会处理不同类型的日志,为了将这些拦截器无差别地组织成列表,则需要将列表声明为List<Interceptor<*>>,因为Interceptor<任何类型>都是Interceptor<*>的子类型。但是在使用 star 投影的拦截器时需要强转成对应类型,为了让日志在拦截器上传递,需要将其强转为Any。
强转是有可能报错的,假设下一个拦截器是Interceptor<String>但责任链上传递来的数据是protobuf,则会发生 ClassCastException,遂使用 try-catch 兜底。
下面就组装一条柔性生产线以实现上图日志处理流程:
1. Logcat 拦截器
柔性生产线上的第一个拦截器是 Logcat 拦截器:
class LogcatInterceptor : Interceptor<Any>() {
override fun log(tag: String, message: Any, priority: Int, chain: Chain, vararg args: Any) {
// 将日志输出到 logcat
if (enable()) Log.println(priority, tag, getFormatLog(message, *args))
chain.proceed(tag, message, priority, args)
}
// 格式化日志
private fun getFormatLog(message: Any, vararg args: Any) =
// 如果日志是 Throwable 则追加堆栈信息
if (message is Throwable)
getStackTraceString(message)
else
// 如果 format arguments 不为空则格式化
if (args.isNotEmpty()) message.toString().format(args)
// 否则直接将结构体转换为string
else message.toString()
// 获取堆栈信息
private fun getStackTraceString(t: Throwable): String {
val sw = StringWriter(256)
val pw = PrintWriter(sw, false)
t.printStackTrace(pw)
pw.flush()
return sw.toString()
}
// 支持 format arguments
private fun String.format(args: Array<out Any>) =
if (args.isEmpty()) this else String.format(this, *args)
}
因为 Logcat 拦截器可以接受任何类型的日志,所以被定义为Interceptor<Any>。
该拦截器对日志做了格式化:如果是日志是 Throwable 类型的,则在当前日志后追加调用栈,否则将其直接转换为 String。
线性拦截器
为了在多线程环境下安全地使用日志库,在柔性生产线上插入了一个线性拦截器:
class LinearInterceptor : Interceptor<Any>() {
private val CHANNEL_CAPACITY = 50
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
// 日志处理Channel
private val channel = Channel<Event>(CHANNEL_CAPACITY)
init {
// 启动协程消费日志
scope.launch {
channel.consumeEach { event ->
// 防止 Channel 因一场关闭 的 try-catch
try {
event.apply { chain.proceed(tag, message, priority) }
} catch(e: Exception){
}
}
}
}
override fun log(tag: String, message: Any, priority: Int, chain: Chain, vararg args: Any) {
if (enable()) {
//启动协程发送日志
scope.launch { channel.send(Event(tag, message, priority, chain)) }
} else {
chain.proceed(tag, message, priority)
}
}
// 日志包装类
data class Event(val tag: String, val message: Any, val priority: Int, val chain: Chain)
}
实现线程安全的策略是“并行问题串行化”。Channel 是串行化的容器。线性拦截器初始化时启动了一个协程作为 Channel 的消费者(从队头取日志),而 log() 方法是 Channel 的生产者(从队尾插日志)。Channel 除了实现串行化还有背压策略,其默认大小是 50,表示若生产速度大于消费速度时,最多缓存 50 条日志,当超过该阈值时,生产者就会被挂起。
若消费 Channel 时抛出异常,则其会被关闭:
// 以挂起的方式消费 Channel 中元素
public suspend inline fun <E> ReceiveChannel<E>.consumeEach(action: (E) -> Unit): Unit =
consume {
for (e in this) action(e)
}
public inline fun <E, R> ReceiveChannel<E>.consume(block: ReceiveChannel<E>.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
var cause: Throwable? = null
try {
return block()
} catch (e: Throwable) {
cause = e
throw e
} finally {
cancelConsumed(cause)// 取消 Channel
}
}
// 取消 Channel
internal fun ReceiveChannel<*>.cancelConsumed(cause: Throwable?) {
cancel(cause?.let {
it as? CancellationException ?: CancellationException("Channel was consumed, consumer had failed", it)
})
}
为了避免后续日志处理逻辑抛出异常导致 Channel 取消,将消费逻辑 try-catch。
唯一标识拦截器
为了避免日志丢失,每一条日志都会被持久化,当成功上传服务器后再删除。
为了方便日志的增删,为每条日志生成唯一标识符,通过唯一标识符拦截器实现:
class LogInterceptor : Interceptor<Any>() {
override fun log(tag: String, message: Any, priority: Int, chain: Chain, vararg args: Any) {
if (enable()) {
// 把每条日志包装成带唯一标识符的 Log
val log = Log(UUID.randomUUID().toString(), message)
// 将 Log 沿责任链传递
chain.proceed(tag, log, priority, args)
}
}
}
// 带唯一标识符的日志
data class Log<T>(val id: String, val data: T)
唯一标识符拦截器接收 Any 类型的日志,把它包装 Log 再传递给下游责任链。
持久化拦截器
为了确保日志的完整性,每一条日志都会持久化到文件,通过持久化拦截器实现:
class SinkInterceptor : Interceptor<Log<Message>>() {
companion object {
val mmkv by lazy { MMKV.defaultMMKV() }
}
override fun log(tag: String, message: Log<Message>, priority: Int, chain: Chain, vararg args: Any) {
// 将日志持久化为二进制
if (enable()) mmkv.encode(message.id, message.data.toByteArray())
// 将日志原样传递给下一个拦截器
chain.proceed(tag, message, priority)
}
}
持久化拦截器不再接受任何类型,而是只接收Log<Message>,其中 Message 是 com.google.protobuf.Message,protobuf 的基类,它有一个toByteArray()序列化方法。
每一条日志都会以键值对形式持久化。键是日志唯一标识符,值是被序列化为二进制的日志内容,它们通过 MMKV 持久化到文件。
之所以选择 MMKV 是因为其出色的写性能,MMKV写的是内存(内核空间),并有系统机制保证落到文件。但相较之下其读性能稍差,并且其读只能将文件中所有的 key 一次性读出,若 key 很多,则可能发生 ANR。写数据库应该是一个更中庸保险的方法。
批处理拦截器
每来一条日志都上传服务器,浪费流量。遂先将日志堆积在内存中,达到一定数量后再一并上报。
上报的埋点事件使用 protobuf 定义如下:
// 业务事件1
message Event1 {
string eventName = 1;
uint64 eventTime = 2;
int duration = 3;
}
// 所有事件的泛化类型,protobuf中用 Any 表示
message Event {
google.protobuf.Any event = 1;
}
// 事件批次,用于批量上传
message EventBatch {
repeated Event event = 2;// 多个 Event
}
之所以使用 protobuf 是因为它高效的序列化。
使用 protoc 命令生成的 kotlin 都使用类似 DSL 方式构建对象,比如:
val event = event1 {
eventName = "load_success"
eventTime = System.currentTimeMillis()
duration = 20
}
将多条 protobuf 事件组织成批量事件的代码如下:
val logs = mutableListOf<Message>()
// 累加堆积日志为 EventBatch
logs.fold(EventBatch.newBuilder()) { acc, message ->
acc.addEvent(event { event = Any.pack(message) })
}.build()
批量日志拦截器定义如下:
class BatchInterceptor() : Interceptor<Log<Message>>() {
companion object {
var size: Int = 50
var interval: Long = 10_000L
}
private val list = mutableListOf<Log<Message>>()
private var lastFlushTime = 0L
private val scope = CoroutineScope(SupervisorJob())
private var flushJob: Job? = null
override fun log(tag: String, log: Log<Message>, priority: Int, chain: Chain, vararg args: Any) {
if (enable()) {
list.add(log)
flushJob?.cancel()
if (isOkFlush()) {
flush(chain, tag, priority)
} else {
flushJob = delayFlush(chain, tag, priority)
}
}
}
private fun isOkFlush() = lastFlushTime != 0L && SystemClock.elapsedRealtime() - lastFlushTime >= interval || list.size >= size
private fun flush(chain: Chain, tag: String, priority: Int) {
// 将多条日志打包
val logs = logs.fold(EventBatch.newBuilder()) { acc, message -> acc.addEvent(event { event = Any.pack(message) }) }.build()
// 使用 LogBatch 包装批量日志
val logBatch = LogBatch(list.map { it.id }, logs)
// 将 LogBatch 传递给下一个拦截器
chain.proceed(tag, logBatch, priority)
list.clear()
lastFlushTime = SystemClock.elapsedRealtime()
}
private fun delayFlush(chain: Chain, tag: String, priority: Int) = scope.launch(singleLogDispatcher) {
val delayTime = if (lastFlushTime == 0L) interval else interval - (SystemClock.elapsedRealtime() - lastFlushTime)
delay(delayTime)
flush(chain, tag, priority)
}
}
// 批量日志包装类,包含一批日志,以及它们的唯一标识符
data class LogBatch<T>(val ids: List<String>, val data: T)
批量日志拦截器只接受带唯一标识符的Log<Message>,它会将单条日志打包成批量日志并包装成 LogBatch 传递给下一个拦截器。LogBatch 携带了批量日志中所有日志的键,用于上传成功后删除日志。
批量日志处理可能遇到“小尾巴”、“线程安全”问题。这些问题的详细讲解可以点击客户端日志&埋点&上报的线程安全问题
上报拦截器
柔性生产线的终点是上报拦截器:
class UploadInterceptor : Interceptor<LogBatch<EventBatch>>() {
private val scope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
private val trackApi by lazy { retrofit.create(TrackApi::class.java) }
override fun log(tag: String, logs: LogBatch<EventBatch>, priority: Int, chain: Chain, vararg args: Any) {
if (enable()) scope.launch {
val success = trackApi.track(logs.data)
// 若上传成功,则删除本地日志
if (success) SinkInterceptor.mmkv.removeValuesForKeys(logs.ids.toTypedArray())
}
}
}
interface TrackApi {
// 直接上传 protobuf 的 post 接口
@POST("event/track")
@Headers("Content-Type: application/protobuf")
suspend fun track(@Body eventBatch: EventBatch): Boolean
}
上报拦截器接收LogBatch<EventBatch>并将其通过 POST 接口上报到服务器。若成功则删除本地日志。若不成功,留在本地的日志在下一次启动时再上传。
如果对日志在服务器落地的实时性&稳定性有要求,可以在这个拦截器实现长链接
断链
将上面 6 个拦截器组装成责任链:
EasyLog.apply {
addInterceptor(LogcatInterceptor())
addInterceptor(LinearInterceptor())
addInterceptor(LogInterceptor())
addInterceptor(SinkInterceptor())
addInterceptor(BatchInterceptor(50, 10_000))
addInterceptor(UploadInterceptor())
}
然后就可以使用 EasyLog 打印日志 & 埋点上报:
// 仅打印日志
EasyLog.log("load start")
// 日志 & 埋点 & 上报
EasyLog.log(
event1 {
eventName = "load_success"
eventTime = System.currentTimeMillis()
duration = 20
}
)
为了实现责任链的分流,即 protobuf 日志执行所有拦截器,其它日志只执行 Logcat 拦截器。责任链需要断流功能。
为了更灵活地实现责任链的断流,需稍微重构下接口,之前拦截器接口定义如下:
// 处理泛型数据的日志拦截器
interface Interceptor<T> {
// 日志处理逻辑
fun log(tag: String, message: T, priority: Int, chain: Chain, vararg args: Any)
// 是否启动当前拦截器
fun enable():Boolean
}
是否启用拦截器被定义为一个方法enable(),这样定义就不能运行时动态修改,重构如下:
abstract class Interceptor<T> {
abstract fun log(tag: String, message: T, priority: Int, chain: Chain, vararg args: Any)
// 可动态赋值的 lambda,默认启动当前拦截器
var isLoggable: (T) -> Boolean = { true }
}
将方法改为一个可在运行时动态赋值的 lambda,并为其添加了默认值为{ true }表示默认启用当前拦截器。接口不能包含默认值,遂把 interface 改为 abstract class。
同时修改addInterceptor()接口:
object EasyLog {
private val interceptors = mutableListOf<Interceptor<in Nothing>>()
// 新增 isLoggable 参数
fun <T> addInterceptor(
interceptor: Interceptor<T>,
isLoggable: (T) -> Boolean = { true }
) {
addInterceptor(interceptors.size, interceptor, isLoggable)
}
fun <T> addInterceptor(
index: Int,
interceptor: Interceptor<T>,
isLoggable: (T) -> Boolean = { true }
) {
interceptors.add(index, interceptor.apply { this.isLoggable = isLoggable })
}
然后只需要在添加唯一标识符拦截器时做一个断流操作就好了:
EasyLog.apply {
addInterceptor(LogcatInterceptor())
addInterceptor(LinearInterceptor())
// 只有 protobuf 日志才能通过
addInterceptor(LogInterceptor()) { log -> log is Message) }
addInterceptor(SinkInterceptor())
addInterceptor(BatchInterceptor(50, 10_000))
addInterceptor(UploadInterceptor())
}
易用性
打印一切
EasyLog.log()不仅可以传入字符串,还可以传入任何对象:
EasyLog.log("test")
EasyLog.log(User("taylor", 187))
打印异常
EasyLog 支持直接打印异常:
EasyLog.log(IllegalArgumentException("wrong type"))
效果如下:
format arg
EasyLog 支持 format arguments:
EasyLog.log("end %s", DEBUG, "ab") // 打印结构体 输出 "end ab"
自动tag/一次性tag
默认情况下 EasyLog 会自动选取所在类作为日志tag:
class UserManager {
fun print() {
// 打印字符串(自动选取tag=UserManager)
EasyLog.log("start", ERROR)
}
}
EasyLog 还提供了一次性tag方法:
EasyLog.tag("test").log("end")
日志的tag被指定为”test“,该tag仅在此条日志中生效。
该特性适用于想通过另一种维度过滤日志。
一次性tag的实现逻辑如下:
object EasyLog {
private val MAX_TAG_LENGTH = 23
private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")
// 一次性tag
private var onetimeTag = ThreadLocal<String>()
// 一次性tag的存取逻辑
private var tag: String?
// 获取一次性tag,获取即销毁
get() = onetimeTag.get()?.also { onetimeTag.remove() }
// 设置一次性tag
set(value) {
onetimeTag.set(value)
}
// 日志tag黑名单(避免自动tag选自日志库内部类)
private val blackList = listOf(
EasyLog::class.java.name,
Chain::class.java.name
)
// 输出日志
fun log(message: Any, priority: Int = VERBOSE, vararg args: Any) {
// 创建tag并输出日志
chain.proceed(createTag(), message, priority, *args)
}
// 创建tag
private fun createTag(): String {
return tag ?: Throwable().stackTrace
.first { it.className !in blackList }
.let(::createStackElementTag)
}
// 获取当前日志所在类名
private fun createStackElementTag(element: StackTraceElement): String {
var tag = element.className.substringAfterLast('.')
val m = ANONYMOUS_CLASS.matcher(tag)
if (m.find()) {
tag = m.replaceAll("")
}
return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
tag
} else {
tag.substring(0, MAX_TAG_LENGTH)
}
}
}
将一次性tag存储在 ThreadLocal 中,可以避免并发访问tag时出现的多线程问题(因为每个线程都会一个自己的一次性tag副本)。
每次输出日志都会调用createTag()创建tag,先获取一次性tag,若获取成功,则移除一次性tag,否则通过调用栈获取当前类名。
一次性拦截器
EasyLog 支持一次性拦截器,即动态地为当前日志添加一种日志处理逻辑且用完即丢:
EasyLog.interceptor(FrameInterceptor()).log("higlight")
EasyLog.log("after highlight")
// 输出
┌──────────────────────────────────────────────────────────────────────────
│ higlight
└──────────────────────────────────────────────────────────────────────────
after highlight
通过interceptor()方法为日志指定了一次性拦截器,使得这条日志带有边框。
一次性拦截器的实现逻辑如下:
object EasyLog {
// 一次性拦截器
private var onetimeInterceptor: ThreadLocal<Interceptor<*>>? = null
fun log(message: Any, priority: Int = VERBOSE, vararg args: Any) {
chain.proceed(createTag(), message, priority, *args)
// 当前日志已流过所有拦截器,删除一次性拦截器
onetimeInterceptor?.takeIf { it.get() != null }?.also { removeInterceptor(it.get()) }
}
// 添加一次性拦截器
fun interceptor(interceptor: Interceptor<*>): EasyLog {
// 总是将一次性拦截器添加在责任链头部
interceptors.add(0, interceptor)
if (onetimeInterceptor == null) onetimeInterceptor = ThreadLocal()
onetimeInterceptor?.set(interceptor)
return this
}
}
打印列表
EasyLog 支持打印列表,还可以选择性地打印复杂结构体中的字段:
val array = listOf(1,2,3)
EasyLog.list(array)// 输出 ”[1, 2, 3]“
val users = listOf(
User(name = "peter", age = 30, company = "ali"),
User(name = "joice", age = 23, company = "baidu"),
User(name = "martin", age = 29, company = "tecent"),
)
EasyLog.list(users) { "${it.name}(${it.age}) in ${it.company}" }
// 输出”[peter(30) in ali, joice(23) in baidu, martin(29) in tecent]“
打印列表是通过一次性拦截器实现的:
object EasyLog {
fun <T> list(message: Iterable<T>, priority: Int = VERBOSE, map: ((T) -> String)? = null) {
// 添加一次性拦截器ListInterceptor
interceptor(ListInterceptor(map))
chain.proceed(createTag(), message, priority)
onetimeInterceptor?.takeIf { it.get() != null }?.also { removeInterceptor(it.get()) }
}
}
其中 ListInterceptor 实现如下:
class ListInterceptor<T>(private val map: ((T) -> String)?) : Interceptor<Iterable<T>>() {
override fun log(tag: String, message: Iterable<T>, priority: Int, chain: Chain, vararg args: Any) {
// 将list折叠成 string
val messageList = message.log { map?.invoke(it) ?: it.toString() }
chain.proceed(tag, messageList, priority, args)
}
// 将遍历的元素折叠成一个 string
fun <T> Iterable<T>.log(map: (T) -> String) =
fold(StringBuilder("[")) { acc: StringBuilder, t: T -> acc.append("\t${map(t)},") }.append("]").toString()
ListInterceptor 的输入是一个 Iterable,通过遍历它,将其中每个元素根据指定的规则追加到字符串末尾。
打印Map
EasyLog 支持打印 Map 以及嵌套Map。
val map = mapOf(
"a" to mapOf( "1" to true),
"b" to mapOf( "2" to false, "3" to true)
)
EasyLog.map(map)
上述代码输入如下:
{
[a] = { [1] = true },
[b] = {
[2] = false,
[3] = true
}
}
打印map依然通过一次性拦截器实现:
class MapInterceptor<K, V> : Interceptor<Map<K, V>>() {
override fun log(tag: String, message: Map<K, V>, priority: Int, chain: Chain, vararg args: Any) {
chain.proceed(tag, message.log(4), priority, args)
}
}
fun <K, V> Map<K, V?>.log(space: Int = 0): String {
val indent = StringBuilder().apply {
repeat(space) { append(" ") }
}.toString()
return StringBuilder("\n${indent}{").also { sb ->
this.iterator().forEach { entry ->
val value = entry.value.let { v ->
(v as? Map<*, *>)?.log("${indent}${entry.key} = ".length) ?: v.toString()
}
sb.append("\n\t${indent}[${entry.key}] = $value,")
}
sb.append("\n${indent}}")
}.toString()
}
日志过滤
有些日志只希望在调试时输出,可以通过优先级开关在线上版本关闭之:
object EasyLog {
const val VERBOSE = 2
const val DEBUG = 3
const val INFO = 4
const val WARN = 5
const val ERROR = 6
const val ASSERT = 7
const val NONE = 8
// 当前优先级
var curPriority = VERBOSE
}
class LogcatInterceptor : Interceptor<Any>() {
override fun log(tag: String, message: Any, priority: Int, chain: Chain, vararg args: Any) {
//为 logcat 拦截器添加优先级过滤逻辑
if (enable() &&
EasyLog.curPriority <= priority &&
EasyLog.curPriority != EasyLog.NONE
) Log.println(priority, tag, getFormatLog(message, *args))
chain.proceed(tag, message, priority, args)
}
}
通过动态设置优先级可实现线上版本 logcat 日志过滤
EasyLog.priority = if(BuildConfig.DEBUG) EasyLog.VERBOSE else EasyLog.NONE