Skip to content

Commit 61ed692

Browse files
feat(client): improve logging
Logging is now: 1. Streaming 4. Configurable in-memory 5. Generally more robust 6. Usable with any underlying http client
1 parent 2568fbd commit 61ed692

10 files changed

Lines changed: 1727 additions & 17 deletions

File tree

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,6 @@ The SDK throws custom unchecked exception types:
236236

237237
## Logging
238238

239-
The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
240-
241239
Enable logging by setting the `CAS_PARSER_LOG` environment variable to `info`:
242240

243241
```sh
@@ -250,6 +248,19 @@ Or to `debug` for more verbose logging:
250248
export CAS_PARSER_LOG=debug
251249
```
252250

251+
Or configure the client manually using the `logLevel` method:
252+
253+
```java
254+
import com.cas_parser.api.client.CasParserClient;
255+
import com.cas_parser.api.client.okhttp.CasParserOkHttpClient;
256+
import com.cas_parser.api.core.LogLevel;
257+
258+
CasParserClient client = CasParserOkHttpClient.builder()
259+
.fromEnv()
260+
.logLevel(LogLevel.INFO)
261+
.build();
262+
```
263+
253264
## ProGuard and R8
254265

255266
Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `cas-parser-java-core` is published with a [configuration file](cas-parser-java-core/src/main/resources/META-INF/proguard/cas-parser-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).

