用过OkHttp的人都知道,使用HttpLoggingInterceptor
可以定位接口请求日志,来排查问题,但是多个接口并发请求(或者说一个页面多个接口并发请求)时,打印的日志有时候是错乱,原因是 HttpLoggingInterceptor.Logge
r中的log(String message)
是分段回调的。日志打印错乱导致定位问题极为不方便
修复这个问题也很简单,复制HttpLoggingInterceptor
一份类修改。
修改版本:
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
修改如下:
public final class HttpLoggingInterceptor2 implements Interceptor {
private static final Charset UTF8 = Charset.forName("UTF-8");
public enum Level {
/** No logs. */
NONE,
/**
* Logs request and response lines.
*
* Example:
*
{@code
* --> 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:
*
{@code
* --> 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:
*
{@code
* --> 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
}
public interface Logger {
/**
* 日志回调
* @param message 返回一个完整的日志信息
*/
void log(String message);
}
private static final String NEW_LINE="\n";
private void callbackLog(StringBuilder sb) {
logger.log(sb.toString());
sb.setLength(0);
}
public HttpLoggingInterceptor2(Logger logger) {
this.logger = logger;
}
private final Logger logger;
private volatile Level level = Level.NONE;
/** Change the level at which this interceptor logs. */
public HttpLoggingInterceptor2 setLevel(Level level) {
if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
this.level = level;
return this;
}
public Level getLevel() {
return level;
}
@Override public Response intercept(Chain chain) throws IOException {
Level level = this.level;
Request request = chain.request();
if (level == Level.NONE) {
return chain.proceed(request);
}
boolean logBody = level == Level.BODY;
boolean logHeaders = logBody || level == Level.HEADERS;
RequestBody requestBody = request.body();
boolean hasRequestBody = requestBody != null;
Connection connection = chain.connection();
String requestStartMessage = "--> "
+ request.method()
+ ' ' + request.url()
+ (connection != null ? " " + connection.protocol() : "");
if (!logHeaders && hasRequestBody) {
requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
}
StringBuilder sb = new StringBuilder();
sb.append(requestStartMessage).append(NEW_LINE);
if (logHeaders) {
if (hasRequestBody) {
// 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) {
sb.append("Content-Type: " + requestBody.contentType()).append(NEW_LINE);
}
if (requestBody.contentLength() != -1) {
sb.append("Content-Length: " + requestBody.contentLength()).append(NEW_LINE);
}
}
Headers headers = request.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
String name = headers.name(i);
// Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
sb.append(name + ": " + headers.value(i)).append(NEW_LINE);
}
}
if (!logBody || !hasRequestBody) {
sb.append("--> END " + request.method()).append(NEW_LINE);
} else if (bodyHasUnknownEncoding(request.headers())) {
sb.append("--> END " + request.method() + " (encoded body omitted)").append(NEW_LINE);
} else {
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
sb.append("");
if (isPlaintext(buffer)) {
String s = JsonUtils.formatJson(buffer.readString(charset));
sb.append(s).append(NEW_LINE);
sb.append("--> END " + request.method()
+ " (" + requestBody.contentLength() + "-byte body)").append(NEW_LINE);
} else {
sb.append("--> END " + request.method() + " (binary "
+ requestBody.contentLength() + "-byte body omitted)").append(NEW_LINE);
}
}
}
long startNs = System.nanoTime();
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
sb.append("<-- HTTP FAILED: " + e).append(NEW_LINE);
throw e;
}
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
sb.append("<-- "
+ response.code()
+ (response.message().isEmpty() ? "" : ' ' + response.message())
+ ' ' + response.request().url()
+ " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')').append(NEW_LINE);
if (logHeaders) {
Headers headers = response.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
sb.append(headers.name(i) + ": " + headers.value(i)).append(NEW_LINE);
}
if (!logBody || !HttpHeaders.hasBody(response)) {
sb.append("<-- END HTTP").append(NEW_LINE);;
callbackLog(sb);
} else if (bodyHasUnknownEncoding(response.headers())) {
sb.append("<-- END HTTP (encoded body omitted)").append(NEW_LINE);
callbackLog(sb);
} else {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
Long gzippedLength = null;
if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
gzippedLength = buffer.size();
GzipSource gzippedResponseBody = null;
try {
gzippedResponseBody = new GzipSource(buffer.clone());
buffer = new Buffer();
buffer.writeAll(gzippedResponseBody);
} finally {
if (gzippedResponseBody != null) {
gzippedResponseBody.close();
}
}
}
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (!isPlaintext(buffer)) {
sb.append("").append(NEW_LINE);
sb.append("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)").append(NEW_LINE);
callbackLog(sb);
return response;
}
if (contentLength != 0) {
sb.append("").append(NEW_LINE);
String s = buffer.clone().readString(charset);
sb.append(JsonUtils.formatJson(s)).append(NEW_LINE);
}
if (gzippedLength != null) {
sb.append("<-- END HTTP (" + buffer.size() + "-byte, "
+ gzippedLength + "-gzipped-byte body)").append(NEW_LINE);
} else {
sb.append("<-- END HTTP (" + buffer.size() + "-byte body)").append(NEW_LINE);
}
callbackLog(sb);
}
}
return response;
}
/**
* 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.
*/
static boolean isPlaintext(Buffer buffer) {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted()) {
break;
}
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false;
}
}
return true;
} catch (EOFException e) {
return false; // Truncated UTF-8 sequence.
}
}
private boolean bodyHasUnknownEncoding(Headers headers) {
String contentEncoding = headers.get("Content-Encoding");
return contentEncoding != null
&& !contentEncoding.equalsIgnoreCase("identity")
&& !contentEncoding.equalsIgnoreCase("gzip");
}
}
使用:
private static final class HttpLogger implements HttpLoggingInterceptor2.Logger {
@Override
public void log(String message) {
com.orhanobut.logger.Logger.i(message);// https://github.com/orhanobut/logger
}
}
OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (isDebug){
HttpLoggingInterceptor2 logInterceptor = new HttpLoggingInterceptor2(new HttpLogger());
logInterceptor.setLevel(HttpLoggingInterceptor2.Level.BODY);
builder.addNetworkInterceptor(logInterceptor); //添加网络日志拦截器
}
最后会发现一家人(日志)整整齐齐的打印出来,是不是很完美。
涉及的类:
public class JsonUtils {
/**
* 格式化json字符串
*
* @param jsonStr 需要格式化的json串
* @return 格式化后的json串
*/
public static String formatJson(String jsonStr) {
if (null == jsonStr || "".equals(jsonStr)) return "";
StringBuilder sb = new StringBuilder();
char last = '\0';
char current = '\0';
int indent = 0;
for (int i = 0; i < jsonStr.length(); i++) {
last = current;
current = jsonStr.charAt(i);
//遇到{ [换行,且下一行缩进
switch (current) {
case '{':
case '[':
sb.append(current);
sb.append('\n');
indent++;
addIndentBlank(sb, indent);
break;
//遇到} ]换行,当前行缩进
case '}':
case ']':
sb.append('\n');
indent--;
addIndentBlank(sb, indent);
sb.append(current);
break;
//遇到,换行
case ',':
sb.append(current);
if (last != '\\') {
sb.append('\n');
addIndentBlank(sb, indent);
}
break;
default:
sb.append(current);
}
}
return sb.toString();
}
/**
* 添加space
*
* @param sb
* @param indent
*/
private static void addIndentBlank(StringBuilder sb, int indent) {
for (int i = 0; i < indent; i++) {
sb.append('\t');
}
}
}