1. 为什么需要OkHttp日志输出
在移动端开发中,网络请求是最基础也最频繁的操作之一。OkHttp作为目前Android平台上最流行的HTTP客户端库,几乎成为了现代Android应用的标配。但很多开发者在实际使用中经常会遇到这样的困扰:当网络请求出现问题时,我们往往只能看到一个笼统的错误码,却无法快速定位问题根源。
我曾在一次线上故障排查中深有体会——用户反馈某个接口总是返回空数据,但服务端日志显示请求一切正常。当时我们团队花了整整两天时间才最终定位到问题:原来是客户端在构建请求时错误地添加了一个空格字符。如果当时有完整的请求日志输出,这个问题可能在5分钟内就能解决。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. OkHttp日志拦截器实现方案
2.1 基础日志拦截器实现
OkHttp的拦截器机制是其最强大的特性之一。通过自定义Interceptor,我们可以轻松实现请求日志的输出。以下是一个最基本的日志拦截器实现:
kotlin复制class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// 记录请求日志
val requestLog = String.format(
"发送请求: %s %nHeaders: %s%nBody: %s",
request.url,
request.headers,
request.body?.toString() ?: "null"
)
Log.d("OkHttp", requestLog)
val response = chain.proceed(request)
// 记录响应日志
val responseLog = String.format(
"收到响应: %s %nCode: %d %nHeaders: %s%nBody: %s",
response.request.url,
response.code,
response.headers,
response.peekBody(1024 * 1024).string()
)
Log.d("OkHttp", responseLog)
return response
}
}
这个基础版本已经可以输出请求和响应的关键信息,但在实际项目中还需要考虑更多细节。
2.2 生产环境可用的增强版拦截器
在实际项目中,我们需要考虑更多因素:
- 性能优化:日志输出不应该影响主线程性能
- 敏感信息过滤:自动过滤Authorization等敏感头信息
- 格式化输出:使日志更易读
- 日志级别控制:区分开发和生产环境
以下是增强后的实现:
kotlin复制class EnhancedLoggingInterceptor(private val logger: Logger = DefaultLogger()) : Interceptor {
interface Logger {
fun log(message: String)
}
class DefaultLogger : Logger {
override fun log(message: String) {
if (BuildConfig.DEBUG) {
Log.d("OkHttp", message)
}
}
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// 记录请求开始时间
val startNs = System.nanoTime()
// 构建请求日志
val requestLog = buildString {
appendln("--> ${request.method} ${request.url}")
request.headers.forEach { header ->
if (!header.first.equals("Authorization", ignoreCase = true)) {
appendln("${header.first}: ${header.second}")
} else {
appendln("Authorization: ******")
}
}
appendln()
val requestBody = request.body
if (requestBody != null) {
val buffer = Buffer()
requestBody.writeTo(buffer)
appendln(buffer.readUtf8())
}
appendln("--> END ${request.method}")
}
logger.log(requestLog)
val response = chain.proceed(request)
// 计算请求耗时
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
// 构建响应日志
val responseLog = buildString {
val responseBody = response.body!!
val content = responseBody.peekBody(1024 * 1024).string()
appendln("<-- ${response.code} ${response.message} ${response.request.url} (${tookMs}ms)")
response.headers.forEach { header ->
appendln("${header.first}: ${header.second}")
}
appendln()
appendln(content)
appendln("<-- END HTTP")
}
logger.log(responseLog)
return response
}
}
3. 日志格式化与美化
3.1 使用第三方库简化日志输出
虽然我们可以自己实现完整的日志拦截器,但在实际项目中,使用成熟的第三方库往往更高效。以下是几个流行的选择:
-
HttpLoggingInterceptor:OkHttp官方提供的日志拦截器
gradle复制implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'使用示例:
kotlin复制val logging = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } val client = OkHttpClient.Builder() .addInterceptor(logging) .build() -
Chucker:不仅记录日志,还能在设备上查看网络请求历史
gradle复制debugImplementation "com.github.chuckerteam.chucker:library:3.5.2" releaseImplementation "com.github.chuckerteam.chucker:library-no-op:3.5.2"使用示例:
kotlin复制val client = OkHttpClient.Builder() .addInterceptor(ChuckerInterceptor(context)) .build()
3.2 自定义日志格式
如果你需要特定的日志格式,可以结合第三方日志库如Timber来实现:
kotlin复制class TimberLoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
Timber.tag("OkHttp").d("""
|--> ${request.method} ${request.url}
|Headers: ${request.headers}
|Body: ${request.body?.toString()}
|--> END ${request.method}
""".trimMargin())
val response = chain.proceed(request)
Timber.tag("OkHttp").d("""
|<-- ${response.code} ${response.message}
|Headers: ${response.headers}
|Body: ${response.peekBody(1024 * 1024).string()}
|<-- END HTTP
""".trimMargin())
return response
}
}
4. 高级日志处理技巧
4.1 日志级别动态控制
在生产环境中,我们可能需要根据不同情况动态调整日志级别:
kotlin复制class DynamicLoggingInterceptor : Interceptor {
var level: Level = Level.NONE
enum class Level {
NONE,
BASIC,
HEADERS,
BODY
}
override fun intercept(chain: Interceptor.Chain): Response {
if (level == Level.NONE) {
return chain.proceed(chain.request())
}
val request = chain.request()
// 请求日志处理...
val response = chain.proceed(request)
// 响应日志处理...
return response
}
}
4.2 敏感信息自动脱敏
对于包含敏感信息的请求,我们需要自动脱敏:
kotlin复制class SensitiveDataFilterInterceptor : Interceptor {
private val sensitiveKeys = listOf(
"password",
"token",
"authorization",
"credit_card"
)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val filteredHeaders = request.headers.mapValues { (key, value) ->
if (sensitiveKeys.any { key.contains(it, ignoreCase = true) }) {
"******"
} else {
value
}
}
val filteredRequest = request.newBuilder()
.headers(Headers.Builder().apply {
filteredHeaders.forEach { (name, value) ->
add(name, value)
}
}.build())
.build()
return chain.proceed(filteredRequest)
}
}
4.3 日志持久化与上报
对于生产环境,我们可能需要将关键请求日志持久化并上报到服务器:
kotlin复制class PersistentLoggingInterceptor(
private val storage: LogStorage
) : Interceptor {
interface LogStorage {
fun saveLog(log: NetworkLog)
}
data class NetworkLog(
val url: String,
val method: String,
val requestHeaders: Map<String, String>,
val requestBody: String?,
val responseCode: Int,
val responseHeaders: Map<String, String>,
val responseBody: String?,
val timestamp: Long,
val duration: Long
)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val startNs = System.nanoTime()
// 记录请求信息...
val response = chain.proceed(request)
// 记录响应信息...
val log = NetworkLog(
url = request.url.toString(),
method = request.method,
requestHeaders = request.headers.toMap(),
requestBody = request.body?.toString(),
responseCode = response.code,
responseHeaders = response.headers.toMap(),
responseBody = response.peekBody(1024 * 1024).string(),
timestamp = System.currentTimeMillis(),
duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
)
storage.saveLog(log)
return response
}
}
5. 常见问题与解决方案
5.1 日志输出导致应用卡顿
问题现象:添加日志拦截器后,应用在发起网络请求时出现明显卡顿。
原因分析:日志输出特别是响应体的处理(如peekBody)是比较耗时的操作,如果在主线程执行会导致UI卡顿。
解决方案:
- 确保日志拦截器是作为NetworkInterceptor添加的,而不是Application Interceptor
- 将日志处理移到后台线程
kotlin复制class AsyncLoggingInterceptor : Interceptor {
private val executor = Executors.newSingleThreadExecutor()
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val startNs = System.nanoTime()
val response = chain.proceed(request)
executor.execute {
// 在这里处理日志输出
val responseBody = response.peekBody(1024 * 1024).string()
Log.d("OkHttp", "Async log: $responseBody")
}
return response
}
}
5.2 大文件上传/下载时的日志问题
问题现象:当上传或下载大文件时,日志拦截器可能导致内存溢出。
原因分析:默认情况下,日志拦截器会尝试读取整个请求体或响应体,对于大文件来说这会消耗大量内存。
解决方案:
- 对于大文件请求,跳过body日志记录
- 或者只记录前几KB数据
kotlin复制override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// 检查是否是文件上传/下载
val isLargeFile = request.body?.contentType()?.toString()?.contains("octet-stream") == true
|| request.header("Content-Length")?.toLongOrNull() ?: 0 > 1024 * 1024
if (isLargeFile) {
Log.d("OkHttp", "Large file request detected, skipping body logging")
return chain.proceed(request)
}
// 正常处理日志...
}
5.3 日志中缺少某些关键信息
问题现象:日志中缺少如DNS解析时间、SSL握手时间等关键性能指标。
解决方案:使用OkHttp的事件监听器(EventListener)来获取更详细的网络事件:
kotlin复制class TimingEventListener : EventListener() {
private var startNs: Long = 0
override fun callStart(call: Call) {
startNs = System.nanoTime()
}
override fun dnsStart(call: Call, domainName: String) {
Log.d("OkHttp", "DNS lookup started for $domainName")
}
override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {
val durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
Log.d("OkHttp", "DNS lookup completed in $durationMs ms")
}
// 其他事件回调...
}
// 使用方式
val client = OkHttpClient.Builder()
.eventListenerFactory(TimingEventListener.Factory())
.build()
6. 性能优化建议
6.1 生产环境日志策略
在生产环境中,建议采用以下策略平衡日志需求和性能:
- 采样记录:只记录部分请求(如1%的请求)
- 错误优先:只记录失败的请求
- 关键路径记录:只记录业务关键路径的请求
- 动态控制:通过远程配置动态调整日志级别
kotlin复制class SmartLoggingInterceptor : Interceptor {
private var sampleRate = 0.01 // 1%采样率
private var logErrorsOnly = true
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val shouldLog = when {
!logErrorsOnly && Math.random() < sampleRate -> true
else -> false
}
if (!shouldLog) {
return chain.proceed(request)
}
// 记录请求日志...
val response = chain.proceed(request)
// 记录响应日志...
return response
}
fun updateConfig(sampleRate: Double, logErrorsOnly: Boolean) {
this.sampleRate = sampleRate
this.logErrorsOnly = logErrorsOnly
}
}
6.2 日志压缩与批量上报
对于需要持久化和上报的日志,建议:
- 压缩日志:使用GZIP等算法压缩日志数据
- 批量上报:积累一定量后批量发送,减少网络请求
- 本地缓存:在网络不可用时缓存日志,待网络恢复后上报
kotlin复制class CompressedLogStorage : PersistentLoggingInterceptor.LogStorage {
private val gzip = GZIPOutputStream(FileOutputStream("logs.gz"))
override fun saveLog(log: PersistentLoggingInterceptor.NetworkLog) {
val json = Json.encodeToString(log)
gzip.write(json.toByteArray())
gzip.write("\n".toByteArray())
}
fun flush() {
gzip.flush()
}
fun close() {
gzip.close()
}
}
7. 测试与验证
7.1 单元测试日志拦截器
为日志拦截器编写单元测试非常重要:
kotlin复制class LoggingInterceptorTest {
@Test
fun `test request logging`() {
val logger = TestLogger()
val interceptor = EnhancedLoggingInterceptor(logger)
val client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
val request = Request.Builder()
.url("https://example.com/api")
.get()
.build()
client.newCall(request).execute()
assertTrue(logger.logs.any { it.contains("发送请求") })
assertTrue(logger.logs.any { it.contains("收到响应") })
}
class TestLogger : EnhancedLoggingInterceptor.Logger {
val logs = mutableListOf<String>()
override fun log(message: String) {
logs.add(message)
}
}
}
7.2 性能基准测试
使用Android Benchmark库测试日志拦截器的性能影响:
kotlin复制@RunWith(AndroidJUnit4::class)
class LoggingInterceptorBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun benchmarkRequestWithLogging() {
val client = OkHttpClient.Builder()
.addInterceptor(EnhancedLoggingInterceptor())
.build()
benchmarkRule.measureRepeated {
val request = Request.Builder()
.url("https://example.com/api")
.get()
.build()
client.newCall(request).execute().close()
}
}
}
8. 最佳实践总结
经过多个项目的实践验证,以下是我总结的OkHttp日志最佳实践:
-
分层记录:
- 开发环境:记录完整请求/响应
- 测试环境:记录关键信息
- 生产环境:只记录错误和采样数据
-
敏感信息处理:
- 自动识别并脱敏敏感字段
- 不要记录认证头信息
- 对于包含敏感数据的响应,考虑使用占位符替代
-
性能优化:
- 对于大文件请求,跳过body记录
- 使用异步方式处理日志
- 限制单个日志条目的大小
-
日志管理:
- 使用远程配置动态控制日志级别
- 实现日志轮转和自动清理
- 对于关键业务请求,实现端到端的请求追踪
-
异常处理:
- 确保日志记录过程中的异常不会影响正常请求流程
- 对于日志存储失败的情况,要有降级方案
- 实现日志系统的健康检查
在实际项目中,我通常会创建一个OkHttpClient的工厂类,集中管理所有与日志相关的配置:
kotlin复制object HttpClientFactory {
fun create(
context: Context,
isDebug: Boolean = BuildConfig.DEBUG
): OkHttpClient {
return OkHttpClient.Builder().apply {
if (isDebug) {
addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
addInterceptor(ChuckerInterceptor(context))
} else {
addInterceptor(SmartLoggingInterceptor().apply {
updateConfig(sampleRate = 0.01, logErrorsOnly = true)
})
}
addInterceptor(SensitiveDataFilterInterceptor())
eventListenerFactory(TimingEventListener.Factory())
}.build()
}
}
这种集中管理的方式使得日志策略可以全局一致,也便于后续调整和维护。
