Okhttp 定制打印请求日志

在安卓开发项目中,需要排查请求 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.
    }
}
相关推荐
来之梦3 小时前
Android红包雨动画效果实现 - 可自定义的扩散范围动画组件
android
杨筱毅3 小时前
【Android】【JNI多线程】JNI多线程安全、问题、性能常见卡点
android·jni
散人10243 小时前
Android Service 的一个细节
android·service
安卓蓝牙Vincent3 小时前
《Android BLE ScanSettings 完全解析:从参数到实战》
android
江上清风山间明月3 小时前
LOCAL_STATIC_ANDROID_LIBRARIES的作用
android·静态库·static_android
三少爷的鞋4 小时前
Android 中 `runBlocking` 其实只有一种使用场景
android
应用市场6 小时前
PHP microtime()函数精度问题深度解析与解决方案
android·开发语言·php
沐怡旸8 小时前
【Android】Dalvik 对比 ART
android·面试
消失的旧时光-19438 小时前
Android NDK 完全学习指南:从入门到精通
android