在安卓开发项目中,需要排查请求 api 的问题,需要依赖 http 请求的日志,下面是自定义的 okhttp 请求日志拦截器,相信大家用得上
kotlin
import androidx.core.util.Supplier
import java.io.IOException
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets.UTF_8
import java.util.TreeSet
import java.util.concurrent.TimeUnit
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.internal.http.promisesBody
import okhttp3.internal.platform.Platform
import okio.Buffer
import okio.GzipSource
import java.io.EOFException
import java.nio.charset.StandardCharsets
import java.util.UUID
/**
* Author: IT 乐手
* Description:
*/
class CustomHttpLoggingInterceptor @JvmOverloads constructor(
) : Interceptor {
@Volatile private var headersToRedact = emptySet<String>()
var levelSupplier: Supplier<Level> ?= null
enum class Level {
/** No logs. */
NONE,
/**
* Logs request and response lines.
*
* Example:
* ```
* --> POST /greeting http/1.1 (3-byte body)
*
* <-- 200 OK (22ms, 6-byte body)
* ```
*/
BASIC,
/**
* Logs request and response lines and their respective headers.
*
* Example:
* ```
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
* <-- END HTTP
* ```
*/
HEADERS,
/**
* Logs request and response lines and their respective headers and bodies (if present).
*
* Example:
* ```
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
*
* Hi?
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
*
* Hello!
* <-- END HTTP
* ```
*/
BODY
}
fun interface Logger {
fun log(message: String)
companion object {
/** A [Logger] defaults output appropriate for the current platform. */
@JvmField
val DEFAULT: Logger = DefaultLogger()
private class DefaultLogger : Logger {
override fun log(message: String) {
Platform.get().log(message)
}
}
}
}
fun redactHeader(name: String) {
val newHeadersToRedact = TreeSet(String.CASE_INSENSITIVE_ORDER)
newHeadersToRedact += headersToRedact
newHeadersToRedact += name
headersToRedact = newHeadersToRedact
}
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val level = this.levelSupplier?.get() ?: Level.NONE
val request = chain.request()
if (level == Level.NONE) {
return chain.proceed(request)
}
val logBody = level == Level.BODY
val logHeaders = logBody || level == Level.HEADERS
val requestBody = request.body
val connection = chain.connection()
var requestStartMessage =
("--> ${request.method} ${request.url}${if (connection != null) " " + connection.protocol() else ""}")
if (!logHeaders && requestBody != null) {
requestStartMessage += " (${requestBody.contentLength()}-byte body)"
}
val sb = StringBuilder()
sb.append(requestStartMessage).append("\r\n")
var body = ""
if (logHeaders) {
val headers = request.headers
if (requestBody != null) {
// Request body headers are only present when installed as a network interceptor. When not
// already present, force them to be included (if available) so their values are known.
requestBody.contentType()?.let {
if (headers["Content-Type"] == null) {
sb.append("Content-Type: $it").append("\r\n")
}
}
if (requestBody.contentLength() != -1L) {
if (headers["Content-Length"] == null) {
sb.append("Content-Length: ${requestBody.contentLength()}").append("\r\n")
}
}
}
for (i in 0 until headers.size) {
logHeader(headers, i, sb)
}
if (!logBody || requestBody == null) {
sb.append("--> END ${request.method}").append("\r\n")
} else if (bodyHasUnknownEncoding(request.headers)) {
sb.append("--> END ${request.method} (encoded body omitted)").append("\r\n")
} else if (requestBody.isDuplex()) {
sb.append("--> END ${request.method} (duplex request body omitted)").append("\r\n")
} else if (requestBody.isOneShot()) {
sb.append("--> END ${request.method} (one-shot body omitted)").append("\r\n")
} else {
val buffer = Buffer()
requestBody.writeTo(buffer)
val contentType = requestBody.contentType()
val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
sb.append("\r\n")
if (buffer.isProbablyUtf8()) {
if (requestBody is MultipartBody) {
sb.append("---> End Multipart Body (${UUID.randomUUID()}) --->").append("\r\n")
} else {
body = buffer.readString(charset)
sb.append(body).append("\r\n")
sb.append("--> END ${request.method} (${requestBody.contentLength()}-byte body)").append("\r\n")
}
} else {
sb.append(
"--> END ${request.method} (binary ${requestBody.contentLength()}-byte body omitted)").append("\r\n")
}
}
}
AtotoLogger.i(sb.toString())
sb.setLength(0)
val startNs = System.nanoTime()
val response: Response
try {
response = chain.proceed(request)
} catch (e: Exception) {
sb.append("<-- HTTP FAILED: $e").append("\r\n")
AtotoLogger.i(sb.toString())
throw e
}
if (request.url.toString().endsWith(".apk")) {
// Avoid logging APK files to prevent log flooding
sb.append("<-- ${response.code} ${response.message} ${response.request.url} (APK file, body omitted)").append("\r\n")
AtotoLogger.i(sb.toString())
return response
}
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
val responseBody = response.body!!
val contentLength = responseBody.contentLength()
val bodySize = if (contentLength != -1L) "$contentLength-byte" else "unknown-length"
sb.append(
"<-- ${response.code}${if (response.message.isEmpty()) "" else ' ' + response.message} ${response.request.url} (${tookMs}ms${if (!logHeaders) ", $bodySize body" else ""})").append("\r\n")
var responseResult = ""
if (logHeaders) {
val headers = response.headers
for (i in 0 until headers.size) {
logHeader(headers, i, sb)
}
if (!logBody || !response.promisesBody()) {
sb.append("<-- END HTTP").append("\r\n")
} else if (bodyHasUnknownEncoding(response.headers)) {
sb.append("<-- END HTTP (encoded body omitted)").append("\r\n")
} else {
val source = responseBody.source()
source.request(Long.MAX_VALUE) // Buffer the entire body.
var buffer = source.buffer
var gzippedLength: Long? = null
if ("gzip".equals(headers["Content-Encoding"], ignoreCase = true)) {
gzippedLength = buffer.size
GzipSource(buffer.clone()).use { gzippedResponseBody ->
buffer = Buffer()
buffer.writeAll(gzippedResponseBody)
}
}
val contentType = responseBody.contentType()
val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
if (!buffer.isProbablyUtf8()) {
sb.append("\r\n")
sb.append("<-- END HTTP (binary ${buffer.size}-byte body omitted)").append("\r\n")
AtotoLogger.i(sb.toString())
return response
}
if (contentLength != 0L) {
responseResult = buffer.clone().readString(charset)
sb.append("\r\n")
sb.append(responseResult).append("\r\n")
}
if (gzippedLength != null) {
sb.append("<-- END HTTP (${buffer.size}-byte, $gzippedLength-gzipped-byte body)").append("\r\n")
} else {
sb.append("<-- END HTTP (${buffer.size}-byte body)").append("\r\n")
}
}
}
AtotoLogger.i(sb.toString())
return response
}
private fun logHeader(headers: Headers, i: Int, sb:StringBuilder) {
val value = if (headers.name(i) in headersToRedact) "██" else headers.value(i)
sb.append(headers.name(i) + ": " + value).append("\r\n")
}
private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
val contentEncoding = headers["Content-Encoding"] ?: return false
return !contentEncoding.equals("identity", ignoreCase = true) &&
!contentEncoding.equals("gzip", ignoreCase = true)
}
}
/**
* Returns true if the body in question probably contains human readable text. Uses a small
* sample of code points to detect unicode control characters commonly used in binary file
* signatures.
*/
internal fun Buffer.isProbablyUtf8(): Boolean {
try {
val prefix = Buffer()
val byteCount = size.coerceAtMost(64)
copyTo(prefix, 0, byteCount)
for (i in 0 until 16) {
if (prefix.exhausted()) {
break
}
val codePoint = prefix.readUtf8CodePoint()
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false
}
}
return true
} catch (_: EOFException) {
return false // Truncated UTF-8 sequence.
}
}