cas-parser-java-client-okhttp/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ dependencies {
77
api(project(":cas-parser-java-core"))
88

99
implementation("com.squareup.okhttp3:okhttp:4.12.0")
10-
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
1110

1211
testImplementation(kotlin("test"))
1312
testImplementation("org.assertj:assertj-core:3.27.7")

cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClient.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package com.cas_parser.api.client.okhttp
55
import com.cas_parser.api.client.CasParserClient
66
import com.cas_parser.api.client.CasParserClientImpl
77
import com.cas_parser.api.core.ClientOptions
8+
import com.cas_parser.api.core.LogLevel
89
import com.cas_parser.api.core.Sleeper
910
import com.cas_parser.api.core.Timeout
1011
import com.cas_parser.api.core.http.Headers
@@ -277,6 +278,15 @@ class CasParserOkHttpClient private constructor() {
277278
*/
278279
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
279280

281+
/**
282+
* The level at which to log request and response information.
283+
*
284+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
285+
*
286+
* Defaults to [LogLevel.fromEnv].
287+
*/
288+
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
289+
280290
/** Your API key for authentication. Use `sandbox-with-json-responses` as Sandbox key. */
281291
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
282292

cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/CasParserOkHttpClientAsync.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package com.cas_parser.api.client.okhttp
55
import com.cas_parser.api.client.CasParserClientAsync
66
import com.cas_parser.api.client.CasParserClientAsyncImpl
77
import com.cas_parser.api.core.ClientOptions
8+
import com.cas_parser.api.core.LogLevel
89
import com.cas_parser.api.core.Sleeper
910
import com.cas_parser.api.core.Timeout
1011
import com.cas_parser.api.core.http.Headers
@@ -277,6 +278,15 @@ class CasParserOkHttpClientAsync private constructor() {
277278
*/
278279
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
279280

281+
/**
282+
* The level at which to log request and response information.
283+
*
284+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
285+
*
286+
* Defaults to [LogLevel.fromEnv].
287+
*/
288+
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
289+
280290
/** Your API key for authentication. Use `sandbox-with-json-responses` as Sandbox key. */
281291
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
282292

cas-parser-java-client-okhttp/src/main/kotlin/com/cas_parser/api/client/okhttp/OkHttpClient.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import okhttp3.Request
3535
import okhttp3.RequestBody
3636
import okhttp3.RequestBody.Companion.toRequestBody
3737
import okhttp3.Response
38-
import okhttp3.logging.HttpLoggingInterceptor
3938
import okio.BufferedSink
4039
import okio.buffer
4140
import okio.sink
@@ -93,18 +92,6 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
9392
private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
9493
val clientBuilder = okHttpClient.newBuilder()
9594

96-
val logLevel =
97-
when (System.getenv("CAS_PARSER_LOG")?.lowercase()) {
98-
"info" -> HttpLoggingInterceptor.Level.BASIC
99-
"debug" -> HttpLoggingInterceptor.Level.BODY
100-
else -> null
101-
}
102-
if (logLevel != null) {
103-
clientBuilder.addNetworkInterceptor(
104-
HttpLoggingInterceptor().setLevel(logLevel).apply { redactHeader("x-api-key") }
105-
)
106-
}
107-
10895
requestOptions.timeout?.let {
10996
clientBuilder
11097
.connectTimeout(it.connect())

cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/ClientOptions.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package com.cas_parser.api.core
44

55
import com.cas_parser.api.core.http.Headers
66
import com.cas_parser.api.core.http.HttpClient
7+
import com.cas_parser.api.core.http.LoggingHttpClient
78
import com.cas_parser.api.core.http.PhantomReachableClosingHttpClient
89
import com.cas_parser.api.core.http.QueryParams
910
import com.cas_parser.api.core.http.RetryingHttpClient
@@ -96,6 +97,14 @@ private constructor(
9697
* Defaults to 2.
9798
*/
9899
@get:JvmName("maxRetries") val maxRetries: Int,
100+
/**
101+
* The level at which to log request and response information.
102+
*
103+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
104+
*
105+
* Defaults to [LogLevel.fromEnv].
106+
*/
107+
@get:JvmName("logLevel") val logLevel: LogLevel,
99108
/** Your API key for authentication. Use `sandbox-with-json-responses` as Sandbox key. */
100109
@get:JvmName("apiKey") val apiKey: String,
101110
) {
@@ -152,6 +161,7 @@ private constructor(
152161
private var responseValidation: Boolean = false
153162
private var timeout: Timeout = Timeout.default()
154163
private var maxRetries: Int = 2
164+
private var logLevel: LogLevel = LogLevel.fromEnv()
155165
private var apiKey: String? = null
156166

157167
@JvmSynthetic
@@ -167,6 +177,7 @@ private constructor(
167177
responseValidation = clientOptions.responseValidation
168178
timeout = clientOptions.timeout
169179
maxRetries = clientOptions.maxRetries
180+
logLevel = clientOptions.logLevel
170181
apiKey = clientOptions.apiKey
171182
}
172183

@@ -277,6 +288,15 @@ private constructor(
277288
*/
278289
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
279290

291+
/**
292+
* The level at which to log request and response information.
293+
*
294+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
295+
*
296+
* Defaults to [LogLevel.fromEnv].
297+
*/
298+
fun logLevel(logLevel: LogLevel) = apply { this.logLevel = logLevel }
299+
280300
/** Your API key for authentication. Use `sandbox-with-json-responses` as Sandbox key. */
281301
fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
282302

@@ -375,6 +395,7 @@ private constructor(
375395
* System properties take precedence over environment variables.
376396
*/
377397
fun fromEnv() = apply {
398+
logLevel(LogLevel.fromEnv())
378399
(System.getProperty("casparser.baseUrl") ?: System.getenv("CAS_PARSER_BASE_URL"))?.let {
379400
baseUrl(it)
380401
}
@@ -431,7 +452,13 @@ private constructor(
431452
return ClientOptions(
432453
httpClient,
433454
RetryingHttpClient.builder()
434-
.httpClient(httpClient)
455+
.httpClient(
456+
LoggingHttpClient.builder()
457+
.httpClient(httpClient)
458+
.clock(clock)
459+
.level(logLevel)
460+
.build()
461+
)
435462
.sleeper(sleeper)
436463
.clock(clock)
437464
.maxRetries(maxRetries)
@@ -446,6 +473,7 @@ private constructor(
446473
responseValidation,
447474
timeout,
448475
maxRetries,
476+
logLevel,
449477
apiKey,
450478
)
451479
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// File generated from our OpenAPI spec by Stainless.
2+
3+
package com.cas_parser.api.core
4+
5+
/** The level at which to log request and response information. */
6+
enum class LogLevel {
7+
/** No logging. */
8+
OFF,
9+
/** Minimal request and response summary logs. No headers or bodies are logged. */
10+
INFO,
11+
/** [INFO] logs plus details about request failures. */
12+
ERROR,
13+
/**
14+
* Full request and response logs. Sensitive headers are redacted, but sensitive data in request
15+
* and response bodies may still be visible.
16+
*/
17+
DEBUG;
18+
19+
/** Returns whether this level is at or higher than the given [level]. */
20+
fun shouldLog(level: LogLevel): Boolean = ordinal >= level.ordinal
21+
22+
companion object {
23+
24+
/** Returns a [LogLevel] based on the `CAS_PARSER_LOG` environment variable. */
25+
fun fromEnv() =
26+
when (System.getenv("CAS_PARSER_LOG")?.lowercase()) {
27+
"info" -> INFO
28+
"error" -> ERROR
29+
"debug" -> DEBUG
30+
else -> OFF
31+
}
32+
}
33+
}

cas-parser-java-core/src/main/kotlin/com/cas_parser/api/core/Utils.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package com.cas_parser.api.core
55
import com.cas_parser.api.errors.CasParserInvalidDataException
66
import java.util.Collections
77
import java.util.SortedMap
8+
import java.util.SortedSet
89
import java.util.concurrent.CompletableFuture
910
import java.util.concurrent.locks.Lock
1011

@@ -16,6 +17,11 @@ internal fun <T : Any> T?.getOrThrow(name: String): T =
1617
internal fun <T> List<T>.toImmutable(): List<T> =
1718
if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList())
1819

20+
@JvmSynthetic
21+
internal fun <V : Comparable<V>> SortedSet<V>.toImmutable(): SortedSet<V> =
22+
if (isEmpty()) Collections.emptySortedSet()
23+
else Collections.unmodifiableSortedSet(toSortedSet(comparator() ?: Comparator.naturalOrder()))
24+
1925
@JvmSynthetic
2026
internal fun <K, V> Map<K, V>.toImmutable(): Map<K, V> =
2127
if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())

0 commit comments

Comments
 (0)