Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions sdk-core/api/sdk-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -829,13 +829,22 @@ public final class org/dexpace/sdk/core/http/pipeline/steps/HttpInstrumentationO

public final class org/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel : java/lang/Enum {
public static final field BODY_AND_HEADERS Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
public static final field Companion Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel$Companion;
public static final field HEADERS Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
public static final field NONE Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
public static final fun fromConfiguration (Ljava/lang/String;Lorg/dexpace/sdk/core/config/Configuration;)Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
public static final fun fromConfiguration (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;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
public static fun values ()[Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
}

public final class org/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel$Companion {
public final fun fromConfiguration (Ljava/lang/String;Lorg/dexpace/sdk/core/config/Configuration;)Lorg/dexpace/sdk/core/http/pipeline/steps/HttpLogLevel;
public final fun fromConfiguration (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;
public static synthetic fun fromConfiguration$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;
}

public final class org/dexpace/sdk/core/http/pipeline/steps/HttpRedirectCondition {
public fun <init> (Lorg/dexpace/sdk/core/http/response/Response;ILjava/util/Set;)V
public final fun component1 ()Lorg/dexpace/sdk/core/http/response/Response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

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

import org.dexpace.sdk.core.config.Configuration
import java.util.Locale

/**
* Granularity of HTTP logging emitted by [InstrumentationStep].
*
Expand All @@ -30,4 +33,41 @@ public enum class HttpLogLevel {
* See [HttpInstrumentationOptions] for the streaming and async-completion-thread caveats.
*/
BODY_AND_HEADERS,
;

public companion object {
/**
* Resolves a log level by looking [key] up in [source].
*
* [source] is consulted via [Configuration.get], so the value goes through the full
* layering — explicit override -> environment variable -> normalized system property ->
* default. The value may come from any of those layers: the key `MY_PRODUCT_LOG_LEVEL`
* also matches the `my.product.log.level` system property, and an explicit override wins
* over both.
*
* The SDK is a toolkit, not a product, so it deliberately bakes in **no** default key —
* the caller (e.g. a generated client) supplies its own product's variable name, and
* [source] supplies the lookup. Pass a [Configuration] built with
* [org.dexpace.sdk.core.config.ConfigurationBuilder.envSource] to inject a hermetic env
* in tests; in production a [Configuration] backed by `System.getenv` is the natural fit.
*
* Parsing is tolerant: the resolved value is matched against the enum names
* case-insensitively and after trimming surrounding whitespace (so `headers`,
* `HEADERS`, and ` Headers ` all resolve to [HEADERS]). When the key is unset (or set
* to an empty string, which [Configuration] treats as absent) or holds an unrecognized
* value, [default] is returned. [default] itself defaults to [NONE] — logging stays off
* unless explicitly opted into.
*/
@JvmStatic
@JvmOverloads
public fun fromConfiguration(
key: String,
source: Configuration,
default: HttpLogLevel = NONE,
): HttpLogLevel {
val raw = source.get(key) ?: return default
val name = raw.trim().uppercase(Locale.US)
return entries.firstOrNull { it.name == name } ?: default
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright (c) 2026 dexpace and Omar Aljarrah
*
* Licensed under the MIT License. See LICENSE in the project root.
* SPDX-License-Identifier: MIT
*/

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

import org.dexpace.sdk.core.config.ConfigurationBuilder
import kotlin.test.Test
import kotlin.test.assertEquals

class HttpLogLevelTest {
/** Builds a [org.dexpace.sdk.core.config.Configuration] whose env layer returns [entries] and nothing else. */
private fun configWithEnv(vararg entries: Pair<String, String>) =
ConfigurationBuilder()
.envSource { name -> entries.firstOrNull { it.first == name }?.second }
.propsSource { null }
.build()

// ----- Known value resolves (case-insensitive) -----

@Test
fun `known key resolves to the matching level`() {
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to "HEADERS")
assertEquals(
HttpLogLevel.HEADERS,
HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
)
}

@Test
fun `value resolves case-insensitively`() {
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to "body_and_headers")
assertEquals(
HttpLogLevel.BODY_AND_HEADERS,
HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
)
}

@Test
fun `value resolves with surrounding whitespace trimmed`() {
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to " Headers ")
assertEquals(
HttpLogLevel.HEADERS,
HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
)
}

@Test
fun `each level name round-trips`() {
HttpLogLevel.entries.forEach { level ->
val cfg = configWithEnv("LL" to level.name)
assertEquals(level, HttpLogLevel.fromConfiguration("LL", cfg, HttpLogLevel.NONE))
}
}

// ----- Resolution honors the full Configuration layering, not just env -----

@Test
fun `value resolves from the system-property layer when the env is unset`() {
// Env unset; the property is looked up under the normalized name (MY_PRODUCT_LOG_LEVEL
// -> my.product.log.level). The resolved value must still feed the level parsing.
val cfg =
ConfigurationBuilder()
.envSource { null }
.propsSource { name -> if (name == "my.product.log.level") "HEADERS" else null }
.build()
assertEquals(
HttpLogLevel.HEADERS,
HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
)
}

@Test
fun `explicit override wins over a conflicting env value`() {
// Override is the top layer; it must take precedence over the env entry for the same key.
val cfg =
ConfigurationBuilder()
.put("MY_PRODUCT_LOG_LEVEL", "BODY_AND_HEADERS")
.envSource { name -> if (name == "MY_PRODUCT_LOG_LEVEL") "HEADERS" else null }
.propsSource { null }
.build()
assertEquals(
HttpLogLevel.BODY_AND_HEADERS,
HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
)
}

// ----- Unset key falls back to the supplied default -----

@Test
fun `unset key returns the supplied default`() {
val cfg = configWithEnv() // nothing set
assertEquals(
HttpLogLevel.HEADERS,
HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.HEADERS),
)
}

@Test
fun `empty env value returns the supplied default`() {
// An empty env var (EXAMPLE=) is treated as absent by Configuration, so the default applies.
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to "")
assertEquals(
HttpLogLevel.BODY_AND_HEADERS,
HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.BODY_AND_HEADERS),
)
}

@Test
fun `whitespace-only value returns the supplied default`() {
// Configuration only treats an exactly-empty env string as absent, so a whitespace-only
// value is returned as-is; fromConfiguration's own trim collapses it to "" and falls to the default.
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to " ")
assertEquals(
HttpLogLevel.HEADERS,
HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.HEADERS),
)
}

// ----- Unrecognized value falls back to the supplied default -----

@Test
fun `unrecognized value returns the supplied default`() {
val cfg = configWithEnv("MY_PRODUCT_LOG_LEVEL" to "VERBOSE")
assertEquals(
HttpLogLevel.NONE,
HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg, HttpLogLevel.NONE),
)
}

// ----- Default of the default-defaulted overload -----

@Test
fun `default parameter is NONE when omitted`() {
val cfg = configWithEnv() // nothing set
assertEquals(HttpLogLevel.NONE, HttpLogLevel.fromConfiguration("MY_PRODUCT_LOG_LEVEL", cfg))
}
}
Loading