diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c9d7ec..cb4f5c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- NetworkMock: replaced ~35 unconditional `println` calls in `MockConfigRepository` and + `NetworkMockPlugin` with gated [Kermit](https://github.com/touchlab/Kermit) logging + (tag `DevViewNetworkMock`), consolidating the plugin's multi-line per-request trace into one + line per intercepted request. No response body content is ever logged. A host app can adjust + or silence this via `Logger.setMinSeverity(...)`; `devview-consolelogger`, if installed, + captures it automatically. Detekt's `ForbiddenMethodCall` rule (`println`/`print`) is now + enforced repo-wide. (`devview-networkmock-core`, `devview-networkmock-ktor`, #86) + ## [0.2.0-alpha03] - 2026-09-11 ### Added diff --git a/config/quality/detekt/default-config.yml b/config/quality/detekt/default-config.yml index 83bd2615..00f5d28b 100644 --- a/config/quality/detekt/default-config.yml +++ b/config/quality/detekt/default-config.yml @@ -636,7 +636,7 @@ style: allowedImports: [ ] forbiddenImports: [ ] ForbiddenMethodCall: - active: false + active: true methods: - reason: 'print does not allow you to configure the output stream. Use a logger instead.' value: 'kotlin.io.print' diff --git a/devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepository.kt b/devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepository.kt index 57e8d340..4004deb2 100644 --- a/devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepository.kt +++ b/devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepository.kt @@ -1,5 +1,6 @@ package com.worldline.devview.networkmock.core.repository +import co.touchlab.kermit.Logger import com.worldline.devview.networkmock.core.NetworkMockResourceLoader import com.worldline.devview.networkmock.core.model.MockConfiguration import com.worldline.devview.networkmock.core.model.MockMatch @@ -8,6 +9,8 @@ import com.worldline.devview.networkmock.core.model.OperationKey import com.worldline.devview.networkmock.core.openapi.OpenApiParser import kotlinx.serialization.SerializationException +private val logger = Logger.withTag(tag = "DevViewNetworkMock") + /** * Repository for loading OpenAPI-based mock configuration and response files from resources. * @@ -68,20 +71,16 @@ public class MockConfigRepository( cachedConfig = config responseIndex = parsed.associate { it.apiSpec.id to it.responseIndex } - println( - message = "[NetworkMock][Config] Loaded ${config.specs.size} spec(s): " + + logger.d { + "Loaded ${config.specs.size} spec(s): " + config.specs.joinToString { "${it.id} (${it.operations.size} operations)" } - ) + } Result.success(value = config) } catch (e: IllegalStateException) { - println( - message = "[NetworkMock][Config] ERROR: Failed to load configuration - ${e.message}" - ) + logger.w(throwable = e) { "Failed to load configuration" } Result.failure(exception = e) } catch (e: SerializationException) { - println( - message = "[NetworkMock][Config] ERROR: Failed to load configuration - ${e.message}" - ) + logger.w(throwable = e) { "Failed to load configuration" } Result.failure(exception = e) } } @@ -127,15 +126,12 @@ public class MockConfigRepository( } if (match == null) { - println(message = "[NetworkMock][Matching] No match for $method $host$path") + logger.v { "No match for $method $host$path" } return null } val (spec, matchingOperation) = match - println( - message = "[NetworkMock][Matching] Matched $method $path -> " + - "${spec.id}/${matchingOperation.operationId}" - ) + logger.v { "Matched $method $path -> ${spec.id}/${matchingOperation.operationId}" } return MockMatch( key = OperationKey(specId = spec.id, operationId = matchingOperation.operationId), config = matchingOperation, diff --git a/devview-networkmock-ktor/src/commonMain/kotlin/com/worldline/devview/networkmock/ktor/plugin/NetworkMockPlugin.kt b/devview-networkmock-ktor/src/commonMain/kotlin/com/worldline/devview/networkmock/ktor/plugin/NetworkMockPlugin.kt index 3157e6af..a530a446 100644 --- a/devview-networkmock-ktor/src/commonMain/kotlin/com/worldline/devview/networkmock/ktor/plugin/NetworkMockPlugin.kt +++ b/devview-networkmock-ktor/src/commonMain/kotlin/com/worldline/devview/networkmock/ktor/plugin/NetworkMockPlugin.kt @@ -1,7 +1,6 @@ -@file:Suppress("StringLiteralDuplication") - package com.worldline.devview.networkmock.ktor.plugin +import co.touchlab.kermit.Logger import com.worldline.devview.networkmock.core.model.NetworkMockState import com.worldline.devview.networkmock.core.model.OperationMockState import io.ktor.client.HttpClient @@ -34,7 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -private const val LOG_PREFIX = "[NetworkMock][Plugin]" +private val logger = Logger.withTag(tag = "DevViewNetworkMock") /** * Plugin configuration wrapper. @@ -146,7 +145,7 @@ public val NetworkMockPlugin: HttpClientPlugin(value = null) scope.launch { @@ -164,21 +163,13 @@ public val NetworkMockPlugin: HttpClientPlugin key to values } - println(message = "$LOG_PREFIX ========================================") - println(message = "$LOG_PREFIX Intercepted request: $method $host$path") - val currentState = cachedState.value ?: stateRepository.getState() if (!currentState.globalMockingEnabled) { - println( - message = "$LOG_PREFIX Global mocking is DISABLED - using actual network" - ) - println(message = "$LOG_PREFIX ========================================") + logger.d { "$method $path -> NETWORK (global mocking disabled)" } return@intercept execute(requestBuilder = requestBuilder) } - println(message = "$LOG_PREFIX Global mocking is ENABLED - checking for mock") - val mockMatch = mockRepository.findMatchingMock( host = host, path = path, @@ -186,122 +177,63 @@ public val NetworkMockPlugin: HttpClientPlugin - println( - message = "$LOG_PREFIX Found matching operation: " + - "${match.specId}/${match.operationId}" - ) + if (mockMatch == null) { + logger.d { "$method $path -> NETWORK (no operation match)" } + return@intercept execute(requestBuilder = requestBuilder) + } - val endpointState = currentState.getOperationState(key = match.key) + val endpointState = currentState.getOperationState(key = mockMatch.key) - if (endpointState == null) { - println( - message = "$LOG_PREFIX No state found for operation key: ${match.key.compositeKey}" - ) - println( - message = "$LOG_PREFIX Available operation states: ${currentState.operationStates.keys}" - ) - println(message = "$LOG_PREFIX Using actual network") - println(message = "$LOG_PREFIX ========================================") - return@intercept execute(requestBuilder = requestBuilder) + if (endpointState == null) { + logger.d { + "$method $path -> NETWORK (${mockMatch.key.compositeKey} has no configured state)" } + return@intercept execute(requestBuilder = requestBuilder) + } - println( - message = - "$LOG_PREFIX Operation state: ${ - when (endpointState) { - is OperationMockState.Network -> "network" - is OperationMockState.Mock -> - "mock, status=${endpointState.statusCode}, " + - "example=${endpointState.exampleName}" - } - }" - ) - - when (endpointState) { - is OperationMockState.Network -> { - println( - message = "$LOG_PREFIX Endpoint mock not enabled" - ) - println(message = "$LOG_PREFIX Using actual network") - println( - message = "$LOG_PREFIX ========================================" - ) - } - is OperationMockState.Mock -> { - println( - message = "$LOG_PREFIX Mock is enabled with example: " + - endpointState.exampleName + when (endpointState) { + is OperationMockState.Network -> { + logger.d { "$method $path -> NETWORK (operation set to pass-through)" } + execute(requestBuilder = requestBuilder) + } + is OperationMockState.Mock -> { + @Suppress("TooGenericExceptionCaught") + try { + val mockResponse = mockRepository.loadMockResponse( + key = mockMatch.key, + statusCode = endpointState.statusCode, + exampleName = endpointState.exampleName ) - @Suppress("TooGenericExceptionCaught") - try { - val mockResponse = mockRepository.loadMockResponse( - key = match.key, - statusCode = endpointState.statusCode, - exampleName = endpointState.exampleName - ) - - mockResponse?.let { response -> - println( - message = "$LOG_PREFIX Successfully loaded mock response " + - "(status ${response.statusCode})" - ) - println( - message = "$LOG_PREFIX Returning MOCK response - " + - "NO network call will be made" - ) - println( - message = "$LOG_PREFIX ========================================" - ) - - match.delayMs?.let { ms -> - println(message = "$LOG_PREFIX Simulating delay of ${ms}ms") - delay(timeMillis = ms) - } - - return@intercept createMockHttpClientCall( - client = scope, - requestData = request, - statusCode = HttpStatusCode.fromValue( - value = response.statusCode - ), - content = response.content - ) + if (mockResponse == null) { + logger.w { + "$method $path -> NETWORK (declared mock " + + "${endpointState.statusCode}/${endpointState.exampleName} not found)" } + return@intercept execute(requestBuilder = requestBuilder) + } - if (mockResponse == null) { - println( - message = "$LOG_PREFIX ERROR: Mock response loaded as null" - ) - println(message = "$LOG_PREFIX Falling back to actual network") - println( - message = "$LOG_PREFIX ========================================" - ) - } - } catch (e: Exception) { - println( - message = "$LOG_PREFIX ERROR: Exception loading mock response - ${e.message}" - ) - println(message = "$LOG_PREFIX Falling back to actual network") - println( - message = "$LOG_PREFIX ========================================" - ) + mockMatch.delayMs?.let { ms -> delay(timeMillis = ms) } + + logger.d { + "$method $path -> MOCK ${mockResponse.statusCode}/${mockResponse.exampleName}" } + createMockHttpClientCall( + client = scope, + requestData = request, + statusCode = HttpStatusCode.fromValue( + value = mockResponse.statusCode + ), + content = mockResponse.content + ) + } catch (e: Exception) { + logger.w( + throwable = e + ) { "$method $path -> NETWORK (error loading mock response)" } + execute(requestBuilder = requestBuilder) } } } - - if (mockMatch == null) { - println(message = "$LOG_PREFIX No matching endpoint config found") - println(message = "$LOG_PREFIX Using actual network") - } - - println( - message = "$LOG_PREFIX No mock enabled for $method $path, using actual network" - ) - println(message = "$LOG_PREFIX ========================================") - execute(requestBuilder = requestBuilder) } } } diff --git a/docs/modules/networkmock-core.md b/docs/modules/networkmock-core.md index 44e4346a..caff5f66 100644 --- a/docs/modules/networkmock-core.md +++ b/docs/modules/networkmock-core.md @@ -133,6 +133,22 @@ State is persisted via `MockStateRepository`: **Upgrading from a pre-0.2.0 release**: the operation-state key shape changed (`{groupId}-{environmentId}-{endpointId}` → `{specId}-{operationId}`), and so did the `Mock` payload (a response file name → `(statusCode, exampleName)`). On first launch after upgrading, every `network_mock_endpoint_*` entry from the old shape is wiped once — this is disabled-by-default developer-tooling state, not user data, so previously-selected mocks are reset rather than translated. The global mocking toggle is unaffected. See the [migration guide](../guides/migrating-to-openapi.md) for converting an existing `mocks.json`. +## Logging + +`MockConfigRepository` logs spec-load outcomes through [Kermit](https://github.com/touchlab/Kermit), tagged `DevViewNetworkMock` — the same tag used by `devview-networkmock` and `devview-networkmock-ktor`, so a host can filter every NetworkMock-related log line by tag regardless of which module emitted it. Spec load success logs at `debug`, load failures at `warn` with the causing throwable attached. Request-matching outcomes (`findMatchingMock`) log at `verbose`, since they fire on every intercepted request. + +No response body content is ever logged. To silence NetworkMock's logs (or raise/lower their verbosity) in a host app, configure Kermit directly — this module adds no separate on/off flag of its own: + +```kotlin +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity + +// Silence everything below warnings, repo-wide (affects every Kermit-backed DevView module) +Logger.setMinSeverity(Severity.Warn) +``` + +`devview-consolelogger`, if installed, captures these logs into DevView's own in-app console screen for free — no extra wiring needed. + ## NetworkMockResourceLoader _Added in v0.1.3._ diff --git a/docs/modules/networkmock-ktor.md b/docs/modules/networkmock-ktor.md index 238c6f8f..1b4683ea 100644 --- a/docs/modules/networkmock-ktor.md +++ b/docs/modules/networkmock-ktor.md @@ -51,6 +51,8 @@ For every outgoing request, the plugin: Mock responses are returned with HTTP/1.1 status, an empty header set, and the response body as the content. +Each intercepted request logs exactly one line through [Kermit](https://github.com/touchlab/Kermit) (tag `DevViewNetworkMock`, `debug` level, `warn` for a failed mock load) — e.g. `GET /v1/users/42 -> MOCK 200/default` or `-> NETWORK (no operation match)`. No response body content is ever logged. See [Logging](networkmock-core.md#logging) for how to adjust verbosity or route these into `devview-consolelogger`. + ## Platform Actuals - **Android**: Use `HttpClient(OkHttp)` as the engine.