项目中有使用okhttp+logging-interceptor上传本地保存的log文件,
代码片段:
var logLevel = HttpLoggingInterceptor.Level.BODY //默认BODY级别
val builder = OkHttpClient.Builder()
//...略
.addInterceptor(
HttpLoggingInterceptor().setLevel(logLevel)
然后由于最开始只有一个log文件,日积月累,日志文件很大,有80M+的(这个已经优化成多个文件),
so第一次上传的时候HttpLoggingInterceptor里面报错了,查log是OutOfMemory,
也就是如下代码片段报错了,HttpLoggingInterceptor中直接读取所有80M+的内容
@Override
public Response intercept(Chain chain) throws IOException {
//...略
if (isPlaintext(buffer)) {
logger.log(buffer.readString(charset)); //打印参数内容时,直接读取全部内容,爆掉了
logger.log("--> END " + request.method()
+ " (" + requestBody.contentLength() + "-byte body)");
} else {
logger.log("--> END " + request.method() + " (binary "
+ requestBody.contentLength() + "-byte body omitted)");
}
}
这里没有加判断条件,也没有大小限制,显示是有问题的。
于是思路就重写日志打印拦截器,主要也就是读取以及打印这块地方:
后面查了官方issue,之前也有人遇到过这个问题,参考他们方案,加了2个全局变量requestBodyLogMax和responseBodyLogMax,设置最大打印的大小,超过了只截取部分内容打印。
设置临界值
val builder = OkHttpClient.Builder()
builder
.addInterceptor(
CommonHttpLoggingInterceptor().setLevel(logLevel).setRequestBodyLogMax(2000).setResponseBodyLogMax(
2000
)
)
CommonHttpLoggingInterceptor.java文件如下:
class CommonHttpLoggingInterceptor(val logger: Logger = Logger.DEFAULT) : Interceptor {
@Volatile
private var level = Level.NONE
@Volatile
private var requestBodyLogMax = LOG_LIMITATION_NONE
@Volatile
private var responseBodyLogMax = LOG_LIMITATION_NONE
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,
/**
* Logs request and response lines.
*
*
* Example:
* `--> POST /greeting http/1.1 (3-byte body)
* result...(part)
* <-- 200 OK (22ms, 6-byte body)
* result...(part)
* <-- END GET
`
*
*/
SIMPLE
}
interface Logger {
fun log(message: String)
companion object {
/** A [Logger] defaults output appropriate for the current platform. */
val DEFAULT: Logger = object : Logger {
override fun log(message: String) {
Platform.get().log(INFO, message, null)
}
}
}
}
/** Change the level at which this interceptor logs. */
fun setLevel(level: Level?): CommonHttpLoggingInterceptor {
if (level == null) throw NullPointerException("level == null. Use Level.NONE instead.")
this.level = level
return this
}
fun getLevel(): Level {
return level
}
/** Change the limitation of request body logs size. */
fun setRequestBodyLogMax(requestBodyLogMax: Long): CommonHttpLoggingInterceptor {
if (requestBodyLogMax < 0) {
this.requestBodyLogMax = LOG_LIMITATION_NONE
} else {
this.requestBodyLogMax = requestBodyLogMax
}
return this
}
/** Change the limitation of response body logs size. */
fun setResponseBodyLogMax(responseBodyLogMax: Long): CommonHttpLoggingInterceptor {
if (responseBodyLogMax < 0) {
this.responseBodyLogMax = LOG_LIMITATION_NONE
} else {
this.responseBodyLogMax = responseBodyLogMax
}
return this
}
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val level = this.level
val request = chain.request()
if (level == Level.NONE) {
return chain.proceed(request)
}
val logBody = level == Level.BODY
val logHeaders = logBody || level == Level.HEADERS
val logPartBody = level == Level.SIMPLE
val requestBody = request.body()
val hasRequestBody = requestBody != null
val connection = chain.connection()
var requestStartMessage = ("--> "
+ request.method()
+ ' '.toString() + request.url()
+ (connection?.protocol()?:""))
if (!logHeaders && hasRequestBody) {
requestStartMessage += " (${requestBody!!.contentLength()}-byte body)"
}
logger.log(requestStartMessage)
if (logHeaders) {
if (requestBody != null) {
// Request body headers are only present when installed as a network interceptor. Force
// them to be included (when available) so there values are known.
if (requestBody.contentType() != null) {
logger.log("Content-Type: ${requestBody.contentType()}")
}
if (requestBody.contentLength() != -1L) {
logger.log("Content-Length: ${requestBody.contentLength()}")
}
}
val headers = request.headers()
var i = 0
val count = headers.size()
while (i < count) {
val name = headers.name(i)
// Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equals(name, ignoreCase = true) && !"Content-Length".equals(
name,
ignoreCase = true
)
) {
logger.log(name + ": " + headers.value(i))
}
i++
}
}
if (!(logBody || logPartBody) || !hasRequestBody) {
logger.log("--> END " + request.method())
} else if (bodyHasUnknownEncoding(request.headers())) {
logger.log("--> END " + request.method() + " (encoded body omitted)")
} else {
val buffer = Buffer()
requestBody!!.writeTo(buffer)
var charset: Charset? = UTF8
val contentType = requestBody.contentType()
if (contentType != null) {
charset = contentType.charset(UTF8)
}
logger.log("")
if (isPlaintext(buffer)) {
if (buffer.size() <= requestBodyLogMax || requestBodyLogMax < 0) {
var reqBodyContent = buffer.readString(charset!!)
if (logPartBody && reqBodyContent.length > 200) {
reqBodyContent = reqBodyContent.shrink(200, '.')
}
logger.log(reqBodyContent)
} else {
logger.log(
"Too large to output logs. "
+ "Current limitation is $requestBodyLogMax"
)
}
logger.log("--> END ${request.method()} (${requestBody.contentLength()}-byte body)")
} else {
logger.log(
("--> END ${request.method()} (binary ${requestBody.contentLength()}-byte body omitted)")
)
}
}
val startNs = System.nanoTime()
val response: Response
try {
response = chain.proceed(request)
} catch (e: Exception) {
logger.log("<-- HTTP FAILED: $e")
throw e
}
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
val responseBody = response.body()
val contentLength = responseBody!!.contentLength()
val bodySize = if (contentLength != -1L) contentLength.toString() + "-byte" else "unknown-length"
logger.log(
("<-- "
+ response.code()
+ (if (response.message().isEmpty()) "" else ' ' + response.message())
+ ' '.toString() + response.request().url()
+ " (" + tookMs + "ms" + (if (!logHeaders && !logPartBody) ", $bodySize body" else "") + ')'.toString())
)
val headers = response.headers()
if (logHeaders) {
var i = 0
val count = headers.size()
while (i < count) {
logger.log(headers.name(i) + ": " + headers.value(i))
i++
}
}
if (!(logBody || logPartBody) || !HttpHeaders.hasBody(response)) {
logger.log("<-- END ${request.method()}")
} else if (bodyHasUnknownEncoding(response.headers())) {
logger.log("<-- END ${request.method()} (encoded body omitted)")
} else {
val source = responseBody.source()
source.request(java.lang.Long.MAX_VALUE) // Buffer the entire body.
var buffer = source.buffer()
var gzippedLength: Long? = null
if ("gzip".equals(headers.get("Content-Encoding") ?: "", ignoreCase = true)) {
gzippedLength = buffer.size()
var gzippedResponseBody: GzipSource? = null
try {
gzippedResponseBody = GzipSource(buffer.clone())
buffer = Buffer()
buffer.writeAll(gzippedResponseBody)
} finally {
if (gzippedResponseBody != null) {
gzippedResponseBody.close()
}
}
}
var charset: Charset? = UTF8
val contentType = responseBody.contentType()
if (contentType != null) {
charset = contentType.charset(UTF8)
}
if (!isPlaintext(buffer)) {
logger.log("")
logger.log("<-- END ${request.method()} (binary ${buffer.size()}-byte body omitted)")
return response
}
if (contentLength != 0L) {
if (buffer.size() <= responseBodyLogMax || responseBodyLogMax < 0) {
var resBodyContent = buffer.clone().readString(charset!!)
if (logPartBody && resBodyContent.length > 200) {
resBodyContent = resBodyContent.shrink(200, '.')
}
logger.log(resBodyContent)
} else {
logger.log(
("Too large to output logs. "
+ "Current limitation is $responseBodyLogMax")
)
}
}
if (gzippedLength != null) {
logger.log(
("<-- END ${request.method()} (${buffer.size()}-byte, "
+ gzippedLength + "-gzipped-byte body)")
)
} else {
logger.log("<-- END ${request.method()} (${buffer.size()}-byte body)")
}
}
return response
}
private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
val contentEncoding = headers.get("Content-Encoding")
return (contentEncoding != null
&& !contentEncoding.equals("identity", ignoreCase = true)
&& !contentEncoding.equals("gzip", ignoreCase = true))
}
companion object {
private val UTF8 = Charset.forName("UTF-8")
const val LOG_LIMITATION_NONE: Long = -1
/**
* 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 isPlaintext(buffer: Buffer): Boolean {
try {
val prefix = Buffer()
val byteCount = if (buffer.size() < 64) buffer.size() else 64
buffer.copyTo(prefix, 0, byteCount)
for (i in 0..15) {
if (prefix.exhausted()) {
break
}
val codePoint = prefix.readUtf8CodePoint()
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false
}
}
return true
} catch (e: EOFException) {
return false // Truncated UTF-8 sequence.
}
}
}
}