引子

有没有一个日志库除了能够打印字符串,还能打印列表、Map、反常,并主动增加 tag?

  • 有!比方 Timber,Logger

有没有一个日志库能够动态地追加 N 种日志处理办法?比方输出到 logcat 的一起写文件,上传服务器?

  • 有!比方 Timber,Logger

有没有一种日志库能够打印全部目标以及完成柔性生产线

  • 没有!

什么是柔性生产线?比方下图:

客户端日志&埋点&上报的柔性生产线
在该日志处理链路中,全部的日志都会被输出到 logcat,但 protobuf 日志是原样输出,其他日志会经过美化再输出。而且只有 protobuf 日志会被耐久化并批量上传服务器。

上图的日志处理逻辑是串行的,即上一个处理逻辑的输出是下一个的输入。而 Timber 和 Logger 是并行的:

客户端日志&埋点&上报的柔性生产线

并行的处理办法,使得每个日志处理逻辑的输入都是原始日志,每个日志处理器的逻辑都得从头开始写,相同的日志处理逻辑无法复用。

除了串行日志处理外,柔性生产线的第二个特点是日志处理逻辑动态可拔插。比方能够轻松地在批量上传结点前刺进日志紧缩或加密。

为什么需求柔性生产线?

由于客户端日志其实存在多种多样的需求。

以日志输出目的地分类:

  1. 输出到控制台
  2. 输出到文件
  3. 输出到数据库
  4. 输出到服务器

以日志内容加工办法分类:

  1. 美化日志
  2. 日志加密
  3. 日志紧缩
  4. 追加信息

以开发易用性分类:

  1. tag主动生成
  2. 一次性tag
  3. 打印反常
  4. 支撑 format arguments
  5. 打印列表、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(...))

总结一下,柔性生产线的特点:

  1. 串联日志处理,后续结点能够享受到前序结点的处理结果。
  2. 动态拔插日志处理逻辑。
  3. 可处理任何日志目标。

为了完成打印全部,拦截器接口应该规划成和类型无关:

// 处理泛型数据的日志拦截器
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 是由于 mmap 能够避免日志丢失。由于任何在运用进程的耐久化办法都是耗时的(写数据库,写文件),若在该过程中运用被杀,则日志丢失。

mmap 是坚持日志完整性的仅有解。由于它不是写文件,而是写内存(写到内核空间),并由系统保证落到文件。

批处理拦截器

每来一条日志都上传服务器,浪费流量。遂先将日志堆积在内存中,达到必定数量后再一并上报。

上报的埋点事情运用 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

Talk is cheap, show me the code

EasyLog

推荐阅读

每次调试打印日志都很头痛

客户端日志&埋点&上报的接口规划

客户端日志&埋点&上报的性能优化