日志与监控:Chucker拦截器、网络请求耗时统计、自定义Metrics上报
网络层开发完了,接口调通了,数据也拿到了——然后呢?
说实话,很多项目走到这一步就停了。但真正线上跑起来,你会发现一堆问题:哪个接口慢?哪个接口报错多?用户反馈网络卡顿,你连日志都捞不到。这就是我今天要聊的话题——日志与监控。
核心要点:网络层不仅要「能用」,还要「可观测」。Chucker帮你本地调试,耗时统计帮你定位性能瓶颈,Metrics上报帮你做线上监控。
1. Chucker拦截器:本地调试的瑞士军刀
先说说Chucker。这玩意儿我用了好几年了,从它还是叫「Chuck」的时候就开始用。说白了,它就是一个拦截器,能把你App里所有的网络请求和响应都记录下来,然后在手机上直接查看。
为什么不用Charles或Fiddler?
你想想看,Charles需要电脑,需要代理配置,HTTPS还得装证书。Chucker直接在手机端运行,开发人员、测试人员、甚至产品经理都能用。我在项目中遇到过好几次,测试说「接口报错了」,我直接让他打开Chucker看请求体和响应体,问题秒定位。
集成方式
// build.gradle.kts
debugImplementation("com.github.chuckerteam.chucker:library:4.0.0")
releaseImplementation("com.github.chuckerteam.chucker:library-no-op:4.0.0")
注意这个no-op版本。Release包用空实现,不会记录任何数据。这是基本的安全意识——线上用户的请求数据不能随便存本地。
OkHttp拦截器配置
val chuckerInterceptor = ChuckerInterceptor.Builder(context)
.maxContentLength(250_000L) // 最大记录250KB
.redactHeaders("Authorization", "Cookie") // 脱敏
.alwaysReadResponseBody(true) // 总是读取响应体
.build()
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(chuckerInterceptor)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) BODY else NONE
})
.build()
我的习惯:Chucker和HttpLoggingInterceptor一起用。Chucker看详情,Logcat看实时输出。两个不冲突,各有各的用途。
避坑指南
我曾经在线上包忘了切no-op版本,结果用户手机里存了大量请求日志。虽然Chucker有锁屏保护,但数据在本地就有风险。后来我写了个Gradle插件,自动检查Release构建是否用了no-op,不通过就编译失败。
2. 网络请求耗时统计:从「感觉慢」到「数据说话」
「这个接口好慢啊」——这种话在开发中太常见了。但到底慢在哪?DNS解析?连接建立?数据传输?还是服务器处理?
OkHttp提供了EventListener,可以精确到每个阶段的时间。我一般会封装一个自定义的EventListener,把耗时数据打点上报。
自定义EventListener
class MetricsEventListener : EventListener() {
private var startTime = 0L
private var dnsStart = 0L
private var connectStart = 0L
private var requestHeadersStart = 0L
private var responseBodyStart = 0L
override fun callStart(call: Call) {
startTime = System.currentTimeMillis()
}
override fun dnsStart(call: Call, domainName: String) {
dnsStart = System.currentTimeMillis()
}
override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {
val cost = System.currentTimeMillis() - dnsStart
reportMetric("dns_cost", cost, call)
}
override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
connectStart = System.currentTimeMillis()
}
override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) {
val cost = System.currentTimeMillis() - connectStart
reportMetric("connect_cost", cost, call)
}
override fun requestHeadersStart(call: Call) {
requestHeadersStart = System.currentTimeMillis()
}
override fun requestHeadersEnd(call: Call, request: Request) {
val cost = System.currentTimeMillis() - requestHeadersStart
reportMetric("request_headers_cost", cost, call)
}
override fun responseBodyStart(call: Call) {
responseBodyStart = System.currentTimeMillis()
}
override fun responseBodyEnd(call: Call, byteCount: Long) {
val cost = System.currentTimeMillis() - responseBodyStart
reportMetric("response_body_cost", cost, call)
}
override fun callEnd(call: Call) {
val totalCost = System.currentTimeMillis() - startTime
reportMetric("total_cost", totalCost, call)
}
override fun callFailed(call: Call, ioe: IOException) {
val totalCost = System.currentTimeMillis() - startTime
reportMetric("call_failed", totalCost, call, error = ioe)
}
private fun reportMetric(name: String, cost: Long, call: Call, error: Throwable? = null) {
val url = call.request().url.toString()
// 上报到你的Metrics系统
MetricsCollector.record(name, mapOf(
"url" to url,
"cost" to cost,
"error" to error?.message.orEmpty()
))
}
}
注意:EventListener的回调是在OkHttp的线程池里执行的。不要在回调里做耗时操作,也不要直接更新UI。上报操作最好异步处理,或者用Channel发送到其他协程。
在Retrofit中集成
val okHttpClient = OkHttpClient.Builder()
.eventListener(MetricsEventListener())
.build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.example.com/")
.build()
就这么简单。所有经过这个OkHttpClient的请求,都会被MetricsEventListener记录。你想想看,有了这些数据,你就能回答「哪个接口的DNS解析最慢」「哪个接口的响应体传输最耗时」这类问题了。
3. 自定义Metrics上报:把数据送到该去的地方
数据记录下来了,然后呢?总不能在Logcat里看吧。我们需要一个统一的上报机制。
设计思路
我个人习惯把Metrics上报设计成一个独立的模块,不依赖任何业务代码。它只做一件事:接收数据,然后批量上报。
object MetricsCollector {
private val metricsChannel = Channel<MetricEvent>(Channel.BUFFERED)
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init {
scope.launch {
val batch = mutableListOf<MetricEvent>()
for (event in metricsChannel) {
batch.add(event)
if (batch.size >= 20) {
flush(batch)
batch.clear()
}
}
}
}
fun record(name: String, tags: Map<String, String>) {
metricsChannel.trySend(MetricEvent(name, System.currentTimeMillis(), tags))
}
private suspend fun flush(events: List<MetricEvent>) {
// 批量上报到服务器
// 可以用Retrofit,也可以用原始的HttpURLConnection
// 注意:不要用同一个OkHttpClient,避免循环依赖
}
data class MetricEvent(
val name: String,
val timestamp: Long,
val tags: Map<String, String>
)
}
为什么用Channel?因为Channel天然支持背压。如果上报速度太快,数据会在Channel里排队,不会丢失。而且用协程处理,不会阻塞主线程。
上报策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 实时上报 | 每产生一条数据立即上报 | 关键错误、崩溃信息 |
| 批量上报 | 攒够一定数量或定时上报 | 普通性能指标、请求耗时 |
| 采样上报 | 按比例随机上报 | 高频率事件、用户行为 |
我一般用批量上报+定时上报的组合。比如每20条或者每30秒上报一次。这样既不会太频繁,也不会丢失太多数据。
4. 整体架构图
下面这张图展示了整个日志与监控体系的架构。从请求发出到数据上报,每个环节都清晰可见。
5. 线上监控的注意事项
最后聊几个线上监控的坑。我踩过,希望你别踩。
- 不要上报敏感数据:请求参数里可能包含手机号、密码、Token。上报前一定要做脱敏处理。Chucker的
redactHeaders就是干这个的。 - 控制上报频率:每个接口都上报耗时,如果用户频繁操作,数据量会很大。建议加采样率,比如只上报10%的请求。
- 区分Debug和Release:Debug包可以全量记录,Release包只记录错误和慢请求。我一般设置阈值——超过500ms的请求才上报。
- 不要阻塞主线程:所有上报操作都放到后台线程。用协程+Channel的方式就很合适。
曾经踩过的坑:有一次我把Metrics上报的Retrofit实例和业务Retrofit实例混用了。结果Metrics上报失败时,触发了重试机制,重试又触发了Metrics上报……死循环了。后来我单独建了一个OkHttpClient给Metrics用,不添加任何拦截器。
好了,日志与监控这块就聊这么多。记住一句话:没有监控的网络层,就像没有仪表盘的飞机——你飞得再高,也不知道什么时候会掉下来。
总结:Chucker解决本地调试问题,EventListener解决耗时分析问题,MetricsCollector解决数据上报问题。三者配合,你的网络层就真正做到了「可观测」。