Browse Source

+ 日志记录器

drake 4 years ago
parent
commit
7fc9d07af0

+ 88 - 0
docs/log-recorder.md

@@ -0,0 +1,88 @@
+一般网络请求都会选择在LogCat打印网络日志信息, 但是AndroidStudio的LogCat超长文本会被切割, 甚至发生不完整的情况, 而且容易和其他日志掺杂, 导致可读性差.
+
+Net扩展`Okhttp Profiler`插件支持更好的日志拦截信息, 支持加密的请求和响应信息
+
+## 安装插件
+
+在插件市场搜索: "`Okhttp Profiler`"
+
+<img src="https://i.imgur.com/Pvncs1W.png" width="100%"/>
+
+### 初始化
+```kotlin hl_lines="3"
+initNet("http://182.92.97.186/") {
+    converter(JsonConvert()) // 转换器
+    setLogRecord(true) // 开启日志记录功能
+}
+```
+
+使用效果
+
+<img src="https://i.imgur.com/PJsaKpx.png" width="100%"/>
+
+| 标题 | 描述 |
+|-|-|
+| Device | 选择调试设备 |
+| Process | 选择展示记录的进程 |
+| <img src="https://i.imgur.com/bLXKLrI.png" width="20%"/> 抓取 | 一般情况不需要使用, 假设没有及时更新请点击图标 |
+| <img src="https://i.imgur.com/WG2WgBy.png" width="20%"/> 清空 | 清空记录 |
+
+
+### 自定义转换器
+
+假设你使用的是`DefaultConvert`或者没有使用转换器直接返回String则无需多余处理, 如果覆写或者直接实现的`Convert`, 请确保`result.logResponseBody`被赋值
+```kotlin hl_lines="16"
+@Suppress("UNCHECKED_CAST")
+abstract class DefaultConvert(
+    val success: String = "0",
+    val code: String = "code",
+    val message: String = "msg"
+) : Converter {
+
+    override fun <S, F> convert(
+        succeed: Type,
+        failed: Type,
+        request: Request,
+        response: Response,
+        result: Result<S, F>
+    ) {
+        val body = response.body().string()
+        result.logResponseBody = body // 将字符串响应赋值给result.logResponseBody
+        // .... 其他操作
+    }
+}
+```
+<br>
+
+!!! note
+    假设后端返回的加密数据, 可以为`result.logResponseBody`赋值解密后的字符串
+
+
+### 请求参数加密
+
+假设请求参数为加密后的字符串请在拦截器中为日志记录器赋值请求参数字符串
+
+```kotlin hl_lines="5"
+class NetInterceptor : Interceptor {
+    override fun intercept(chain: Chain): Response {
+        val request = chain.request()
+
+        request.logRequestBody("解密后的请求参数字符串")
+
+        return chain.proceed(request)
+    }
+}
+```
+
+
+## 扩展至其他请求框架
+
+可能你项目中还残留其他网络框架, 也可以使用Net的日志记录器`LogRecorder`来为其他框架打印日志信息
+
+| 函数 | 描述 |
+|-|-|
+| generateId | 产生一个唯一标识符, 用于判断为同一网络请求 |
+| recordRequest | 记录请求信息 |
+| recordResponse | 记录响应信息 |
+| recordException | 记录请求异常信息 |
+| recordDuration | 记录请求间隔时间 |

+ 0 - 6
kalle/src/main/java/com/yanzhenjie/kalle/Kalle.java

@@ -22,12 +22,6 @@ import com.yanzhenjie.kalle.download.UrlDownload;
 import com.yanzhenjie.kalle.simple.SimpleBodyRequest;
 import com.yanzhenjie.kalle.simple.SimpleUrlRequest;
 
