OkHttp 调试日志实战指南:打开 HTTP/2 帧日志与 TaskRunner 任务日志的完整方法
2026/9/18 3:15:32 网站建设 项目流程

OkHttp 调试日志实战指南:打开 HTTP/2 帧日志与 TaskRunner 任务日志的完整方法

【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttp

OkHttp 在核心链路中埋入了基于java.util.logging(JULL)的内部调试日志,但没有对外暴露公开的开关。本篇技术指南将讲解如何用最少的代码启用 HTTP/2 帧级日志(okhttp3.internal.http2.Http2)与任务调度日志(okhttp3.internal.concurrent.TaskRunner),逐字段解读两类日志的真实输出格式,并给出 Android 设备端通过adb命令免代码激活的方法。读完后,你能够在 JVM 应用、Android 设备上快速定位 HTTP/2 连接协商、流控、GOAWAY 等协议层问题,以及连接池与 HTTP/2 SETTINGS 处理等内部任务的调度行为。

为什么需要 OkHttp 的调试日志

排查 HTTP/2 相关问题(协议协商失败、连接被服务端重置、流控窗口异常、GOAWAY触发等)时,应用层日志往往只能看到"连接失败"这一结果,无法还原底层帧交互。OkHttp 的调试日志恰好补上这一层:它在Http2Reader/Http2Writer中逐帧记录收发过程,在TaskRunner中记录后台任务的入队、开始与结束,是诊断协议栈与调度问题的第一手资料。

这些日志基于 JULL 实现,而 JULL 的配置(Logger 层级、Handler 挂载、级别过滤)本身就较为繁琐。官方贡献文档 debug_logging.md 给出的做法是:直接把仓库中的OkHttpDebugLogging工具类复制到项目里,然后调用一行代码即可开启。

核心工具:OkHttpDebugLogging 的工作原理

仓库中该工具类的完整实现在 OkHttpDebugLogging.kt,其结构如下:

object OkHttpDebugLogging { // Keep references to loggers to prevent their configuration from being GC'd. private val configuredLoggers = CopyOnWriteArraySet<Logger>() fun enableHttp2() = enable(Http2::class) fun enableTaskRunner() = enable(TaskRunner::class) fun logHandler() = ConsoleHandler().apply { level = Level.FINE formatter = object : SimpleFormatter() { override fun format(record: LogRecord) = String.format("[%1\$tF %1\$tT] %2\$s %n", record.millis, record.message) } } fun enable( loggerClass: String, handler: Handler = logHandler(), ): Closeable { val logger = Logger.getLogger(loggerClass) if (configuredLoggers.add(logger)) { logger.addHandler(handler) logger.level = Level.FINEST } return Closeable { logger.removeHandler(handler) } } fun enable(loggerClass: KClass<*>) = enable(loggerClass.java.name) }

从源码结构看,有三个关键设计值得注意:

  1. Logger 名称与内部类的绑定关系enableHttp2()等价于enable(Http2::class),即对名为okhttp3.internal.http2.Http2的 Logger 生效。这与内部实现完全对应——Http2Writer.kt 与 Http2Reader.kt 中使用的都是Logger.getLogger(Http2::class.java.name),也就是说读写两侧共享同一个 Logger 名称,启用一次即可同时看到收发两个方向的帧。
  2. 引用防回收configuredLoggers集合持有已配置 Logger 的强引用,注释明确说明这是为了防止 JULL 配置被 GC 回收,保证日志在进程生命周期内持续有效。
  3. 可关闭的 Closeableenable()返回一个Closeable,调用其close()会移除挂载的 Handler。因此该 API 可以精确地"按请求/按阶段"临时开启日志,而不影响全局 JULL 配置。

启用方式是按需组合两个快捷方法(也可以只开启其一):

import okhttp3.OkHttpDebugLogging OkHttpDebugLogging.enableHttp2() OkHttpDebugLogging.enableTaskRunner()

enableTaskRunner()对应 TaskRunner.kt 中的Logger.getLogger(TaskRunner::class.java.name),即 Logger 名称为okhttp3.internal.concurrent.TaskRunner

logHandler()创建的ConsoleHandler级别为FINE,格式化器输出[2020-01-01 00:00:00] 消息这样的"日期时间 + 消息"单行格式,与下文示例输出一致。Logger 本身被提升到Level.FINEST,而 Handler 过滤在FINE,两者配合确保帧日志(FINE级别)不会被 Handler 丢弃。

HTTP/2 帧日志:逐字段解读

开启后,OkHttp 会记录 HTTP/2 连接的入站(<<)与出站(>>)帧。仓库文档中的真实输出示例如下:

[2020-01-01 00:00:00] >> CONNECTION 505249202a20485454502f322e300d0a0d0a534d0d0a0d0a [2020-01-01 00:00:00] >> 0x00000000 6 SETTINGS [2020-01-01 00:00:00] >> 0x00000000 4 WINDOW_UPDATE [2020-01-01 00:00:00] >> 0x00000003 47 HEADERS END_STREAM|END_HEADERS [2020-01-01 00:00:00] << 0x00000000 6 SETTINGS [2020-01-01 00:00:00] << 0x00000000 0 SETTINGS ACK [2020-01-01 00:00:00] << 0x00000000 4 WINDOW_UPDATE [2020-01-01 00:00:00] >> 0x00000000 0 SETTINGS ACK [2020-01-01 00:00:00] << 0x00000003 322 HEADERS END_HEADERS [2020-01-01 00:00:00] << 0x00000003 288 DATA [2020-01-01 00:00:00] << 0x00000003 0 DATA END_STREAM [2020-01-01 00:00:00] << 0x00000000 8 GOAWAY [2020-01-01 00:00:05] << 0x00000000 8 GOAWAY

各字段的含义由 Http2.kt 中的frameLog()决定,其 KDoc 明确定义了格式direction streamID length type flags,实现上按"%s 0x%08x %5d %-13s %s"格式化:

字段说明
<</>>方向。<<为入站(服务端→客户端),>>为出站(客户端→服务端),由inbound参数决定
0x00000000流 ID(8 位十六进制)。0表示连接级帧(SETTINGS、GOAWAY 等),3表示客户端发起的首个请求流
长度列帧载荷长度(字节),如DATA帧的 288
类型列帧类型名,来自内部帧类型表;未知类型以0x%02x十六进制兜底
标志列标志位组合,如END_STREAM|END_HEADERSSETTINGS/PING只认ACK,其余按二进制兜底

几处值得对照实现的细节:

