引子
有没有一个日志库除了能够打印字符串,还能打印列表、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 是由于 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
推荐阅读
每次调试打印日志都很头痛
客户端日志&埋点&上报的接口规划
客户端日志&埋点&上报的性能优化