Skip to content

Commit 7914a83

Browse files
committed
feat: resolve HTTP log level from a caller-supplied env var
Add HttpLogLevel.fromEnv(key, source, default) so a log level can be driven from an environment variable without the toolkit baking in a variable name. The caller supplies both the key (e.g. a generated client passes its own product's variable) and the source, reusing the existing Configuration env-source seam so the lookup is injectable and tests never touch the real process environment. Parsing is tolerant: values match the enum names case-insensitively and after trimming surrounding whitespace. An unset key, an empty value (which Configuration treats as absent), or an unrecognized value falls back to the supplied default, which itself defaults to NONE so logging stays off unless explicitly opted into.
1 parent ea0cc81 commit 7914a83

3 files changed

Lines changed: 146 additions & 0 deletions

File tree

sdk-core/api/sdk-core.api

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,13 +829,22 @@ public final class org/dexpace/sdk/core/http/pipeline/steps/HttpInstrumentationO
829829

830830
public final class org/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel : java/lang/Enum {
831831
public static final field BODY_AND_HEADERS Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
832+
public static final field Companion Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel$Companion;
832833
public static final field HEADERS Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
833834
public static final field NONE Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
835+
public static final fun fromEnv (Ljava/lang/String;Lorg/dexpace/sdk/core/config/Configuration;)Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
836+
public static final fun fromEnv (Ljava/lang/String;Lorg/dexpace/sdk/core/config/Configuration;Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;)Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
834837
public static fun getEntries ()Lkotlin/enums/EnumEntries;
835838
public static fun valueOf (Ljava/lang/String;)Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
836839
public static fun values ()[Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
837840
}
838841

842+
public final class org/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel$Companion {
843+
public final fun fromEnv (Ljava/lang/String;Lorg/dexpace/sdk/core/config/Configuration;)Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
844+
public final fun fromEnv (Ljava/lang/String;Lorg/dexpace/sdk/core/config/Configuration;Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;)Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
845+
public static synthetic fun fromEnv$default (Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel$Companion;Ljava/lang/String;Lorg/dexpace/sdk/core/config/Configuration;Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;ILjava/lang/Object;)Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
846+
}
847+
839848
public final class org/dexpace/sdk/core/http/pipeline/steps/HttpRedirectCondition {
840849
public fun <init> (Lorg/dexpace/sdk/core/http/response/Response;ILjava/util/Set;)V
841850
public final fun component1 ()Lorg/dexpace/sdk/core/http/response/Response;

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
package org.dexpace.sdk.core.http.pipeline.steps
99

10+
import org.dexpace.sdk.core.config.Configuration
11+
import java.util.Locale
12+
1013
/**
1114
* Granularity of HTTP logging emitted by [InstrumentationStep].
1215
*
@@ -30,4 +33,40 @@ public enum class HttpLogLevel {
3033
* See [HttpInstrumentationOptions] for the streaming and async-completion-thread caveats.
3134
*/
3235
BODY_AND_HEADERS,
36+
;
37+
38+
public companion object {
39+
/**
40+
* Resolves a log level from the environment-variable [key], reading through the testable
41+
* env-source seam of [source].
42+
*
43+
* The SDK is a toolkit, not a product, so it deliberately bakes in **no** default key —
44+
* the caller (e.g. a generated client) supplies its own product's variable name, and
45+
* [source] supplies the lookup. Pass a [Configuration] built with
46+
* [org.dexpace.sdk.core.config.ConfigurationBuilder.envSource] to inject a hermetic env
47+
* in tests; in production a [Configuration] backed by `System.getenv` is the natural fit.
48+
*
49+
* Parsing is tolerant: the resolved value is matched against the enum names
50+
* case-insensitively and after trimming surrounding whitespace (so `headers`,
51+
* `HEADERS`, and ` Headers ` all resolve to [HEADERS]). When the key is unset (or set
52+
* to an empty string, which [Configuration] treats as absent) or holds an unrecognized
53+
* value, [default] is returned. [default] itself defaults to [NONE] — logging stays off
54+
* unless explicitly opted into.
55+
*/
56+
@JvmStatic
57+
@JvmOverloads
58+
public fun fromEnv(
59+
key: String,
60+
source: Configuration,
61+
default: HttpLogLevel = NONE,
62+
): HttpLogLevel {
63+
val raw = source.get(key) ?: return default
64+
return when (raw.trim().uppercase(Locale.US)) {
65+
"NONE" -> NONE
66+
"HEADERS" -> HEADERS
67+
"BODY_AND_HEADERS" -> BODY_AND_HEADERS
68+
else -> default
69+
}
70+
}
71+
}
3372
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright (c) 2026 dexpace and Omar Aljarrah
3+
*
4+
* Licensed under the MIT License. See LICENSE in the project root.
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
package org.dexpace.sdk.core.http.pipeline.steps
9+
10+
import org.dexpace.sdk.core.config.ConfigurationBuilder
11+
import kotlin.test.Test
12+
import kotlin.test.assertEquals
13+
14+
class HttpLogLevelTest {
15+
/** Builds a [org.dexpace.sdk.core.config.Configuration] whose env layer returns [entries] and nothing else. */
16+
private fun configWithEnv(vararg entries: Pair<String, String>) =
17+
ConfigurationBuilder()
18+
.envSource { name -> entries.firstOrNull { it.first == name }?.second }
19+
.propsSource { null }
20+
.build()
21+
22+
// ----- Known value resolves (case-insensitive) -----
23+
24+
@Test
25+
fun `known key resolves to the matching level`() {
26+
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to "HEADERS")
27+
assertEquals(
28+
HttpLogLevel.HEADERS,
29+
HttpLogLevel.fromEnv("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
30+
)
31+
}
32+
33+
@Test
34+
fun `value resolves case-insensitively`() {
35+
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to "body_and_headers")
36+
assertEquals(
37+
HttpLogLevel.BODY_AND_HEADERS,
38+
HttpLogLevel.fromEnv("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
39+
)
40+
}
41+
42+
@Test
43+
fun `value resolves with surrounding whitespace trimmed`() {
44+
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to " Headers ")
45+
assertEquals(
46+
HttpLogLevel.HEADERS,
47+
HttpLogLevel.fromEnv("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
48+
)
49+
}
50+
51+
@Test
52+
fun `each level name round-trips`() {
53+
HttpLogLevel.entries.forEach { level ->
54+
val cfg = configWithEnv("LL" to level.name)
55+
assertEquals(level, HttpLogLevel.fromEnv("LL", cfg, HttpLogLevel.NONE))
56+
}
57+
}
58+
59+
// ----- Unset key falls back to the supplied default -----
60+
61+
@Test
62+
fun `unset key returns the supplied default`() {
63+
val cfg = configWithEnv() // nothing set
64+
assertEquals(
65+
HttpLogLevel.HEADERS,
66+
HttpLogLevel.fromEnv("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.HEADERS),
67+
)
68+
}
69+
70+
@Test
71+
fun `empty env value returns the supplied default`() {
72+
// An empty env var (EXAMPLE=) is treated as absent by Configuration, so the default applies.
73+
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to "")
74+
assertEquals(
75+
HttpLogLevel.BODY_AND_HEADERS,
76+
HttpLogLevel.fromEnv("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.BODY_AND_HEADERS),
77+
)
78+
}
79+
80+
// ----- Unrecognized value falls back to the supplied default -----
81+
82+
@Test
83+
fun `unrecognized value returns the supplied default`() {
84+
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to "VERBOSE")
85+
assertEquals(
86+
HttpLogLevel.NONE,
87+
HttpLogLevel.fromEnv("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
88+
)
89+
}
90+
91+
// ----- Default of the default-defaulted overload -----
92+
93+
@Test
94+
fun `default parameter is NONE when omitted`() {
95+
val cfg = configWithEnv() // nothing set
96+
assertEquals(HttpLogLevel.NONE, HttpLogLevel.fromEnv("MY_PRODUCT_LOG_LEVEL", cfg))
97+
}
98+
}

0 commit comments

Comments
 (0)