  • 连接前导串:第一行的>> CONNECTION 5052...是客户端发出的 HTTP/2 连接前导串(PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n的十六进制)。Http2Reader.kt 在读取入站前导串时先判断logger.isLoggable(FINE)再打日志,避免在日志关闭时白白执行十六进制转换。
  • WINDOW_UPDATE 的特殊格式WINDOW_UPDATE帧没有标志位,第五列打印的是窗口增量(windowSizeIncrement)而非 flags,见frameLogWindowUpdate()。示例中<< 0x00000000 4 WINDOW_UPDATE的 4 为载荷长度。
  • HEADERS 的流控观察:示例中客户端发出 47 字节 HEADERS(含END_STREAM|END_HEADERS),服务端回 322 字节响应 HEADERS 加 288 字节 DATA,随后连续两帧GOAWAY——这类输出对判断"服务端为何拒绝/断开连接"非常直观。

TaskRunner 任务日志:观察后台调度

开启enableTaskRunner()后,OkHttp 内部任务系统会记录任务的入队(scheduled)、开始(starting)、再次调度(run again after)与结束(finished run in)。示例输出:

[2020-01-01 00:00:00] Q10000 scheduled after 0 µs: OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 starting : OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 run again after 300 s : OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 finished run in 1 ms: OkHttp ConnectionPool [2020-01-01 00:00:00] Q10001 scheduled after 0 µs: OkHttp squareup.com applyAndAckSettings [2020-01-01 00:00:00] Q10001 starting : OkHttp squareup.com applyAndAckSettings [2020-01-01 00:00:00] Q10003 scheduled after 0 µs: OkHttp squareup.com onSettings [2020-01-01 00:00:00] Q10003 starting : OkHttp squareup.com onSettings [2020-01-01 00:00:00] Q10001 finished run in 3 ms: OkHttp squareup.com applyAndAckSettings [2020-01-01 00:00:00] Q10003 finished run in 528 µs: OkHttp squareup.com onSettings [2020-01-01 00:00:00] Q10000 scheduled after 0 µs: OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 starting : OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 run again after 300 s : OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 finished run in 739 µs: OkHttp ConnectionPool

格式中各部分的来源可以在源码中一一找到:

  • Q10000是任务队列的单调递增编号;"scheduled after …" 与 "run again after …" 两条消息都产生于 TaskQueue.kt 的任务入队/重排路径,括号中的时长由formatDuration格式化为 µs/ms/s 等可读单位;
  • "starting" 与 "finished run in …" 产生于 TaskLogger.kt,记录任务真正开始执行与执行耗时;
  • 冒号后的名称(如OkHttp ConnectionPoolOkHttp squareup.com applyAndAckSettings)是任务队列的名称——连接池清理、按域名划分的 SETTINGS 处理任务都注册为独立的命名队列。

因此通过这份日志,可以直接看到"连接池每 300 s 清理一次过期连接"(run again after 300 s)、"SETTINGS 到达后先触发 onSettings 再 applyAndAckSettings"这类内部时序,而不需要猜测。

Android 设备上的免代码激活:adb 设置日志级别

在 Android 上,OkHttp 内部使用java.util.logging,但通过桥接 Handler 将日志转发到android.util.Log,并且依据启动时 logcat 标签级别决定是否启用——这意味着无需改代码,直接用adb设置系统属性即可打开调试日志。官方文档给出的命令为:

$ adb shell setprop log.tag.okhttp.Http2 DEBUG $ adb shell setprop log.tag.okhttp.TaskRunner DEBUG $ adb logcat '*:E' 'okhttp.Http2:D' 'okhttp.TaskRunner:D'

这三条命令的作用:

