Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added HttpLogger as a configuration for FhirEngine. #1570

Merged
merged 21 commits into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Google LLC
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,6 +25,7 @@ import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.ServerConfiguration
import com.google.android.fhir.demo.data.FhirPeriodicSyncWorker
import com.google.android.fhir.sync.Sync
import com.google.android.fhir.sync.remote.HttpLogger
import timber.log.Timber

class FhirApplication : Application() {
Expand All @@ -40,7 +41,15 @@ class FhirApplication : Application() {
FhirEngineConfiguration(
enableEncryptionIfSupported = true,
RECREATE_AT_OPEN,
ServerConfiguration("https://hapi.fhir.org/baseR4/")
ServerConfiguration(
"https://hapi.fhir.org/baseR4/",
httpLogger =
HttpLogger(
HttpLogger.Configuration(
if (BuildConfig.DEBUG) HttpLogger.Level.BODY else HttpLogger.Level.BASIC
)
) { Timber.tag("App-HttpLog").d(it) }
)
)
)
Sync.oneTimeSync<FhirPeriodicSyncWorker>(this)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Google LLC
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,7 @@ import android.content.Context
import com.google.android.fhir.DatabaseErrorStrategy.UNSPECIFIED
import com.google.android.fhir.sync.Authenticator
import com.google.android.fhir.sync.DataSource
import com.google.android.fhir.sync.remote.HttpLogger

/** The provider for [FhirEngine] instance. */
object FhirEngineProvider {
Expand Down Expand Up @@ -123,14 +124,19 @@ enum class DatabaseErrorStrategy {
RECREATE_AT_OPEN
}

/**
* A configuration to provide the remote FHIR server url and an [Authenticator] for supplying any
* auth token that may be necessary to communicate with the server.
*/
/** A configuration to provide necessary params for network connection. */
data class ServerConfiguration(
/** Url of the remote FHIR server. */
val baseUrl: String,
/** A configuration to provide the network connection parameters. */
val networkConfiguration: NetworkConfiguration = NetworkConfiguration(),
val authenticator: Authenticator? = null
/**
* An [Authenticator] for supplying any auth token that may be necessary to communicate with the
* server
*/
val authenticator: Authenticator? = null,
/** Logs the communication between the engine and the remote server. */
val httpLogger: HttpLogger = HttpLogger.NONE
)

/** A configuration to provide the network connection parameters. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Google LLC
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -51,11 +51,11 @@ internal data class FhirServices(
enableEncryption = true
}

internal fun setDatabaseErrorStrategy(databaseErrorStrategy: DatabaseErrorStrategy) {
internal fun setDatabaseErrorStrategy(databaseErrorStrategy: DatabaseErrorStrategy) = apply {
this.databaseErrorStrategy = databaseErrorStrategy
}

internal fun setServerConfiguration(serverConfiguration: ServerConfiguration) {
internal fun setServerConfiguration(serverConfiguration: ServerConfiguration) = apply {
this.serverConfiguration = serverConfiguration
}

Expand All @@ -71,7 +71,8 @@ internal data class FhirServices(
val remoteDataSource =
serverConfiguration?.let {
RemoteFhirService.builder(it.baseUrl, it.networkConfiguration)
.apply { setAuthenticator(it.authenticator) }
.setAuthenticator(it.authenticator)
.setHttpLogger(it.httpLogger)
.build()
}
return FhirServices(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2022 Google LLC
*
* 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:https://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 com.google.android.fhir.sync.remote

import androidx.annotation.WorkerThread

/** Logger for the network communication between the engine and the remote server */
class HttpLogger(val configuration: Configuration, @WorkerThread val log: (String) -> Unit) {
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved

data class Configuration(
val level: Level,
/** Http headers to be ignored for the logging purpose. */
val headersToIgnore: List<String>? = null
)

/** Different levels to specify the content to be logged. */
enum class Level {
/** Nothing will be logged. */
NONE,
/** Request and response lines will be logged. */
BASIC,
/** Lines along with the headers will be logged for the request and response. */
HEADERS,
/** Lines, headers and body (if present) will be logged for the request and response. */
BODY
}

companion object {
/** The logger will not log any data. */
val NONE = HttpLogger(Configuration(Level.NONE)) {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2022 Google LLC
*
* 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:https://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 com.google.android.fhir.sync.remote
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved

import okhttp3.logging.HttpLoggingInterceptor

internal fun HttpLogger.toOkHttpLoggingInterceptor() =
HttpLoggingInterceptor(log).apply {
level = configuration.level.toOkhttpLogLevel()
configuration.headersToIgnore?.forEach { this.redactHeader(it) }
}

private fun HttpLogger.Level.toOkhttpLogLevel() =
when (this) {
HttpLogger.Level.NONE -> HttpLoggingInterceptor.Level.NONE
HttpLogger.Level.BASIC -> HttpLoggingInterceptor.Level.BASIC
HttpLogger.Level.HEADERS -> HttpLoggingInterceptor.Level.HEADERS
HttpLogger.Level.BODY -> HttpLoggingInterceptor.Level.BODY
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Google LLC
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,7 +16,6 @@

package com.google.android.fhir.sync.remote

import com.google.android.fhir.BuildConfig
import com.google.android.fhir.NetworkConfiguration
import com.google.android.fhir.sync.Authenticator
import com.google.android.fhir.sync.DataSource
Expand Down Expand Up @@ -45,23 +44,24 @@ internal interface RemoteFhirService : DataSource {
private val networkConfiguration: NetworkConfiguration
) {
private var authenticator: Authenticator? = null
private var httpLoggingInterceptor: HttpLoggingInterceptor? = null

fun setAuthenticator(authenticator: Authenticator?) {
fun setAuthenticator(authenticator: Authenticator?) = apply {
this.authenticator = authenticator
}

fun setHttpLogger(httpLogger: HttpLogger) = apply {
httpLoggingInterceptor = httpLogger.toOkHttpLoggingInterceptor()
}

fun build(): RemoteFhirService {
val logger = HttpLoggingInterceptor()
logger.level =
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
else HttpLoggingInterceptor.Level.BASIC
val client =
OkHttpClient.Builder()
.connectTimeout(networkConfiguration.connectionTimeOut, TimeUnit.SECONDS)
.readTimeout(networkConfiguration.readTimeOut, TimeUnit.SECONDS)
.writeTimeout(networkConfiguration.writeTimeOut, TimeUnit.SECONDS)
.apply {
connectTimeout(networkConfiguration.connectionTimeOut, TimeUnit.SECONDS)
readTimeout(networkConfiguration.readTimeOut, TimeUnit.SECONDS)
writeTimeout(networkConfiguration.writeTimeOut, TimeUnit.SECONDS)
addInterceptor(logger)
httpLoggingInterceptor?.let { addInterceptor(it) }
authenticator?.let {
addInterceptor(
Interceptor { chain: Interceptor.Chain ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright 2022 Google LLC
*
* 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:https://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 com.google.android.fhir.sync.remote

import com.google.common.truth.Truth.assertThat
import java.util.concurrent.TimeUnit
import okhttp3.Call
import okhttp3.Connection
import okhttp3.Interceptor
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class MoreHttpLoggerKtTest {

@Test
fun `toOkHttpLoggingInterceptor HttpLoggingInterceptor Level should match provided HttpLogger Level`() {
assertThat(httpLogger(HttpLogger.Level.NONE).toOkHttpLoggingInterceptor().level)
.isEqualTo(HttpLoggingInterceptor.Level.NONE)
assertThat(httpLogger(HttpLogger.Level.BASIC).toOkHttpLoggingInterceptor().level)
.isEqualTo(HttpLoggingInterceptor.Level.BASIC)
assertThat(httpLogger(HttpLogger.Level.HEADERS).toOkHttpLoggingInterceptor().level)
.isEqualTo(HttpLoggingInterceptor.Level.HEADERS)
assertThat(httpLogger(HttpLogger.Level.BODY).toOkHttpLoggingInterceptor().level)
.isEqualTo(HttpLoggingInterceptor.Level.BODY)
}

@Test
fun `toOkHttpLoggingInterceptor all headers should be logged when headersToIgnore is not provided`() {

val logMessages = mutableListOf<String>()
HttpLogger(HttpLogger.Configuration(HttpLogger.Level.BODY, emptyList())) { logMessages.add(it) }
.toOkHttpLoggingInterceptor()
.intercept(testInterceptorChain)
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
assertThat(logMessages)
.containsAtLeast(
"Restricted: request-restricted-value",
"Restricted: response-restricted-value",
"Unrestricted: request-unrestricted-value",
"Unrestricted: response-unrestricted-value"
)
}

@Test
fun `toOkHttpLoggingInterceptor provided headersToIgnore should not be logged`() {

val logMessages = mutableListOf<String>()
HttpLogger(HttpLogger.Configuration(HttpLogger.Level.BODY, listOf("Restricted"))) {
logMessages.add(it)
}
.toOkHttpLoggingInterceptor()
.intercept(testInterceptorChain)

assertThat(logMessages)
.containsAtLeast(
"Unrestricted: request-unrestricted-value",
"Unrestricted: response-unrestricted-value"
)
assertThat(logMessages)
.containsNoneOf(
"Restricted: request-restricted-value",
"Restricted: response-restricted-value"
)
}

private fun httpLogger(level: HttpLogger.Level, headersToIgnore: List<String>? = null) =
HttpLogger(HttpLogger.Configuration(level, headersToIgnore)) {}

private val testInterceptorChain =
object : Interceptor.Chain {

override fun connection(): Connection? = null

override fun proceed(request: Request) =
Response.Builder()
.code(200)
.header("Restricted", "response-restricted-value")
.header("Unrestricted", "response-unrestricted-value")
.request(request())
.protocol(Protocol.HTTP_2)
.message("OK")
.body("Sample-Response".toResponseBody())
.build()

override fun request() =
Request.Builder()
.url("http:https://server.test.url")
.header("Restricted", "request-restricted-value")
.header("Unrestricted", "request-unrestricted-value")
.get()
.build()

override fun connectTimeoutMillis() = 1000

override fun readTimeoutMillis() = 1000

override fun withConnectTimeout(timeout: Int, unit: TimeUnit) = apply {}

override fun withReadTimeout(timeout: Int, unit: TimeUnit) = apply {}

override fun withWriteTimeout(timeout: Int, unit: TimeUnit) = apply {}

override fun writeTimeoutMillis() = 1000

override fun call(): Call {
TODO("Not yet implemented")
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}