日志与监控: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. 整体架构图

下面这张图展示了整个日志与监控体系的架构。从请求发出到数据上报,每个环节都清晰可见。

网络层日志与监控架构 App 网络请求 OkHttp 拦截器链 ChuckerInterceptor | HttpLoggingInterceptor MetricsEventListener DNS | 连接 | 请求头 | 响应体 各阶段耗时 MetricsCollector (Channel) Chucker 本地查看 Metrics 服务器上报 请求层 拦截器 监听器

5. 线上监控的注意事项

最后聊几个线上监控的坑。我踩过,希望你别踩。

  • 不要上报敏感数据:请求参数里可能包含手机号、密码、Token。上报前一定要做脱敏处理。Chucker的redactHeaders就是干这个的。
  • 控制上报频率:每个接口都上报耗时,如果用户频繁操作,数据量会很大。建议加采样率,比如只上报10%的请求。
  • 区分Debug和Release:Debug包可以全量记录,Release包只记录错误和慢请求。我一般设置阈值——超过500ms的请求才上报。
  • 不要阻塞主线程:所有上报操作都放到后台线程。用协程+Channel的方式就很合适。

曾经踩过的坑:有一次我把Metrics上报的Retrofit实例和业务Retrofit实例混用了。结果Metrics上报失败时,触发了重试机制,重试又触发了Metrics上报……死循环了。后来我单独建了一个OkHttpClient给Metrics用,不添加任何拦截器。

好了,日志与监控这块就聊这么多。记住一句话:没有监控的网络层,就像没有仪表盘的飞机——你飞得再高,也不知道什么时候会掉下来。

总结:Chucker解决本地调试问题,EventListener解决耗时分析问题,MetricsCollector解决数据上报问题。三者配合,你的网络层就真正做到了「可观测」。

公众号:蓝海资料掘金营,微信 deep3321