  1. setprop log.tag.okhttp.Http2 DEBUG:把 logcat 标签okhttp.Http2的级别设为 DEBUG,使该标签的 DEBUG 级日志放行;
  2. setprop log.tag.okhttp.TaskRunner DEBUG:同理开启任务调度日志;
  3. adb logcat '*:E' 'okhttp.Http2:D' 'okhttp.TaskRunner:D':过滤其他标签只显示 E 级(ERROR),而将两个 OkHttp 标签放宽到 D(DEBUG),从而在默认较嘈杂的 logcat 中突出调试信息。

仓库源码印证了标签名与"启动时读取级别"的机制。AndroidLog.kt 中维护了 FQN 到 logcat 标签的映射表:

private val knownLoggers = LinkedHashMap<String, String>() .apply { val packageName = OkHttpClient::class.java.`package`?.name if (packageName != null) { this[packageName] = "OkHttp" } this[OkHttpClient::class.java.name] = "okhttp.OkHttpClient" this[Http2::class.java.name] = "okhttp.Http2" this[TaskRunner::class.java.name] = "okhttp.TaskRunner" this["okhttp3.mockwebserver.MockWebServer"] = "okhttp.MockWebServer" }.toMap()

这解释了为什么adb命令中的标签必须是okhttp.Http2/okhttp.TaskRunner而不是包名:logcat 标签有 23 字符上限,okhttp3.internal.http2.Http2这类 FQN 无法直接使用,OkHttp 因此为内部 Logger 预定义了短标签。同一文件中的enableLogging()在初始化时检查Log.isLoggable(tag, Log.DEBUG):若该标签允许 DEBUG,则把对应 JULL Logger 提到Level.FINE;否则降到Level.WARNING。由于该检查发生在启动时,必须先setprop再启动应用,之后才能看到帧日志与任务日志。

此外该桥接层还有一个实用细节:androidLog()会把超过MAX_LOG_LENGTH = 4000字符的日志按行拆分后分段输出,避免 logcat 截断长消息。

进阶用法:自定义 Handler 与临时开关

enable(loggerClass, handler)的两个参数都开放了定制空间:

// 只允许指定名称的 Logger,并输出到自定义 Handler(如文件 Handler) val closeable = OkHttpDebugLogging.enable( "okhttp3.internal.http2.Http2", myFileHandler ) // 需要停止时 closeable.close()
  • loggerClass 参数:可以直接传 Logger 名称字符串,因此也可以为其他内部 Logger(如okhttp3.OkHttpClient,见 Platform.kt 中同样使用 JULL 的 OkHttpClient Logger)单独配置。
  • handler 参数:默认logHandler()输出到控制台;传入自定义Handler可以把调试日志重定向到文件或日志系统。
  • Closeable 语义:关闭只移除 Handler,不会回滚 Logger 的level。如果需要多次开关,建议每次enable后持有返回的Closeable,用完即关,避免同一个 Logger 上残留多个输出通道。
  • 幂等保护:内部configuredLoggers.add(logger)保证同一 Logger 只挂载一次 Handler,重复调用enableHttp2()不会叠加重复输出。

一个需要留意的点:logHandler()ConsoleHandler输出到System.err,在 Android 上若直接在 App 进程内调用OkHttpDebugLogging,日志走向取决于平台的 JULL 桥接;Android 设备上更推荐前文的adb setprop方案,JVM 服务端/桌面应用则直接复制OkHttpDebugLogging.kt到项目即可。

小结

OkHttp 的调试日志是排查协议层问题的利器,使用上有三个要点:

  1. JVM 侧:把 OkHttpDebugLogging.kt 复制到项目,调用enableHttp2()/enableTaskRunner()打开对应 Logger(okhttp3.internal.http2.Http2okhttp3.internal.concurrent.TaskRunner);
  2. 输出解读:帧日志按direction streamID length type flags五列解析(WINDOW_UPDATE第五列是窗口增量),任务日志按Q<编号> <动作> <时长>: <队列名>解析;
  3. Android 侧:启动应用前执行adb shell setprop log.tag.okhttp.Http2 DEBUG(及okhttp.TaskRunner),再用adb logcat '*:E' 'okhttp.Http2:D' 'okhttp.TaskRunner:D'过滤观察,标签名来自源码中的 FQN 映射表,切勿写成 FQN 全称。

配合OkHttpEventListener/EventListener回调与HttpLoggingInterceptor(见 okhttp-logging-interceptor 模块 的说明)可分别覆盖应用层请求日志与协议层帧日志,两者结合基本可以完整还原一次请求从 DNS 到帧交互的全过程。

【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询