OkHttp 调试日志实战指南打开 HTTP/2 帧日志与 TaskRunner 任务日志的完整方法【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttpOkHttp 在核心链路中埋入了基于java.util.loggingJULL的内部调试日志但没有对外暴露公开的开关。本篇技术指南将讲解如何用最少的代码启用 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 GCd. private val configuredLoggers CopyOnWriteArraySetLogger() 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) }从源码结构看有三个关键设计值得注意Logger 名称与内部类的绑定关系enableHttp2()等价于enable(Http2::class)即对名为okhttp3.internal.http2.Http2的 Logger 生效。这与内部实现完全对应——Http2Writer.kt 与 Http2Reader.kt 中使用的都是Logger.getLogger(Http2::class.java.name)也就是说读写两侧共享同一个 Logger 名称启用一次即可同时看到收发两个方向的帧。引用防回收configuredLoggers集合持有已配置 Logger 的强引用注释明确说明这是为了防止 JULL 配置被 GC 回收保证日志在进程生命周期内持续有效。可关闭的 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流 ID8 位十六进制。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 ConnectionPool、OkHttp 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这三条命令的作用setprop log.tag.okhttp.Http2 DEBUG把 logcat 标签okhttp.Http2的级别设为 DEBUG使该标签的 DEBUG 级日志放行setprop log.tag.okhttp.TaskRunner DEBUG同理开启任务调度日志adb logcat *:E okhttp.Http2:D okhttp.TaskRunner:D过滤其他标签只显示 E 级ERROR而将两个 OkHttp 标签放宽到 DDEBUG从而在默认较嘈杂的 logcat 中突出调试信息。仓库源码印证了标签名与启动时读取级别的机制。AndroidLog.kt 中维护了 FQN 到 logcat 标签的映射表private val knownLoggers LinkedHashMapString, 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 的调试日志是排查协议层问题的利器使用上有三个要点JVM 侧把 OkHttpDebugLogging.kt 复制到项目调用enableHttp2()/enableTaskRunner()打开对应 Loggerokhttp3.internal.http2.Http2与okhttp3.internal.concurrent.TaskRunner输出解读帧日志按direction streamID length type flags五列解析WINDOW_UPDATE第五列是窗口增量任务日志按Q编号 动作 时长: 队列名解析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),仅供参考