Skip to content

Commit bb282c7

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 064ee52 commit bb282c7

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
@@ -349,8 +349,6 @@ while (true) {
349349

350350
## Logging
351351

352-
The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
353-
354352
Enable logging by setting the `LITHIC_LOG` environment variable to `info`:
355353

356354
```sh
@@ -363,6 +361,19 @@ Or to `debug` for more verbose logging:
363361
export LITHIC_LOG=debug
364362
```
365363

364+
Or configure the client manually using the `logLevel` method:
365+
366+
```java
367+
import com.lithic.api.client.LithicClient;
368+
import com.lithic.api.client.okhttp.LithicOkHttpClient;
369+
import com.lithic.api.core.LogLevel;
370+
371+
LithicClient client = LithicOkHttpClient.builder()
372+
.fromEnv()
373+
.logLevel(LogLevel.INFO)
374+
.build();
375+
```
376+
366377
## Webhooks
367378

368379
Lithic uses webhooks to notify your application when events happen. The SDK provides methods for verifying webhook signatures and parsing event payloads.

lithic-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(":lithic-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")

lithic-java-client-okhttp/src/main/kotlin/com/lithic/api/client/okhttp/LithicOkHttpClient.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
66
import com.lithic.api.client.LithicClient
77
import com.lithic.api.client.LithicClientImpl
88
import com.lithic.api.core.ClientOptions
9+
import com.lithic.api.core.LogLevel
910
import com.lithic.api.core.Sleeper
1011
import com.lithic.api.core.Timeout
1112
import com.lithic.api.core.http.AsyncStreamResponse
@@ -296,6 +297,15 @@ class LithicOkHttpClient private constructor() {
296297
*/
297298
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
298299

300+
/**
301+
* The level at which to log request and response information.
302+
*
303+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
304+
*
305+
* Defaults to [LogLevel.fromEnv].
306+
*/
307+
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
308+
299309
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
300310

301311
fun webhookSecret(webhookSecret: String?) = apply {

lithic-java-client-okhttp/src/main/kotlin/com/lithic/api/client/okhttp/LithicOkHttpClientAsync.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
66
import com.lithic.api.client.LithicClientAsync
77
import com.lithic.api.client.LithicClientAsyncImpl
88
import com.lithic.api.core.ClientOptions
9+
import com.lithic.api.core.LogLevel
910
import com.lithic.api.core.Sleeper
1011
import com.lithic.api.core.Timeout
1112
import com.lithic.api.core.http.AsyncStreamResponse
@@ -296,6 +297,15 @@ class LithicOkHttpClientAsync private constructor() {
296297
*/
297298
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
298299

300+
/**
301+
* The level at which to log request and response information.
302+
*
303+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
304+
*
305+
* Defaults to [LogLevel.fromEnv].
306+
*/
307+
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
308+
299309
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
300310

301311
fun webhookSecret(webhookSecret: String?) = apply {

lithic-java-client-okhttp/src/main/kotlin/com/lithic/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("LITHIC_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("Authorization") }
105-
)
106-
}
107-
10895
requestOptions.timeout?.let {
10996
clientBuilder
11097
.connectTimeout(it.connect())

lithic-java-core/src/main/kotlin/com/lithic/api/core/ClientOptions.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
66
import com.lithic.api.core.http.AsyncStreamResponse
77
import com.lithic.api.core.http.Headers
88
import com.lithic.api.core.http.HttpClient
9+
import com.lithic.api.core.http.LoggingHttpClient
910
import com.lithic.api.core.http.PhantomReachableClosingHttpClient
1011
import com.lithic.api.core.http.QueryParams
1112
import com.lithic.api.core.http.RetryingHttpClient
@@ -110,6 +111,14 @@ private constructor(
110111
* Defaults to 2.
111112
*/
112113
@get:JvmName("maxRetries") val maxRetries: Int,
114+
/**
115+
* The level at which to log request and response information.
116+
*
117+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
118+
*
119+
* Defaults to [LogLevel.fromEnv].
120+
*/
121+
@get:JvmName("logLevel") val logLevel: LogLevel,
113122
@get:JvmName("apiKey") val apiKey: String,
114123
private val webhookSecret: String?,
115124
) {
@@ -174,6 +183,7 @@ private constructor(
174183
private var responseValidation: Boolean = false
175184
private var timeout: Timeout = Timeout.default()
176185
private var maxRetries: Int = 2
186+
private var logLevel: LogLevel = LogLevel.fromEnv()
177187
private var apiKey: String? = null
178188
private var webhookSecret: String? = null
179189

@@ -191,6 +201,7 @@ private constructor(
191201
responseValidation = clientOptions.responseValidation
192202
timeout = clientOptions.timeout
193203
maxRetries = clientOptions.maxRetries
204+
logLevel = clientOptions.logLevel
194205
apiKey = clientOptions.apiKey
195206
webhookSecret = clientOptions.webhookSecret
196207
}
@@ -322,6 +333,15 @@ private constructor(
322333
*/
323334
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
324335

336+
/**
337+
* The level at which to log request and response information.
338+
*
339+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
340+
*
341+
* Defaults to [LogLevel.fromEnv].
342+
*/
343+
fun logLevel(logLevel: LogLevel) = apply { this.logLevel = logLevel }
344+
325345
fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
326346

327347
fun webhookSecret(webhookSecret: String?) = apply { this.webhookSecret = webhookSecret }
@@ -426,6 +446,7 @@ private constructor(
426446
* System properties take precedence over environment variables.
427447
*/
428448
fun fromEnv() = apply {
449+
logLevel(LogLevel.fromEnv())
429450
(System.getProperty("lithic.baseUrl") ?: System.getenv("LITHIC_BASE_URL"))?.let {
430451
baseUrl(it)
431452
}
@@ -502,7 +523,13 @@ private constructor(
502523
return ClientOptions(
503524
httpClient,
504525
RetryingHttpClient.builder()
505-
.httpClient(httpClient)
526+
.httpClient(
527+
LoggingHttpClient.builder()
528+
.httpClient(httpClient)
529+
.clock(clock)
530+
.level(logLevel)
531+
.build()
532+
)
506533
.sleeper(sleeper)
507534
.clock(clock)
508535
.maxRetries(maxRetries)
@@ -518,6 +545,7 @@ private constructor(
518545
responseValidation,
519546
timeout,
520547
maxRetries,
548+
logLevel,
521549
apiKey,
522550
webhookSecret,
523551
)
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.lithic.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 `LITHIC_LOG` environment variable. */
25+
fun fromEnv() =
26+
when (System.getenv("LITHIC_LOG")?.lowercase()) {
27+
"info" -> INFO
28+
"error" -> ERROR
29+
"debug" -> DEBUG
30+
else -> OFF
31+
}
32+
}
33+
}

lithic-java-core/src/main/kotlin/com/lithic/api/core/Utils.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.lithic.api.core.http.Headers
66
import com.lithic.api.errors.LithicInvalidDataException
77
import java.util.Collections
88
import java.util.SortedMap
9+
import java.util.SortedSet
910
import java.util.concurrent.CompletableFuture
1011
import java.util.concurrent.locks.Lock
1112

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

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

0 commit comments

Comments
 (0)