diff --git a/clashJ/build.gradle.kts b/clashJ/build.gradle.kts index da876a08..95710248 100644 --- a/clashJ/build.gradle.kts +++ b/clashJ/build.gradle.kts @@ -20,10 +20,12 @@ dependencies { implementation(libs.ktor.client.core) implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.logging) implementation(libs.ktor.client.apache5) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.gson) + implementation(libs.mu.logging.jvm) api(libs.logback.classic) testImplementation(libs.ktor.client.mock) diff --git a/clashJ/src/main/kotlin/io/github/maicolantali/Client.kt b/clashJ/src/main/kotlin/io/github/maicolantali/Client.kt index b3693abb..fab8b5c4 100644 --- a/clashJ/src/main/kotlin/io/github/maicolantali/Client.kt +++ b/clashJ/src/main/kotlin/io/github/maicolantali/Client.kt @@ -32,10 +32,12 @@ import io.github.maicolantali.types.internal.configuration.ClientConfiguration import io.github.maicolantali.util.API_BASE_URL import io.github.maicolantali.util.encodeTag import io.github.maicolantali.util.getConfiguredRequestHandler +import io.github.maicolantali.util.level import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async +import mu.KLogging /** * Client class is used to interact with the Clash of Clans API. @@ -53,8 +55,14 @@ open class Client( internal open val password: String, clientConfiguration: ClientConfiguration.() -> Unit = {}, ) { + internal companion object : KLogging() + internal val config = ClientConfiguration().apply(clientConfiguration) + init { + logger("io.github.maicolantali").level = config.logging.clientLogLevel + } + private val requestHandler by lazy { getConfiguredRequestHandler() } /** diff --git a/clashJ/src/main/kotlin/io/github/maicolantali/EventClient.kt b/clashJ/src/main/kotlin/io/github/maicolantali/EventClient.kt index 68d975c2..b1e64124 100644 --- a/clashJ/src/main/kotlin/io/github/maicolantali/EventClient.kt +++ b/clashJ/src/main/kotlin/io/github/maicolantali/EventClient.kt @@ -8,7 +8,6 @@ import io.github.maicolantali.event.PlayerEvents import io.github.maicolantali.event.WarEvents import io.github.maicolantali.event.cache.CacheManager import io.github.maicolantali.exception.MaintenanceException -import io.github.maicolantali.http.RequestHandler import io.github.maicolantali.types.api.model.clans.clan.Clan import io.github.maicolantali.types.api.model.clans.clanMember.ClanMember import io.github.maicolantali.types.api.model.clans.clanwar.ClanWar @@ -25,7 +24,6 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.slf4j.LoggerFactory import java.time.LocalDateTime import java.util.concurrent.Executors @@ -62,10 +60,6 @@ class EventClient( private var isApiInMaintenance = false private var maintenanceStartTime: LocalDateTime? = null - private companion object { - private val log = LoggerFactory.getLogger(RequestHandler::class.java) - } - /** * Starts polling for events and updating player data. * @@ -93,7 +87,7 @@ class EventClient( * @param playerTag The player tag that will be monitored and updated. */ fun addPlayerToUpdateQueue(playerTag: String) { - log.info("Adding the player tag: $playerTag to the update queue for monitoring.") + logger.info("Adding the player tag: $playerTag to the update queue for monitoring.") players.add(adjustTag(playerTag)) } @@ -107,7 +101,7 @@ class EventClient( * @param clanTag The clan tag that will be monitored and updated. */ fun addClanToUpdateQueue(clanTag: String) { - log.info("Adding the clan tag: $clanTag to the update queue for monitoring.") + logger.info("Adding the clan tag: $clanTag to the update queue for monitoring.") clans.add(adjustTag(clanTag)) } @@ -121,7 +115,7 @@ class EventClient( * @param clanTag The clan tag for which war events will be monitored and updated. */ fun addWarToUpdateQueue(clanTag: String) { - log.info("Adding the clan tag: $clanTag to the update queue for monitoring war events.") + logger.info("Adding the clan tag: $clanTag to the update queue for monitoring war events.") wars.add(adjustTag(clanTag)) } @@ -135,7 +129,7 @@ class EventClient( * @param playerTag The player tag that will no longer be monitored. */ fun removePlayerToUpdateQueue(playerTag: String) { - log.info("Removing the player tag: $playerTag from the update queue.") + logger.info("Removing the player tag: $playerTag from the update queue.") players.remove(adjustTag(playerTag)) } @@ -149,7 +143,7 @@ class EventClient( * @param clanTag The clan tag that will no longer be monitored. */ fun removeClanToUpdateQueue(clanTag: String) { - log.info("Removing the clan tag: $clanTag from the update queue.") + logger.info("Removing the clan tag: $clanTag from the update queue.") clans.remove(adjustTag(clanTag)) } @@ -163,7 +157,7 @@ class EventClient( * @param clanTag The clan tag for which war events will no longer be monitored. */ fun removeWarToUpdateQueue(clanTag: String) { - log.info("Removing the clan tag: $clanTag from the update queue for monitoring war events.") + logger.info("Removing the clan tag: $clanTag from the update queue for monitoring war events.") wars.remove(adjustTag(clanTag)) } @@ -278,7 +272,7 @@ class EventClient( event: Event<*, *, *, *>, callback: Callback<*, *, *>, ) { - log.info("Adding a new callback for the $event event.") + logger.info("Adding a new callback for the $event event.") eventCallbacks .computeIfAbsent(event::class.java.superclass) { HashMap() } @@ -294,14 +288,14 @@ class EventClient( if (isApiInMaintenance) { isApiInMaintenance = false - log.info("API is back online. Resuming updates.") + logger.info("API is back online. Resuming updates.") triggerEvent(maintenanceStartTime, LocalDateTime.now(), MaintenanceEvents::class.java) maintenanceStartTime = null } } catch (e: MaintenanceException) { if (!isApiInMaintenance) { - log.info("API is in maintenance. Stopping updates.") + logger.info("API is in maintenance. Stopping updates.") isApiInMaintenance = true // API is in maintenance } if (maintenanceStartTime == null) { @@ -309,7 +303,7 @@ class EventClient( triggerEvent(maintenanceStartTime, eventType = MaintenanceEvents::class.java) } } catch (e: Exception) { - log.error("Error checking API maintenance status: ${e.message}") + logger.error("Error checking API maintenance status: ${e.message}") } delay(config.event.maintenanceCheckInterval) @@ -340,10 +334,10 @@ class EventClient( delayMillis = 100L // Reset delay on successful request } catch (e: MaintenanceException) { - log.info("API is in maintenance. Stopping updates (${type.simpleName}: $tag).") + logger.info("API is in maintenance. Stopping updates (${type.simpleName}: $tag).") isApiInMaintenance = true } catch (e: Exception) { - log.error("Error: ${e.message}") + logger.error("Error: ${e.message}") delay(delayMillis) // Exponential backoff delayMillis *= 2 } diff --git a/clashJ/src/main/kotlin/io/github/maicolantali/http/RequestHandler.kt b/clashJ/src/main/kotlin/io/github/maicolantali/http/RequestHandler.kt index a4c24436..b1923da2 100644 --- a/clashJ/src/main/kotlin/io/github/maicolantali/http/RequestHandler.kt +++ b/clashJ/src/main/kotlin/io/github/maicolantali/http/RequestHandler.kt @@ -40,7 +40,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import org.slf4j.LoggerFactory +import mu.KLogging import java.time.LocalDateTime import java.util.Base64 import java.util.concurrent.atomic.AtomicBoolean @@ -61,9 +61,8 @@ class RequestHandler( private val engine: HttpClientEngine, private val throttler: BaseThrottler, ) { - private companion object { + private companion object : KLogging() { private const val MAX_KEY_COUNT = 10 - private val log = LoggerFactory.getLogger(RequestHandler::class.java) } private val keysSet = SafeSet() @@ -93,7 +92,7 @@ class RequestHandler( */ suspend fun login() = coroutineScope { - log.info("Logging into the developer website.") + logger.info("Logging into the developer website.") keysSet.clear() @@ -110,29 +109,29 @@ class RequestHandler( break // Success, exit the loop } catch (e: HttpRequestTimeoutException) { if (attempt == 3) { - log.error("Login operation failed. Error ${e.message}") + logger.error("Login operation failed. Error ${e.message}") throw e } - log.warn("Login operation failed. Retry in $delayMillis ms") + logger.warn("Login operation failed. Retry in $delayMillis ms") delay(delayMillis) delayMillis *= 2 // Exponential backoff } } if (loginResponse.status == HttpStatusCode.Forbidden) { - log.error("The provided credential are incorrect or invalid.") + logger.error("The provided credential are incorrect or invalid.") closeHttpClient() throw InvalidCredentialException("The provided credential are incorrect or invalid.") } - log.info("Successfully logged in.") + logger.info("Successfully logged in.") val ip: String = async { getIp(loginResponse.body().getAsJsonPrimitive("temporaryAPIToken").asString) }.await() - log.info("The current IP address has been successfully retrieved. IP address: $ip") + logger.info("The current IP address has been successfully retrieved. IP address: $ip") retriveKeys(ip, loginResponse.headers["set-cookie"]!!) } @@ -217,7 +216,7 @@ class RequestHandler( continue } else { - log.error("Forbidden, resp: $responseJson, reason: ${responseJson.get("reason")}") + logger.error("Forbidden, resp: $responseJson, reason: ${responseJson.get("reason")}") } throw HttpException("Forbidden, resp: $responseJson, reason: ${responseJson.get("reason")}") @@ -228,7 +227,7 @@ class RequestHandler( } HttpStatusCode.TooManyRequests -> { - log.error("Reached the maximum rate-limits by the API. Consider a new number of request allowed per second.") + logger.error("Reached the maximum rate-limits by the API. Consider a new number of request allowed per second.") throw HttpException( "Reached the maximum rate-limits by the API. Consider a new number of request allowed per second.", ) @@ -251,13 +250,13 @@ class RequestHandler( throw BadGatewayException("The API timed out waiting for the request.") } - log.debug("HttpRequestTimeoutException caught. Retry to execute the request: $url.") + logger.debug("HttpRequestTimeoutException caught. Retry to execute the request: $url.") delay(tries * 3 + 1L) continue } } - log.error("Reached the maxRetryAttempts (${requestOptions.maxRetryAttempts}) without a valid responses.") + logger.error("Reached the maxRetryAttempts (${requestOptions.maxRetryAttempts}) without a valid responses.") throw ClashJException("Reached the maxRetryAttempts (${requestOptions.maxRetryAttempts}) without a valid responses.") } @@ -283,7 +282,7 @@ class RequestHandler( // Filter key for the specified key name, val keys = keyListResponse.keys.toMutableList() - log.info("${keys.size} valid keys retrieved from the developer site.") + logger.info("${keys.size} valid keys retrieved from the developer site.") // Revoke keys that have not the current ip keys.filter { it.name == keyOptions.keyName } @@ -315,14 +314,14 @@ class RequestHandler( } if (keysSet.size() < keyOptions.keyCount && keys.size == 10) { - log.warn( + logger.warn( "Required ${keyOptions.keyCount} keys, but maximum ${keysSet.size()} found/made (limit: 10/acc). " + "Please delete keys or decrease `keyCount`.", ) } if (keysSet.size() == 0) { - log.error( + logger.error( "${keys.size} existing API keys, none match keyName: ${keyOptions.keyName}. " + "Specify another keyName or delete unused keys at 'https://developer.clashofclans.com'.", ) @@ -356,7 +355,7 @@ class RequestHandler( LocalDateTime.now() } }"}""" - log.info("Generating a new key based on the data: $keyJson") + logger.info("Generating a new key based on the data: $keyJson") val response: CreateKeyResponse = httpClient.post("$DEV_SITE_BASE_URL/apikey/create") { @@ -365,7 +364,7 @@ class RequestHandler( }.body() if (response.status.message != "ok") { - log.error("Failed to create the new API Key. Details: ${response.status.detail}") + logger.error("Failed to create the new API Key. Details: ${response.status.detail}") closeHttpClient() throw HttpException("Failed to create the new API Key. Details: ${response.status.detail}") } @@ -386,7 +385,7 @@ class RequestHandler( key: Key, cookie: String, ): Boolean { - log.info("Removing the key named: ${key.name} and IP: ${key.cidrRanges} (as it does not match our current IP address).") + logger.info("Removing the key named: ${key.name} and IP: ${key.cidrRanges} (as it does not match our current IP address).") val response = httpClient.post("${DEV_SITE_BASE_URL}/apikey/revoke") { diff --git a/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/HttpClientLogLevel.kt b/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/HttpClientLogLevel.kt new file mode 100644 index 00000000..670e7841 --- /dev/null +++ b/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/HttpClientLogLevel.kt @@ -0,0 +1,52 @@ +package io.github.maicolantali.types.internal + +import io.ktor.client.plugins.logging.LogLevel + +/** + * Enumeration representing different log levels for an HTTP client. + * + * This enum class defines log levels that can be associated with an HTTP client, + * mapping them to the corresponding log levels in the Ktor HTTP client library. + */ +enum class HttpClientLogLevel { + /** + * No logging. + */ + NONE, + + /** + * Log only informational messages for HTTP requests and responses. + */ + INFO, + + /** + * Log HTTP request and response bodies in addition to informational messages. + */ + BODY, + + /** + * Log HTTP headers in addition to informational messages. + */ + HEADERS, + + /** + * Log all details, including HTTP headers and request/response bodies. + */ + ALL, + + ; + + /** + * Converts the enum value to the corresponding Ktor log level. + * + * @return The Ktor log level associated with the enum value. + */ + internal fun toKtorLogLevel() = + when (this) { + NONE -> LogLevel.NONE + INFO -> LogLevel.INFO + BODY -> LogLevel.BODY + HEADERS -> LogLevel.HEADERS + ALL -> LogLevel.ALL + } +} diff --git a/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/configuration/ClientConfiguration.kt b/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/configuration/ClientConfiguration.kt index 75037eda..51ae80ef 100644 --- a/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/configuration/ClientConfiguration.kt +++ b/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/configuration/ClientConfiguration.kt @@ -14,13 +14,12 @@ import io.github.maicolantali.http.throttler.BatchThrottler data class ClientConfiguration( internal var key: KeyConfiguration = KeyConfiguration(), internal var httpClient: HttpConfiguration = HttpConfiguration(), + internal var logging: LoggingConfiguration = LoggingConfiguration(), internal var event: EventConfiguration = EventConfiguration(), internal var throttler: BaseThrottler = BatchThrottler(), ) { /** - * Configures the API key settings for the client. - * - * @param config Lambda function to configure the [KeyConfiguration]. + * Configures key using the settings specified in [KeyConfiguration]. */ fun key(config: KeyConfiguration.() -> Unit) { key.config() @@ -36,9 +35,14 @@ data class ClientConfiguration( } /** - * Configures the event handling settings for the client. - * - * @param config Lambda function to configure the [EventConfiguration]. + * Configures logging using the settings specified in [LoggingConfiguration]. + */ + fun logging(config: LoggingConfiguration.() -> Unit) { + logging.config() + } + + /** + * Configures events using the settings specified in [EventConfiguration]. */ fun event(config: EventConfiguration.() -> Unit) { event.config() diff --git a/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/configuration/LoggingConfiguration.kt b/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/configuration/LoggingConfiguration.kt new file mode 100644 index 00000000..123965c3 --- /dev/null +++ b/clashJ/src/main/kotlin/io/github/maicolantali/types/internal/configuration/LoggingConfiguration.kt @@ -0,0 +1,15 @@ +package io.github.maicolantali.types.internal.configuration + +import ch.qos.logback.classic.Level +import io.github.maicolantali.types.internal.HttpClientLogLevel + +/** + * Data class representing the configuration for logging in an application. + * + * @property clientLogLevel The log level for general client-side logging. + * @property httClientLogLevel The log level for HTTP client-specific logging. + */ +data class LoggingConfiguration( + var clientLogLevel: Level = Level.INFO, + var httClientLogLevel: HttpClientLogLevel = HttpClientLogLevel.NONE, +) diff --git a/clashJ/src/main/kotlin/io/github/maicolantali/util/ClientUtils.kt b/clashJ/src/main/kotlin/io/github/maicolantali/util/ClientUtils.kt new file mode 100644 index 00000000..6965f0a3 --- /dev/null +++ b/clashJ/src/main/kotlin/io/github/maicolantali/util/ClientUtils.kt @@ -0,0 +1,10 @@ +package io.github.maicolantali.util + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger + +internal var mu.KLogger.level: Level + get() = (underlyingLogger as Logger).level + set(value) { + (underlyingLogger as Logger).level = value + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 35bec980..729a0a96 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ coroutines = "1.7.3" ktor = "2.3.7" logback = "1.4.14" +mu-logging = "3.0.5" kotlin_test = "1.9.21" junit = "5.10.1" @@ -15,12 +16,14 @@ kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", # Ktor ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-apache5 = { module = "io.ktor:ktor-client-apache5", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-gson = { module = "io.ktor:ktor-serialization-gson", version.ref = "ktor" } # Logging logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +mu-logging-jvm = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "mu-logging" } # Testing ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }