diff --git a/.gitignore b/.gitignore index 4e40dea27..bd1e3ef25 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ local.properties build captures .externalNativeBuild -.idea/ \ No newline at end of file +.idea/ +.kotlin diff --git a/README.md b/README.md index 1c933e2ad..1e2108610 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ allprojects { } dependencies { - implementation('com.github.ihsanbal:LoggingInterceptor:3.1.0') { - exclude group: 'org.json', module: 'json' - } + implementation('com.github.ihsanbal:LoggingInterceptor:3.1.0') { + exclude group: 'org.json', module: 'json' + } } ``` @@ -55,13 +55,51 @@ allprojects { dependencies { - implementation("com.github.ihsanbal:LoggingInterceptor:3.1.0") { - exclude(group = "org.json", module = "json") - } + implementation("com.github.ihsanbal:LoggingInterceptor:3.1.0") { + exclude(group = "org.json", module = "json") + } } ``` +## Batching and custom sinks (fork feature) + +This fork adds a `sink(...)` API so you can batch a whole request/response block before logging +to avoid interleaving in Logcat. Example (using the bundled `BatchingSink`, now public): + +```kotlin +val sink = BatchingSink(LogSink { type, tag, message -> + // Logcat truncates ~4k per line; forward to your own chunker if needed + Log.println(type, tag, message) +}) + +val client = OkHttpClient.Builder() + .addInterceptor( + LoggingInterceptor.Builder() + .setLevel(Level.BODY) + .log(Log.DEBUG) + .sink(sink) + .build() + ) + .build() + +If you need chunking/queuing (e.g., Logcat 4k limit), wrap the `LogSink` to your own queue before passing to `BatchingSink`, similar to the sample above. +``` + +If you want the forked artifact via JitPack: + +```kotlin +allprojects { + repositories { maven { setUrl("https://jitpack.io") } } +} + +dependencies { + implementation("com.github.rtsketo:LoggingInterceptor:3.1.0rt6") { + exclude(group = "org.json", module = "json") + } +} +``` + Maven: ```xml diff --git a/app/build.gradle b/app/build.gradle index 09b98e21c..b75cbbfa1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.application' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { @@ -67,4 +67,4 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:rules:1.2.0' -} \ No newline at end of file +} diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index aee44e138..1551d5964 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/build.gradle b/build.gradle index cedcb870d..b204d0ac6 100644 --- a/build.gradle +++ b/build.gradle @@ -26,9 +26,9 @@ buildscript { } project.ext { - groupId = 'com.github.ihsanbal' - artifactId = 'LoggingInterceptor' - snapshot = '3.1.0-rc2' + groupId = project.findProperty("group") ?: 'com.github.ihsanbal' + artifactId = project.findProperty("artifactId") ?: 'LoggingInterceptor' + snapshot = project.findProperty("version") ?: '3.1.0-rc2' } allprojects { diff --git a/images/logcat.png b/images/logcat.png index 40dcd8201..d72ea1faa 100644 Binary files a/images/logcat.png and b/images/logcat.png differ diff --git a/images/screen_shot_1.png b/images/screen_shot_1.png index 8a6e99d49..f08aafcf4 100644 Binary files a/images/screen_shot_1.png and b/images/screen_shot_1.png differ diff --git a/images/screen_shot_2.png b/images/screen_shot_2.png index 8adf934c0..6331a9552 100644 Binary files a/images/screen_shot_2.png and b/images/screen_shot_2.png differ diff --git a/images/screen_shot_4.png b/images/screen_shot_4.png index 4556f84c3..cbdc92b89 100644 Binary files a/images/screen_shot_4.png and b/images/screen_shot_4.png differ diff --git a/images/screen_shot_5.png b/images/screen_shot_5.png index b4aad3446..a0a6f60a4 100644 Binary files a/images/screen_shot_5.png and b/images/screen_shot_5.png differ diff --git a/lib/.gitignore b/lib/.gitignore index 796b96d1c..908c292cc 100644 --- a/lib/.gitignore +++ b/lib/.gitignore @@ -1 +1,2 @@ /build +.kotlin diff --git a/lib/build.gradle b/lib/build.gradle index de5bbdc3c..0c69c2b90 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -16,4 +16,4 @@ dependencies { implementation group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: okhttpVersion } -apply from: '../pTML.gradle' \ No newline at end of file +apply from: '../pTML.gradle' diff --git a/lib/src/main/java/com/ihsanbal/logging/BatchingSink.kt b/lib/src/main/java/com/ihsanbal/logging/BatchingSink.kt new file mode 100644 index 000000000..7bc28b3cb --- /dev/null +++ b/lib/src/main/java/com/ihsanbal/logging/BatchingSink.kt @@ -0,0 +1,29 @@ +package com.ihsanbal.logging + +import java.util.concurrent.ConcurrentHashMap + +/** + * Batches all lines for a request/response and flushes them as one block. + */ +class BatchingSink( + private val delegate: LogSink +) : LogSink { + + private data class BufferKey(val tag: String) + + private val buffers = ConcurrentHashMap() + + override fun log(type: Int, tag: String, message: String) { + val key = BufferKey(tag) + val buffer = buffers.getOrPut(key) { StringBuilder() } + if (buffer.isNotEmpty()) buffer.append('\n') + buffer.append(message) + } + + override fun close(type: Int, tag: String) { + val key = BufferKey(tag) + buffers.remove(key)?.let { block -> + delegate.log(type, tag, block.toString()) + } + } +} diff --git a/lib/src/main/java/com/ihsanbal/logging/I.kt b/lib/src/main/java/com/ihsanbal/logging/I.kt index b85374fdc..3e39770e7 100644 --- a/lib/src/main/java/com/ihsanbal/logging/I.kt +++ b/lib/src/main/java/com/ihsanbal/logging/I.kt @@ -1,6 +1,5 @@ package com.ihsanbal.logging -import okhttp3.internal.platform.Platform.Companion.INFO import java.util.logging.Level import java.util.logging.Logger @@ -11,15 +10,46 @@ internal open class I protected constructor() { companion object { private val prefix = arrayOf(". ", " .") private var index = 0 - fun log(type: Int, tag: String, msg: String?, isLogHackEnable: Boolean) { + + fun log(type: Int, tag: String, msg: String?, isLogHackEnable: Boolean, sink: LogSink? = null) { + if (sink != null) { + sink.log(type, tag, msg ?: "") + return + } + val finalTag = getFinalTag(tag, isLogHackEnable) - val logger = Logger.getLogger(if (isLogHackEnable) finalTag else tag) - when (type) { - INFO -> logger.log(Level.INFO, msg) - else -> logger.log(Level.WARNING, msg) + + if (!logWithAndroid(type, finalTag, msg)) { + val logger = Logger.getLogger(if (isLogHackEnable) finalTag else tag) + logger.log(mapJavaLevel(type), msg) } } + private fun logWithAndroid(type: Int, tag: String, msg: String?): Boolean { + return try { + val logClass = Class.forName("android.util.Log") + val printlnMethod = logClass.getMethod( + "println", + Int::class.javaPrimitiveType, + String::class.java, + String::class.java + ) + printlnMethod.invoke(null, type, tag, msg ?: "") + true + } catch (_: Throwable) { + false + } + } + + private fun mapJavaLevel(type: Int): Level = + when (type) { + 2, 3 -> Level.FINE // VERBOSE/DEBUG + 4 -> Level.INFO + 5 -> Level.WARNING + 6, 7, 8, 9 -> Level.SEVERE + else -> Level.INFO + } + private fun getFinalTag(tag: String, isLogHackEnable: Boolean): String { return if (isLogHackEnable) { index = index xor 1 @@ -33,4 +63,4 @@ internal open class I protected constructor() { init { throw UnsupportedOperationException() } -} \ No newline at end of file +} diff --git a/lib/src/main/java/com/ihsanbal/logging/LogSink.kt b/lib/src/main/java/com/ihsanbal/logging/LogSink.kt new file mode 100644 index 000000000..8c9a5aefd --- /dev/null +++ b/lib/src/main/java/com/ihsanbal/logging/LogSink.kt @@ -0,0 +1,6 @@ +package com.ihsanbal.logging + +interface LogSink { + fun log(type: Int, tag: String, message: String) + fun close(type: Int, tag: String) {} +} diff --git a/lib/src/main/java/com/ihsanbal/logging/LoggingInterceptor.kt b/lib/src/main/java/com/ihsanbal/logging/LoggingInterceptor.kt index b458c7ed2..76f1e1e19 100644 --- a/lib/src/main/java/com/ihsanbal/logging/LoggingInterceptor.kt +++ b/lib/src/main/java/com/ihsanbal/logging/LoggingInterceptor.kt @@ -1,9 +1,12 @@ package com.ihsanbal.logging -import okhttp3.* +import okhttp3.HttpUrl +import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody -import okhttp3.internal.platform.Platform.Companion.INFO import java.util.* import java.util.concurrent.Executor import java.util.concurrent.TimeUnit @@ -95,7 +98,7 @@ class LoggingInterceptor private constructor(private val builder: Builder) : Int var isLogHackEnable = false private set var isDebugAble = false - var type: Int = INFO + var type: Int = DEFAULT_LOG_TYPE private set private var requestTag: String? = null private var responseTag: String? = null @@ -103,6 +106,7 @@ class LoggingInterceptor private constructor(private val builder: Builder) : Int private set var logger: Logger? = null private set + var sink: LogSink? = null var isMockEnabled = false var sleepMs: Long = 0 var listener: BufferListener? = null @@ -250,12 +254,22 @@ class LoggingInterceptor private constructor(private val builder: Builder) : Int return this } + /** + * Override the default line logger with a sink. If both logger and sink + * are set, sink takes precedence. + */ + fun sink(sink: LogSink): Builder { + this.sink = sink + return this + } + fun build(): LoggingInterceptor { return LoggingInterceptor(this) } companion object { + private const val DEFAULT_LOG_TYPE = 4 // android.util.Log.INFO compatible private var TAG = "LoggingI" } } -} \ No newline at end of file +} diff --git a/lib/src/main/java/com/ihsanbal/logging/Printer.kt b/lib/src/main/java/com/ihsanbal/logging/Printer.kt index 5de884a77..62c4bddc2 100644 --- a/lib/src/main/java/com/ihsanbal/logging/Printer.kt +++ b/lib/src/main/java/com/ihsanbal/logging/Printer.kt @@ -31,7 +31,7 @@ class Printer private constructor() { private const val URL_TAG = "URL: " private const val METHOD_TAG = "Method: @" private const val HEADERS_TAG = "Headers:" - private const val STATUS_CODE_TAG = "Status Code: " + private const val STATUS_LINE_TAG = "Status Code: " private const val RECEIVED_TAG = "Received in: " private const val DEFAULT_LINE = "│ " private val OOM_OMITTED = LINE_SEPARATOR + "Output omitted because of Object size." @@ -44,34 +44,32 @@ class Printer private constructor() { LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + bodyToString(body, header) } ?: "" val tag = builder.getTag(true) - if (builder.logger == null) I.log(builder.type, tag, REQUEST_UP_LINE, builder.isLogHackEnable) - logLines(builder.type, tag, arrayOf(URL_TAG + url), builder.logger, false, builder.isLogHackEnable) - logLines(builder.type, tag, getRequest(builder.level, header, method), builder.logger, true, builder.isLogHackEnable) + val sink = builder.sink + emit(builder, tag, REQUEST_UP_LINE) + logLines(builder.type, tag, arrayOf(URL_TAG + url), builder.logger, false, builder.isLogHackEnable, sink) + logLines(builder.type, tag, getRequest(builder.level, header, method), builder.logger, true, builder.isLogHackEnable, sink) if (builder.level == Level.BASIC || builder.level == Level.BODY) { - logLines(builder.type, tag, requestBody.split(LINE_SEPARATOR).toTypedArray(), builder.logger, true, builder.isLogHackEnable) + logLines(builder.type, tag, requestBody.split(LINE_SEPARATOR).toTypedArray(), builder.logger, true, builder.isLogHackEnable, sink) } - if (builder.logger == null) I.log(builder.type, tag, END_LINE, builder.isLogHackEnable) + emit(builder, tag, END_LINE) + sink?.close(builder.type, tag) } fun printJsonResponse(builder: LoggingInterceptor.Builder, chainMs: Long, isSuccessful: Boolean, code: Int, headers: Headers, response: Response, segments: List, message: String, responseUrl: String) { val responseBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + getResponseBody(response) val tag = builder.getTag(false) - val urlLine = arrayOf(URL_TAG + responseUrl, N) - val responseString = getResponse(headers, chainMs, code, isSuccessful, - builder.level, segments, message) - if (builder.logger == null) { - I.log(builder.type, tag, RESPONSE_UP_LINE, builder.isLogHackEnable) - } - logLines(builder.type, tag, urlLine, builder.logger, true, builder.isLogHackEnable) - logLines(builder.type, tag, responseString, builder.logger, true, builder.isLogHackEnable) + val statusLine = getStatusLine(chainMs, code, message) + val sink = builder.sink + emit(builder, tag, RESPONSE_UP_LINE) + logLines(builder.type, tag, arrayOf(URL_TAG + responseUrl), builder.logger, false, builder.isLogHackEnable, sink) + logLines(builder.type, tag, statusLine, builder.logger, true, builder.isLogHackEnable, sink) if (builder.level == Level.BASIC || builder.level == Level.BODY) { logLines(builder.type, tag, responseBody.split(LINE_SEPARATOR).toTypedArray(), builder.logger, - true, builder.isLogHackEnable) - } - if (builder.logger == null) { - I.log(builder.type, tag, END_LINE, builder.isLogHackEnable) + true, builder.isLogHackEnable, sink) } + emit(builder, tag, END_LINE) + sink?.close(builder.type, tag) } private fun getResponseBody(response: Response): String { @@ -124,20 +122,19 @@ class Printer private constructor() { return log.split(LINE_SEPARATOR).toTypedArray() } - private fun getResponse(headers: Headers, tookMs: Long, code: Int, isSuccessful: Boolean, - level: Level, segments: List, message: String): Array { - val log: String - val loggableHeader = level == Level.HEADERS || level == Level.BASIC - val segmentString = slashSegments(segments) - log = ((if (segmentString.isNotEmpty()) "$segmentString - " else "") + "[is success : " - + isSuccessful + "] - " + RECEIVED_TAG + tookMs + "ms" + DOUBLE_SEPARATOR + STATUS_CODE_TAG + - code + " / " + message + DOUBLE_SEPARATOR + when { - isEmpty("$headers") -> "" - loggableHeader -> HEADERS_TAG + LINE_SEPARATOR + - dotHeaders(headers) - else -> "" - }) - return log.split(LINE_SEPARATOR).toTypedArray() + private fun getStatusLine(tookMs: Long, code: Int, message: String): Array { + val status = "$STATUS_LINE_TAG$code / $message ($RECEIVED_TAG$tookMs ms)" + return arrayOf(status) + } + + private fun emit(builder: LoggingInterceptor.Builder, tag: String, line: String) { + val sink = builder.sink + val logger = builder.logger + when { + sink != null -> sink.log(builder.type, tag, line) + logger == null -> I.log(builder.type, tag, line, builder.isLogHackEnable) + else -> logger.log(builder.type, tag, line) + } } private fun slashSegments(segments: List): String { @@ -157,7 +154,7 @@ class Printer private constructor() { } private fun logLines(type: Int, tag: String, lines: Array, logger: Logger?, - withLineSize: Boolean, useLogHack: Boolean) { + withLineSize: Boolean, useLogHack: Boolean, sink: LogSink? = null) { for (line in lines) { val lineLength = line.length val maxLogSize = if (withLineSize) 110 else lineLength @@ -165,10 +162,11 @@ class Printer private constructor() { val start = i * maxLogSize var end = (i + 1) * maxLogSize end = if (end > line.length) line.length else end - if (logger == null) { - I.log(type, tag, DEFAULT_LINE + line.substring(start, end), useLogHack) - } else { - logger.log(type, tag, line.substring(start, end)) + val chunk = DEFAULT_LINE + line.substring(start, end) + when { + sink != null -> sink.log(type, tag, chunk) + logger == null -> I.log(type, tag, chunk, useLogHack) + else -> logger.log(type, tag, chunk) } } } @@ -271,4 +269,4 @@ internal fun Buffer.isProbablyUtf8(): Boolean { } catch (_: EOFException) { return false // Truncated UTF-8 sequence. } -} \ No newline at end of file +}