-/**
- * <p>
- * Kalle.
- * </p>
- * Created in Jul 28, 2015 7:32:22 PM.
- */
 public final class Kalle {
 
     private static KalleConfig sConfig;

+ 9 - 0
kalle/src/main/java/com/yanzhenjie/kalle/KalleConfig.java

@@ -19,6 +19,7 @@ import com.yanzhenjie.kalle.connect.ConnectFactory;
 import com.yanzhenjie.kalle.connect.Interceptor;
 import com.yanzhenjie.kalle.connect.Network;
 import com.yanzhenjie.kalle.cookie.CookieStore;
+import com.yanzhenjie.kalle.recorder.LogRecorder;
 import com.yanzhenjie.kalle.simple.Converter;
 import com.yanzhenjie.kalle.simple.cache.CacheStore;
 import com.yanzhenjie.kalle.ssl.SSLUtils;
@@ -216,6 +217,14 @@ public final class KalleConfig {
             return this;
         }
 
+        /**
+         * 是否开启日志记录器
+         */
+        public Builder setLogRecord(boolean enabled) {
+            LogRecorder.INSTANCE.setLogEnabled(enabled);
+            return this;
+        }
+
         /**
          * Global charset.
          */

+ 42 - 2
kalle/src/main/java/com/yanzhenjie/kalle/Request.java

@@ -15,6 +15,8 @@
  */
 package com.yanzhenjie.kalle;
 
+import com.yanzhenjie.kalle.recorder.LogRecorder;
+
 import java.net.Proxy;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -29,7 +31,6 @@ public abstract class Request {
 
     private final RequestMethod mMethod;
     private final Headers mHeaders;
-
     private final Proxy mProxy;
     private final SSLSocketFactory mSSLSocketFactory;
     private final HostnameVerifier mHostnameVerifier;
@@ -37,13 +38,52 @@ public abstract class Request {
     private final int mReadTimeout;
     private final Object mTag;
     private final Object uid;
-
+    private String logId = LogRecorder.INSTANCE.generateId();
+    private String logRequestBody;
+    private long requestTime = System.currentTimeMillis();
     private String location;
 
+    /**
+     * 请求Id, 每个请求都有独一无二的Id
+     */
+    public String logId() {
+        return logId;
+    }
+
+    /**
+     * 请求开始时间
+     */
+    public long getRequestStartTime() {
+        return requestTime;
+    }
+
+    /**
+     * 请求体
+     */
+    public String logRequestBody() {
+        if (logRequestBody == null) {
+            return copyParams().toString();
+        } else return logRequestBody;
+    }
+
+    /**
+     * 设置一个请求体, 默认使用copyParams()
+     */
+    public void logRequestBody(String logRequestBody) {
+        this.logRequestBody = logRequestBody;
+    }
+
+    /**
+     * 一个表示请求的地址值, 因为某些接口可能存在无法通过Url来判断请求地址(例如通过参数区分接口), 这个时候可以通过location来为开发者标识
+     * 默认使用Request.url()
+     */
     public void location(String location) {
         this.location = location;
     }
 
+    /**
+     * 一个表示请求的地址值
+     */
     public String location() {
         if (location != null) {
             return location;

+ 181 - 0
kalle/src/main/java/com/yanzhenjie/kalle/recorder/LogRecorder.kt

@@ -0,0 +1,181 @@
+package com.yanzhenjie.kalle.recorder
+
+import android.annotation.SuppressLint
+import android.os.*
+import android.util.Log
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.concurrent.atomic.AtomicLong
+
+object LogRecorder {
+
+    var logEnabled = false;
+
+    private val handler: Handler
+        get() {
+            val handlerThread = HandlerThread("OkHttpProfiler", Process.THREAD_PRIORITY_BACKGROUND)
+            handlerThread.start()
+            return LogBodyHandler(handlerThread.looper)
+        }
+
+    private const val LOG_LENGTH = 4000
+    private const val SLOW_DOWN_PARTS_AFTER = 20
+    private const val LOG_PREFIX = "OKPRFL"
+    private const val DELIMITER = "_"
+    private const val HEADER_DELIMITER = ':'
+    private const val SPACE = ' '
+    private const val KEY_TAG = "TAG"
+    private const val KEY_VALUE = "VALUE"
+    private const val KEY_PARTS_COUNT = "PARTS_COUNT"
+    private val format: DateFormat = SimpleDateFormat("ddhhmmssSSS", Locale.US)
+    private val previousTime = AtomicLong()
+
+    /**
+     * 产生一个唯一的基于时间戳Id
+     */
+    @Synchronized
+    fun generateId(): String {
+        if (!logEnabled) return ""
+        var currentTime: Long = format.format(Date()).toLong()
+        var previousTime: Long = this.previousTime.get()
+        if (currentTime <= previousTime) {
+            currentTime = ++previousTime
+        }
+        this.previousTime.set(currentTime)
+        return currentTime.toString(Character.MAX_RADIX)
+    }
+
+    /**
+     * 发送请求信息到记录器中
+     *
+     * @param id 请求的唯一标识符
+     * @param url 请求URL地址
+     * @param method 请求方法
+     * @param headers 请求头
+     * @param body 请求体
+     */
+    fun recordRequest(id: String,
+                      url: String,
+                      method: String,
+                      headers: Map<String, List<String>>,
+                      body: String?) {
+        if (!logEnabled) return
+
+        fastLog(id, MessageType.REQUEST_METHOD, method)
+        fastLog(id, MessageType.REQUEST_URL, url)
+        fastLog(id, MessageType.REQUEST_TIME, System.currentTimeMillis().toString())
+
+        for ((key, value) in headers) {
+            var header = value.toString()
+            if (header.length > 2) header = header.substring(1, header.length - 1)
+            fastLog(id, MessageType.REQUEST_HEADER, key + HEADER_DELIMITER + SPACE + header)
+        }
+        if (body != null) {
+            largeLog(id, MessageType.REQUEST_BODY, body)
+        } else {
+            largeLog(id, MessageType.REQUEST_BODY, url)
+        }
+    }
+
+    /**
+     * 发送响应信息到记录器中
+     *
+     * @param id 请求的唯一标识符
+     * @param code 响应码
+     * @param headers 响应头
+     * @param body 响应体
+     */
+    fun recordResponse(id: String,
+                       code: String,
+                       headers: Map<String, List<String>>,
+                       body: String?) {
+        if (!logEnabled) return
+        largeLog(id, MessageType.RESPONSE_BODY, body)
+        logWithHandler(id, MessageType.RESPONSE_STATUS, code, 0)
+        for ((key, value) in headers) {
+            var header = value.toString()
+            if (header.length > 2) header = header.substring(1, header.length - 1)
+            logWithHandler(id, MessageType.RESPONSE_HEADER, key + HEADER_DELIMITER + header, 0)
+        }
+    }
+
+    /**
+     * 发送请求异常到记录器
+     *
+     * @param id 请求的唯一标识符
+     */
+    fun recordException(id: String, response: Exception) {
+        if (!logEnabled) return
+        logWithHandler(id, MessageType.RESPONSE_ERROR, response.localizedMessage, 0)
+    }
+
+    /**
+     * 发送请求到响应时间间隔
+     *
+     * @param id 请求的唯一标识符
+     * @param duration 间隔时间
+     */
+    fun recordDuration(id: String, duration: Long) {
+        if (!logEnabled) return
+        logWithHandler(id, MessageType.RESPONSE_TIME, duration.toString(), 0)
+        logWithHandler(id, MessageType.RESPONSE_END, "-->", 0)
+    }
+
+    @SuppressLint("LogNotTimber")
+    private fun fastLog(id: String, type: MessageType, message: String?) {
+        val tag = LOG_PREFIX + DELIMITER + id + DELIMITER + type.type
+        if (message != null) {
+            Log.v(tag, message)
+        }
+    }
+
+    private fun logWithHandler(id: String, type: MessageType, message: String?, partsCount: Int) {
+        val handlerMessage = handler.obtainMessage()
+        val tag = LOG_PREFIX + DELIMITER + id + DELIMITER + type.type
+        val bundle = Bundle()
+        bundle.putString(KEY_TAG, tag)
+        bundle.putString(KEY_VALUE, message)
+        bundle.putInt(KEY_PARTS_COUNT, partsCount)
+        handlerMessage.data = bundle
+        handler.sendMessage(handlerMessage)
+    }
+
+    private fun largeLog(id: String, type: MessageType, content: String?) {
+        val contentLength = content?.length ?: 0
+        if (contentLength > LOG_LENGTH) {
+            val parts = contentLength / LOG_LENGTH
+            for (i in 0..parts) {
+                val start = i * LOG_LENGTH
+                var end = start + LOG_LENGTH
+                if (end > contentLength) {
+                    end = contentLength
+                }
+                logWithHandler(id, type, content?.substring(start, end), parts)
+            }
+        } else {
+            logWithHandler(id, type, content, 0)
+        }
+    }
+
+    private class LogBodyHandler(looper: Looper) : Handler(looper) {
+        override fun handleMessage(msg: Message) {
+            val bundle = msg.data
+            if (bundle != null) {
+                val partsCount = bundle.getInt(KEY_PARTS_COUNT, 0)
+                if (partsCount > SLOW_DOWN_PARTS_AFTER) {
+                    try {
+                        Thread.sleep(5L)
+                    } catch (e: InterruptedException) {
+                        e.printStackTrace()
+                    }
+                }
+                val data = bundle.getString(KEY_VALUE)
+                val key = bundle.getString(KEY_TAG)
+                if (data != null && key != null) {
+                    Log.v(key, data)
+                }
+            }
+        }
+    }
+}

+ 17 - 0
kalle/src/main/java/com/yanzhenjie/kalle/recorder/MessageType.kt

@@ -0,0 +1,17 @@
+package com.yanzhenjie.kalle.recorder
+
+enum class MessageType(var type: String) {
+    REQUEST_URL("RQU"),
+    REQUEST_TIME("RQT"),
+    REQUEST_METHOD("RQM"),
+    REQUEST_HEADER("RQH"),
+    REQUEST_BODY("RQB"),
+    REQUEST_END("RQD"),
+    RESPONSE_TIME("RST"),
+    RESPONSE_STATUS("RSS"),
+    RESPONSE_HEADER("RSH"),
+    RESPONSE_BODY("RSB"),
+    RESPONSE_END("RSD"),
+    RESPONSE_ERROR("REE"),
+    UNKNOWN("UNKNOWN");
+}

+ 16 - 7
kalle/src/main/java/com/yanzhenjie/kalle/simple/BasicWorker.java

@@ -18,10 +18,12 @@ package com.yanzhenjie.kalle.simple;
 import com.yanzhenjie.kalle.Canceller;
 import com.yanzhenjie.kalle.Headers;
 import com.yanzhenjie.kalle.Kalle;
+import com.yanzhenjie.kalle.Request;
 import com.yanzhenjie.kalle.Response;
 import com.yanzhenjie.kalle.exception.NetException;
 import com.yanzhenjie.kalle.exception.NoCacheError;
 import com.yanzhenjie.kalle.exception.ParseError;
+import com.yanzhenjie.kalle.recorder.LogRecorder;
 import com.yanzhenjie.kalle.simple.cache.Cache;
 import com.yanzhenjie.kalle.simple.cache.CacheMode;
 import com.yanzhenjie.kalle.simple.cache.CacheStore;
@@ -62,8 +64,10 @@ abstract class BasicWorker<T extends SimpleRequest, Succeed, Failed>
         if (response != null) return buildSimpleResponse(response, true);
 
         tryAttachCache();
-
         try {
+            Request request = mRequest.request();
+            LogRecorder.INSTANCE.recordRequest(request.logId(), request.location(), request.method().toString(), request.headers().toMap(), request.logRequestBody());
+
             response = requestNetwork(mRequest);
 
             int code = response.code();
@@ -76,14 +80,13 @@ abstract class BasicWorker<T extends SimpleRequest, Succeed, Failed>
                 }
             }
             Headers headers = response.headers();
+
             byte[] body = {};
             if (code != 204) {
                 body = response.body().byteArray();
             }
             IOUtils.closeQuietly(response);
-
             tryDetachCache(code, headers, body);
-
             response = buildResponse(code, headers, body);
             return buildSimpleResponse(response, false);
         } catch (IOException e) {
@@ -91,6 +94,7 @@ abstract class BasicWorker<T extends SimpleRequest, Succeed, Failed>
             if (cacheResponse != null) {
                 return buildSimpleResponse(cacheResponse, true);
             }
+            LogRecorder.INSTANCE.recordException(mRequest.request().logId(), e);
             throw e;
         } finally {
             IOUtils.closeQuietly(response);
@@ -287,21 +291,26 @@ abstract class BasicWorker<T extends SimpleRequest, Succeed, Failed>
     }
 
     private Result<Succeed, Failed> buildSimpleResponse(Response response, boolean cache) throws IOException {
+        Request request = mRequest.request();
         try {
+            Result<Succeed, Failed> result = new Result<>();
 
-            Result<Succeed, Failed> result = new Result<>(response.code(), response.headers(), cache, null, null);
+            mConverter.convert(mSucceed, mFailed, request, response, result);
 
-            mConverter.convert(mSucceed, mFailed, mRequest.request(), response, result);
+            LogRecorder.INSTANCE.recordDuration(request.logId(), System.currentTimeMillis() - request.getRequestStartTime());
+            LogRecorder.INSTANCE.recordResponse(request.logId(), String.valueOf(response.code()), response.headers().toMap(), result.getLogResponseBody());
 
             if (result.getSuccess() == null && result.getFailure() == null) {
-                throw new ParseError(mRequest.request(), mConverter.getClass().getName() + " does not process result", null);
+                throw new ParseError(request, mConverter.getClass().getName() + " does not process result", null);
             }
 
             return result;
         } catch (NetException e) {
+            LogRecorder.INSTANCE.recordException(request.logId(), e);
             throw e;
         } catch (Exception e) {
-            throw new ParseError(mRequest.request(), "An exception occurred while parsing the data", e);
+            LogRecorder.INSTANCE.recordException(request.logId(), e);
+            throw new ParseError(request, "An exception occurred while parsing the data", e);
         }
     }
 }

+ 3 - 4
kalle/src/main/java/com/yanzhenjie/kalle/simple/Converter.kt

@@ -19,9 +19,6 @@ import com.yanzhenjie.kalle.Request
 import com.yanzhenjie.kalle.Response
 import java.lang.reflect.Type
 
-/**
- * Created by Zhenjie Yan on 2018/2/12.
- */
 @Suppress("UNCHECKED_CAST")
 interface Converter {
 
@@ -47,7 +44,9 @@ interface Converter {
                 result: Result<S, F>
             ) {
                 if (succeed === String::class.java) {
-                    result.success = response.body().string() as S
+                    val string = response.body().string()
+                    result.logResponseBody = string
+                    result.success = string as S
                 }
             }
         }

+ 2 - 7
kalle/src/main/java/com/yanzhenjie/kalle/simple/Result.kt

@@ -16,17 +16,12 @@
 
 package com.yanzhenjie.kalle.simple
 
-import com.yanzhenjie.kalle.Headers
-
 data class Result<S, F>(
-    var code: Int,
-    var headers: Headers,
-    var fromCache: Boolean,
+    var logResponseBody: String? = null,
     var success: S? = null,
     var failure: F? = null
-) {
+                       ) {
 
     val isSucceed
         get() = success != null || failure == null
-
 }

+ 1 - 0
mkdocs.yml

@@ -56,5 +56,6 @@ nav:
   - 嵌套作用域: nested-scope.md
   - 最快请求结果: fastest.md
   - 取消请求: cancel.md
+  - 日志记录器: log-recorder.md
   - 轮循器: interval.md
   - 2.x文档: api/net/index.md

+ 2 - 2
net/src/main/java/com/drake/net/convert/DefaultConvert.kt

@@ -51,15 +51,15 @@ abstract class DefaultConvert(
         result: Result<S, F>
     ) {
         val body = response.body().string()
+        result.logResponseBody = body // 日志记录响应信息
         val code = response.code()
 
         when {
             code in 200..299 -> { // 请求成功
                 val jsonObject = JSONObject(body) // 获取JSON中后端定义的错误码和错误信息
-
                 if (jsonObject.getString(this.code) == success) { // 对比后端自定义错误码
                     result.success =
-                        if (succeed === String::class.java) body as S else body.parseBody(succeed)
+                            if (succeed === String::class.java) body as S else body.parseBody(succeed)
                 } else { // 错误码匹配失败, 开始写入错误异常
                     result.failure =
                         ResponseException(code, jsonObject.getString(message), request) as F

+ 1 - 0
sample/src/main/java/com/drake/net/sample/base/App.kt

@@ -42,6 +42,7 @@ class App : Application() {
         initNet("http://182.92.97.186/") {
             converter(JsonConvert()) // 自动解析JSON映射到实体类中, 转换器分为全局和单例, 覆盖生效(拦截器允许多个)
             cacheEnabled()
+            setLogRecord(true)
         }
 
         // 初始化SmartRefreshLayout, 这是自动下拉刷新和上拉加载采用的第三方库  [https://github.com/scwang90/SmartRefreshLayout/tree/master] V2版本

+ 2 - 0
sample/src/main/java/com/drake/net/sample/callback/NetInterceptor.kt

@@ -27,6 +27,8 @@ class NetInterceptor : Interceptor {
     override fun intercept(chain: Chain): Response {
         val request = chain.request()
 
+        request.logRequestBody()
+
         val tag = request.tag() as TAG
 
         if (tag.contains(REQUEST)) {