Log http requests, for easy debugging

This commit is contained in:
Benoit Marty 2019-03-19 12:42:11 +01:00
parent f4170f55b7
commit 40d4e3fe83
6 changed files with 281 additions and 4 deletions

View File

@ -30,10 +30,25 @@ android {
versionName "1.0" versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
} }
buildTypes { buildTypes {
debug {
// Set to true to log privacy or sensible data, such as token
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
// Set to BODY instead of NONE to enable logging
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
}
release { release {
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
@ -44,6 +59,21 @@ android {
} }
} }
static def gitRevision() {
def cmd = "git rev-parse --short HEAD"
return cmd.execute().text.trim()
}
static def gitRevisionUnixDate() {
def cmd = "git show -s --format=%ct HEAD^{commit}"
return cmd.execute().text.trim()
}
static def gitRevisionDate() {
def cmd = "git show -s --format=%ci HEAD^{commit}"
return cmd.execute().text.trim()
}
dependencies { dependencies {
def arrow_version = "0.8.0" def arrow_version = "0.8.0"

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2016 Jeff Gilfelt.
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.network.interceptors
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import okio.Buffer
import java.io.IOException
import java.nio.charset.Charset
/**
* An OkHttp interceptor that logs requests as curl shell commands. They can then
* be copied, pasted and executed inside a terminal environment. This might be
* useful for troubleshooting client/server API interaction during development,
* making it easy to isolate and share requests made by the app. <p> Warning: The
* logs generated by this interceptor have the potential to leak sensitive
* information. It should only be used in a controlled manner or in a
* non-production environment.
*/
internal class CurlLoggingInterceptor(private val logger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger.DEFAULT)
: Interceptor {
/**
* Set any additional curl command options (see 'curl --help').
*/
var curlOptions: String? = null
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var compressed = false
var curlCmd = "curl"
if (curlOptions != null) {
curlCmd += " " + curlOptions!!
}
curlCmd += " -X " + request.method()
val requestBody = request.body()
if (requestBody != null) {
val buffer = Buffer()
requestBody.writeTo(buffer)
var charset: Charset? = UTF8
val contentType = requestBody.contentType()
if (contentType != null) {
charset = contentType.charset(UTF8)
}
// try to keep to a single line and use a subshell to preserve any line breaks
curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'"
}
val headers = request.headers()
var i = 0
val count = headers.size()
while (i < count) {
val name = headers.name(i)
val value = headers.value(i)
if ("Accept-Encoding".equals(name, ignoreCase = true) && "gzip".equals(value, ignoreCase = true)) {
compressed = true
}
curlCmd += " -H \"$name: $value\""
i++
}
curlCmd += ((if (compressed) " --compressed " else " ") + "'" + request.url().toString()
// Replace localhost for emulator by localhost for shell
.replace("://10.0.2.2:8080/".toRegex(), "://127.0.0.1:8080/")
+ "'")
// Add Json formatting
curlCmd += " | python -m json.tool"
logger.log("--- cURL (" + request.url() + ")")
logger.log(curlCmd)
return chain.proceed(request)
}
companion object {
private val UTF8 = Charset.forName("UTF-8")
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.network.interceptors
import androidx.annotation.NonNull
import im.vector.matrix.android.BuildConfig
import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
companion object {
private const val INDENT_SPACE = 2
}
/**
* Log the message and try to log it again as a JSON formatted string
* Note: it can consume a lot of memory but it is only in DEBUG mode
*
* @param message
*/
@Synchronized
override fun log(@NonNull message: String) {
// In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG
if (BuildConfig.DEBUG) {
Timber.v(message)
if (message.startsWith("{")) {
// JSON Detected
try {
val o = JSONObject(message)
logJson(o.toString(INDENT_SPACE))
} catch (e: JSONException) {
// Finally this is not a JSON string...
Timber.e(e)
}
} else if (message.startsWith("[")) {
// JSON Array detected
try {
val o = JSONArray(message)
logJson(o.toString(INDENT_SPACE))
} catch (e: JSONException) {
// Finally not JSON...
Timber.e(e)
}
}
// Else not a json string to log
}
}
private fun logJson(formattedJson: String) {
val arr = formattedJson.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (s in arr) {
Timber.v(s)
}
}
}

View File

@ -19,14 +19,14 @@ package im.vector.matrix.android.internal.di
import com.facebook.stetho.okhttp3.StethoInterceptor import com.facebook.stetho.okhttp3.StethoInterceptor
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.network.* import im.vector.matrix.android.internal.network.*
import im.vector.matrix.android.internal.network.UnitConverterFactory import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.network.interceptors.FormattedJsonHttpLogger
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import okreplay.OkReplayInterceptor import okreplay.OkReplayInterceptor
import org.koin.dsl.module.module import org.koin.dsl.module.module
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkModule { class NetworkModule {
@ -46,12 +46,16 @@ class NetworkModule {
} }
single { single {
val logger = HttpLoggingInterceptor.Logger { message -> Timber.v(message) } val logger = FormattedJsonHttpLogger()
val interceptor = HttpLoggingInterceptor(logger) val interceptor = HttpLoggingInterceptor(logger)
interceptor.level = HttpLoggingInterceptor.Level.BASIC interceptor.level = BuildConfig.OKHTTP_LOGGING_LEVEL
interceptor interceptor
} }
single {
CurlLoggingInterceptor()
}
single { single {
OkReplayInterceptor() OkReplayInterceptor()
} }
@ -69,6 +73,11 @@ class NetworkModule {
.addInterceptor(get<UserAgentInterceptor>()) .addInterceptor(get<UserAgentInterceptor>())
.addInterceptor(get<AccessTokenInterceptor>()) .addInterceptor(get<AccessTokenInterceptor>())
.addInterceptor(get<HttpLoggingInterceptor>()) .addInterceptor(get<HttpLoggingInterceptor>())
.apply {
if (BuildConfig.LOG_PRIVATE_DATA) {
addInterceptor(get<CurlLoggingInterceptor>())
}
}
.addInterceptor(get<OkReplayInterceptor>()) .addInterceptor(get<OkReplayInterceptor>())
.build() .build()
} }

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2016 Jeff Gilfelt.
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.network.interceptors
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
/**
* No op interceptor
*/
internal class CurlLoggingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request())
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.network.interceptors
import androidx.annotation.NonNull
import okhttp3.logging.HttpLoggingInterceptor
/**
* No op logger
*/
internal class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
@Synchronized
override fun log(@NonNull message: String) {
}
}