Skip to content

Commit

Permalink
Added HttpLogger as a configuration for FhirEngine. (#1570)
Browse files Browse the repository at this point in the history
* Added HttpLogger as a configuration for FhirEngine

* Corrected docs

* Review changes

* Review changes: Updated kdoc

* Review comments: Updated redact headers test

* Updated review comments

* Updated tests
  • Loading branch information
aditya-07 committed Oct 4, 2022
1 parent a939137 commit e4c7b78
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 23 deletions.
13 changes: 11 additions & 2 deletions demo/src/main/java/com/google/android/fhir/demo/FhirApplication.kt
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
18 changes: 12 additions & 6 deletions engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt
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
9 changes: 5 additions & 4 deletions engine/src/main/java/com/google/android/fhir/FhirServices.kt
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) {

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

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,160 @@
/*
* 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 MoreHttpLoggersTest {

@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 =
intercept(
level = HttpLogger.Level.BODY,
headersToIgnore = emptyList(),
requestHeaders =
listOf(
"Restricted" to "request-restricted-value",
"Unrestricted" to "request-unrestricted-value"
),
responseHeaders =
listOf(
"Restricted" to "response-restricted-value",
"Unrestricted" to "response-unrestricted-value"
)
)
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 =
intercept(
level = HttpLogger.Level.BODY,
headersToIgnore = listOf("Restricted"),
requestHeaders =
listOf(
"Restricted" to "request-restricted-value",
"Unrestricted" to "request-unrestricted-value"
),
responseHeaders =
listOf(
"Restricted" to "response-restricted-value",
"Unrestricted" to "response-unrestricted-value"
)
)

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 fun intercept(
level: HttpLogger.Level,
headersToIgnore: List<String>? = null,
requestHeaders: List<Pair<String, String>>,
responseHeaders: List<Pair<String, String>>
): List<String> {
val logMessages = mutableListOf<String>()
HttpLogger(HttpLogger.Configuration(level, headersToIgnore)) { logMessages.add(it) }
.toOkHttpLoggingInterceptor()
.intercept(TestInterceptorChain(requestHeaders, responseHeaders))
return logMessages
}

class TestInterceptorChain(
private val requestHeaders: List<Pair<String, String>>,
private val responseHeaders: List<Pair<String, String>>
) : Interceptor.Chain {

override fun connection(): Connection? = null

override fun proceed(request: Request) =
Response.Builder()
.code(200)
.request(request())
.protocol(Protocol.HTTP_2)
.message("OK")
.body("Sample-Response".toResponseBody())
.apply { responseHeaders.forEach { header(it.first, it.second) } }
.build()

override fun request() =
Request.Builder()
.url("http:https://server.test.url")
.get()
.apply { requestHeaders.forEach { header(it.first, it.second) } }
.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")
}
}
}

0 comments on commit e4c7b78

Please sign in to comment.