Skip to content

Commit 1ca2d2f

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 00a20b6 commit 1ca2d2f

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

340340
## Logging
341341

342-
The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
343-
344342
Enable logging by setting the `ORB_LOG` environment variable to `info`:
345343

346344
```sh
@@ -353,6 +351,19 @@ Or to `debug` for more verbose logging:
353351
export ORB_LOG=debug
354352
```
355353

354+
Or configure the client manually using the `logLevel` method:
355+
356+
```java
357+
import com.withorb.api.client.OrbClient;
358+
import com.withorb.api.client.okhttp.OrbOkHttpClient;
359+
import com.withorb.api.core.LogLevel;
360+
361+
OrbClient client = OrbOkHttpClient.builder()
362+
.fromEnv()
363+
.logLevel(LogLevel.INFO)
364+
.build();
365+
```
366+
356367
## Webhook Verification
357368

358369
We provide helper methods for verifying that a webhook request came from Orb, and not a malicious third party.

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

orb-java-client-okhttp/src/main/kotlin/com/withorb/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("ORB_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())

orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClient.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.withorb.api.client.OrbClient
77
import com.withorb.api.client.OrbClientImpl
88
import com.withorb.api.core.ClientOptions
9+
import com.withorb.api.core.LogLevel
910
import com.withorb.api.core.Sleeper
1011
import com.withorb.api.core.Timeout
1112
import com.withorb.api.core.http.AsyncStreamResponse
@@ -290,6 +291,15 @@ class OrbOkHttpClient private constructor() {
290291
*/
291292
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
292293

294+
/**
295+
* The level at which to log request and response information.
296+
*
297+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
298+
*
299+
* Defaults to [LogLevel.fromEnv].
300+
*/
301+
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
302+
293303
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
294304

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

orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClientAsync.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.withorb.api.client.OrbClientAsync
77
import com.withorb.api.client.OrbClientAsyncImpl
88
import com.withorb.api.core.ClientOptions
9+
import com.withorb.api.core.LogLevel
910
import com.withorb.api.core.Sleeper
1011
import com.withorb.api.core.Timeout
1112
import com.withorb.api.core.http.AsyncStreamResponse
@@ -290,6 +291,15 @@ class OrbOkHttpClientAsync private constructor() {
290291
*/
291292
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
292293

294+
/**
295+
* The level at which to log request and response information.
296+
*
297+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
298+
*
299+
* Defaults to [LogLevel.fromEnv].
300+
*/
301+
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
302+
293303
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
294304

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

orb-java-core/src/main/kotlin/com/withorb/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.withorb.api.core.http.AsyncStreamResponse
77
import com.withorb.api.core.http.Headers
88
import com.withorb.api.core.http.HttpClient
9+
import com.withorb.api.core.http.LoggingHttpClient
910
import com.withorb.api.core.http.PhantomReachableClosingHttpClient
1011
import com.withorb.api.core.http.QueryParams
1112
import com.withorb.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
) {
@@ -169,6 +178,7 @@ private constructor(
169178
private var responseValidation: Boolean = false
170179
private var timeout: Timeout = Timeout.default()
171180
private var maxRetries: Int = 2
181+
private var logLevel: LogLevel = LogLevel.fromEnv()
172182
private var apiKey: String? = null
173183
private var webhookSecret: String? = null
174184

@@ -186,6 +196,7 @@ private constructor(
186196
responseValidation = clientOptions.responseValidation
187197
timeout = clientOptions.timeout
188198
maxRetries = clientOptions.maxRetries
199+
logLevel = clientOptions.logLevel
189200
apiKey = clientOptions.apiKey
190201
webhookSecret = clientOptions.webhookSecret
191202
}
@@ -311,6 +322,15 @@ private constructor(
311322
*/
312323
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
313324

325+
/**
326+
* The level at which to log request and response information.
327+
*
328+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
329+
*
330+
* Defaults to [LogLevel.fromEnv].
331+
*/
332+
fun logLevel(logLevel: LogLevel) = apply { this.logLevel = logLevel }
333+
314334
fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
315335

316336
fun webhookSecret(webhookSecret: String?) = apply { this.webhookSecret = webhookSecret }
@@ -415,6 +435,7 @@ private constructor(
415435
* System properties take precedence over environment variables.
416436
*/
417437
fun fromEnv() = apply {
438+
logLevel(LogLevel.fromEnv())
418439
(System.getProperty("orb.baseUrl") ?: System.getenv("ORB_BASE_URL"))?.let {
419440
baseUrl(it)
420441
}
@@ -490,7 +511,13 @@ private constructor(
490511
return ClientOptions(
491512
httpClient,
492513
RetryingHttpClient.builder()
493-
.httpClient(httpClient)
514+
.httpClient(
515+
LoggingHttpClient.builder()
516+
.httpClient(httpClient)
517+
.clock(clock)
518+
.level(logLevel)
519+
.build()
520+
)
494521
.sleeper(sleeper)
495522
.clock(clock)
496523
.maxRetries(maxRetries)
@@ -507,6 +534,7 @@ private constructor(
507534
responseValidation,
508535
timeout,
509536
maxRetries,
537+
logLevel,
510538
apiKey,
511539
webhookSecret,
512540
)
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.withorb.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 `ORB_LOG` environment variable. */
25+
fun fromEnv() =
26+
when (System.getenv("ORB_LOG")?.lowercase()) {
27+
"info" -> INFO
28+
"error" -> ERROR
29+
"debug" -> DEBUG
30+
else -> OFF
31+
}
32+
}
33+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.withorb.api.core.http.Headers
66
import com.withorb.api.errors.OrbInvalidDataException
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)