mirror of
https://github.com/owncloud/android-library.git
synced 2025-06-07 07:56:19 +00:00
Merge pull request #345 from owncloud/add_network_logs
Log network calls
This commit is contained in:
commit
61937825b8
@ -57,6 +57,7 @@ public class HttpClient {
|
||||
private static OkHttpClient sOkHttpClient;
|
||||
private static Context sContext;
|
||||
private static HashMap<String, List<Cookie>> sCookieStore = new HashMap<>();
|
||||
private static LogInterceptor sLogInterceptor;
|
||||
|
||||
public static OkHttpClient getOkHttpClient() {
|
||||
if (sOkHttpClient == null) {
|
||||
@ -110,6 +111,7 @@ public class HttpClient {
|
||||
};
|
||||
|
||||
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(getLogInterceptor())
|
||||
.protocols(Arrays.asList(Protocol.HTTP_1_1))
|
||||
.readTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
@ -120,6 +122,7 @@ public class HttpClient {
|
||||
.cookieJar(cookieJar);
|
||||
// TODO: Not verifying the hostname against certificate. ask owncloud security human if this is ok.
|
||||
//.hostnameVerifier(new BrowserCompatHostnameVerifier());
|
||||
|
||||
sOkHttpClient = clientBuilder.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
@ -137,6 +140,13 @@ public class HttpClient {
|
||||
sContext = context;
|
||||
}
|
||||
|
||||
public static LogInterceptor getLogInterceptor() {
|
||||
if (sLogInterceptor == null) {
|
||||
sLogInterceptor = new LogInterceptor();
|
||||
}
|
||||
return sLogInterceptor;
|
||||
}
|
||||
|
||||
public List<Cookie> getCookiesFromUrl(HttpUrl httpUrl) {
|
||||
return sCookieStore.get(httpUrl.host());
|
||||
}
|
||||
|
@ -51,6 +51,14 @@ public class HttpConstants {
|
||||
public static final String ACCEPT_ENCODING_IDENTITY = "identity";
|
||||
public static final String OC_FILE_REMOTE_ID = "OC-FileId";
|
||||
|
||||
/***********************************************************************************************************
|
||||
************************************************ CONTENT TYPES ********************************************
|
||||
***********************************************************************************************************/
|
||||
|
||||
public static final String CONTENT_TYPE_XML = "application/xml";
|
||||
public static final String CONTENT_TYPE_JSON = "application/json";
|
||||
public static final String CONTENT_TYPE_WWW_FORM = "application/x-www-form-urlencoded";
|
||||
|
||||
/***********************************************************************************************************
|
||||
************************************************ STATUS CODES *********************************************
|
||||
***********************************************************************************************************/
|
||||
|
@ -0,0 +1,65 @@
|
||||
/* ownCloud Android Library is available under MIT license
|
||||
* Copyright (C) 2020 ownCloud GmbH.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package com.owncloud.android.lib.common.http
|
||||
|
||||
import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_JSON
|
||||
import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_WWW_FORM
|
||||
import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_XML
|
||||
import okhttp3.MediaType
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
object LogBuilder {
|
||||
fun logHttp(
|
||||
networkPetition: NetworkPetition,
|
||||
networkNode: NetworkNode,
|
||||
requestId: String? = "",
|
||||
description: String
|
||||
) = Timber.d("[Network, $networkPetition] [$networkNode] [$requestId] $description")
|
||||
}
|
||||
|
||||
enum class NetworkPetition {
|
||||
REQUEST, RESPONSE;
|
||||
|
||||
override fun toString(): String = super.toString().toLowerCase(Locale.ROOT)
|
||||
}
|
||||
|
||||
enum class NetworkNode {
|
||||
INFO, HEADER, BODY;
|
||||
|
||||
override fun toString(): String = super.toString().toLowerCase(Locale.ROOT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a media type is loggable.
|
||||
*
|
||||
* @return true if its type is text, xml, json, or x-www-form-urlencoded.
|
||||
*/
|
||||
fun MediaType?.isLoggable(): Boolean =
|
||||
this?.let { mediaType ->
|
||||
val mediaTypeString = mediaType.toString()
|
||||
(mediaType.type == "text" ||
|
||||
mediaTypeString.contains(CONTENT_TYPE_XML) ||
|
||||
mediaTypeString.contains(CONTENT_TYPE_JSON) ||
|
||||
mediaTypeString.contains(CONTENT_TYPE_WWW_FORM))
|
||||
} ?: false
|
@ -0,0 +1,179 @@
|
||||
/* ownCloud Android Library is available under MIT license
|
||||
* Copyright (C) 2020 ownCloud GmbH.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
package com.owncloud.android.lib.common.http
|
||||
|
||||
import com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER
|
||||
import com.owncloud.android.lib.common.http.HttpConstants.OC_X_REQUEST_ID
|
||||
import com.owncloud.android.lib.common.http.LogBuilder.logHttp
|
||||
import com.owncloud.android.lib.common.http.NetworkNode.BODY
|
||||
import com.owncloud.android.lib.common.http.NetworkNode.HEADER
|
||||
import com.owncloud.android.lib.common.http.NetworkNode.INFO
|
||||
import com.owncloud.android.lib.common.http.NetworkPetition.REQUEST
|
||||
import com.owncloud.android.lib.common.http.NetworkPetition.RESPONSE
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okio.Buffer
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.math.max
|
||||
|
||||
class LogInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
|
||||
if (!httpLogsEnabled) {
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
|
||||
val request = chain.request().also {
|
||||
val requestId = it.headers[OC_X_REQUEST_ID]
|
||||
logHttp(REQUEST, INFO, requestId, "Type: ${it.method} URL: ${it.url}")
|
||||
logHeaders(requestId, it.headers, REQUEST)
|
||||
logRequestBody(requestId, it.body)
|
||||
}
|
||||
|
||||
val response = chain.proceed(request)
|
||||
|
||||
return response.also {
|
||||
val requestId = it.request.headers[OC_X_REQUEST_ID]
|
||||
logHttp(
|
||||
RESPONSE,
|
||||
INFO,
|
||||
requestId,
|
||||
"Code: ${it.code} Message: ${it.message} IsSuccessful: ${it.isSuccessful}"
|
||||
)
|
||||
logHeaders(requestId, it.headers, RESPONSE)
|
||||
logResponseBody(requestId, it.body)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logHeaders(requestId: String?, headers: Headers, networkPetition: NetworkPetition) {
|
||||
headers.forEach { header ->
|
||||
val headerValue: String = if (header.first.equals(AUTHORIZATION_HEADER, true)) {
|
||||
"[redacted]"
|
||||
} else {
|
||||
header.second
|
||||
}
|
||||
logHttp(networkPetition, HEADER, requestId, "${header.first}: $headerValue")
|
||||
}
|
||||
}
|
||||
|
||||
private fun logRequestBody(requestId: String?, requestBodyParam: RequestBody?) {
|
||||
requestBodyParam?.let { requestBody ->
|
||||
|
||||
if (requestBody.isOneShot()) {
|
||||
logHttp(REQUEST, BODY, requestId, "One shot body -- Omitted")
|
||||
return@let
|
||||
}
|
||||
|
||||
if (requestBody.isDuplex()) {
|
||||
logHttp(REQUEST, BODY, requestId, "Duplex body -- Omitted")
|
||||
return@let
|
||||
}
|
||||
|
||||
val buffer = Buffer()
|
||||
requestBody.writeTo(buffer)
|
||||
|
||||
val contentType = requestBody.contentType()
|
||||
val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
|
||||
|
||||
logHttp(REQUEST, BODY, requestId, "Length: ${requestBody.contentLength()} byte body")
|
||||
logHttp(REQUEST, BODY, requestId, "Type: ${requestBody.contentType()}")
|
||||
logHttp(REQUEST, BODY, requestId, "--> Body start for request")
|
||||
|
||||
if (contentType.isLoggable()) {
|
||||
if (requestBody.contentLength() < LIMIT_BODY_LOG) {
|
||||
logHttp(REQUEST, BODY, requestId, buffer.readString(charset))
|
||||
} else {
|
||||
logHttp(REQUEST, BODY, requestId, buffer.readString(LIMIT_BODY_LOG, charset))
|
||||
}
|
||||
logHttp(
|
||||
REQUEST,
|
||||
BODY,
|
||||
requestId,
|
||||
"<-- Body end for request -- Omitted: ${max(0, requestBody.contentLength() - LIMIT_BODY_LOG)} bytes"
|
||||
)
|
||||
} else {
|
||||
logHttp(
|
||||
REQUEST,
|
||||
BODY,
|
||||
requestId,
|
||||
"<-- Body end for request -- Binary -- Omitted: ${requestBody.contentLength()} bytes"
|
||||
)
|
||||
}
|
||||
|
||||
} ?: logHttp(REQUEST, BODY, requestId, "Empty body")
|
||||
}
|
||||
|
||||
private fun logResponseBody(requestId: String?, responseBodyParam: ResponseBody?) {
|
||||
responseBodyParam?.let { responseBody ->
|
||||
|
||||
val contentType = responseBody.contentType()
|
||||
val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
|
||||
|
||||
logHttp(RESPONSE, BODY, requestId, "Length: ${responseBody.contentLength()} byte body")
|
||||
logHttp(RESPONSE, BODY, requestId, "Type: ${responseBody.contentType()}")
|
||||
logHttp(RESPONSE, BODY, requestId, "--> Body start for response")
|
||||
|
||||
val source = responseBody.source()
|
||||
source.request(LIMIT_BODY_LOG)
|
||||
val buffer = source.buffer
|
||||
|
||||
if (contentType.isLoggable()) {
|
||||
|
||||
if (responseBody.contentLength() < LIMIT_BODY_LOG) {
|
||||
logHttp(RESPONSE, BODY, requestId, buffer.clone().readString(charset))
|
||||
} else {
|
||||
logHttp(RESPONSE, BODY, requestId, buffer.clone().readString(LIMIT_BODY_LOG, charset))
|
||||
}
|
||||
logHttp(
|
||||
RESPONSE,
|
||||
BODY,
|
||||
requestId,
|
||||
"<-- Body end for response -- Omitted: ${
|
||||
max(
|
||||
0,
|
||||
responseBody.contentLength() - LIMIT_BODY_LOG
|
||||
)
|
||||
} bytes"
|
||||
)
|
||||
} else {
|
||||
logHttp(
|
||||
RESPONSE,
|
||||
BODY,
|
||||
requestId,
|
||||
"<-- Body end for response -- Binary -- Omitted: ${responseBody.contentLength()} bytes"
|
||||
)
|
||||
}
|
||||
} ?: logHttp(RESPONSE, BODY, requestId, "Empty body")
|
||||
}
|
||||
|
||||
companion object {
|
||||
var httpLogsEnabled: Boolean = false
|
||||
private const val LIMIT_BODY_LOG: Long = 1024
|
||||
}
|
||||
}
|
@ -84,18 +84,12 @@ public class ChunkFromFileRequestBody extends FileRequestBody {
|
||||
long maxCount = Math.min(mOffset + mChunkSize, mChannel.size());
|
||||
while (mChannel.position() < maxCount) {
|
||||
|
||||
Timber.v("Sink buffer size: %s", sink.buffer().size());
|
||||
|
||||
readCount = mChannel.read(mBuffer);
|
||||
|
||||
Timber.v("Read " + readCount + " bytes from file channel to " + mBuffer.toString());
|
||||
|
||||
sink.buffer().write(mBuffer.array(), 0, readCount);
|
||||
sink.getBuffer().write(mBuffer.array(), 0, readCount);
|
||||
|
||||
sink.flush();
|
||||
|
||||
Timber.v("Write " + readCount + " bytes to sink buffer with size " + sink.buffer().size());
|
||||
|
||||
mBuffer.clear();
|
||||
if (mTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks
|
||||
mTransferred += readCount;
|
||||
|
@ -53,6 +53,11 @@ public class FileRequestBody extends RequestBody implements ProgressiveDataTrans
|
||||
mContentType = contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOneShot() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return mContentType;
|
||||
|
Loading…
x
Reference in New Issue
Block a user