diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f924ffa60340a1e698e657a3fb5da535b95b03c3..52e52ffac5f6ec850a16e2673b05709c8bce785c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,12 +5,12 @@ java = "17" kotlin = "2.0.21" ksp = "2.0.21-1.0.28" -ktor = "3.0.1" +ktor = "3.0.2" kotlinx-serialization = "1.7.3" cache4k = "0.13.0" statelyConcurrentCollections = "2.1.0" slf4j = "2.0.16" - +kotlinx-io = "0.6.0" ktorfit = "2.2.0" coroutines = "1.9.0" @@ -80,6 +80,8 @@ ktor-test = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" cache4k = { group = "io.github.reactivecircus.cache4k", name = "cache4k", version.ref = "cache4k" } statelyConcurrentCollections = { group = "co.touchlab", name = "stately-concurrent-collections", version.ref = "statelyConcurrentCollections" } slf4j = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } + kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } @@ -87,6 +89,7 @@ kotlinx-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines- kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } +kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io", version.ref = "kotlinx-io" } ktorfit-lib = { module = "de.jensklingenberg.ktorfit:ktorfit-lib-light", version.ref = "ktorfit" } diff --git a/temerity/NOTICE b/temerity/NOTICE index 63f8d0fa9d96698bafee25b7a2a884655b75c898..9ad9767805d081fcc63003a80956b53cf79bbef6 100644 --- a/temerity/NOTICE +++ b/temerity/NOTICE @@ -1,7 +1,34 @@ -This product includes software developed by Ãlvaro Salcedo GarcÃa +This library includes portions of software developed by John O'Reilly et. al.: https://github.com/joreilly/FantasyPremierLeague/ + +Function(s): buildHttpClient() +File(s): commonMain/Temerity.kt +Description: A method which builds and returns a Ktor HttpClient given a request engine, engine config, and logger +License: Apache License, Version 2.0 + +-------------------------------------------- + +This library includes portions of software developed by Chris Krueger et. al: https://github.com/ChrisKruegerDev/tmdb-kotlin + +Function(s): buildHttpClient() +File(s): commonMain/Temerity.kt +Description: A method which builds and returns a Ktor HttpClient given a request engine, engine config, and logger +License: Apache License, Version 2.0 + +-------------------------------------------- + +This library includes software developed by Ãlvaro Salcedo GarcÃa File(s): jvmTest/Util.kt Description: A utility function for configuring Koin DI context in Kotest tests. License: Apache License, Version 2.0 +-------------------------------------------- + +This library includes software developed by Paul Hawke et. al: https://github.com/psh/KermitExt + +File(s): /extensions/log/*.kt +Description: A set of extension functions for Kermit, a Kotlin multiplatform logging library. +These allow for custom log file writers, builders, and framework binders. +License: Apache License, Version 2.0 + -------------------------------------------- \ No newline at end of file diff --git a/temerity/TemerityDemo.ipynb b/temerity/TemerityDemo.ipynb index 5c539d3c3d1f8ee0e4f5087dca22942865f77de7..48c19dbce2243aad4fab3b1df87bb2ab2e34525e 100644 --- a/temerity/TemerityDemo.ipynb +++ b/temerity/TemerityDemo.ipynb @@ -5,8 +5,8 @@ "metadata": { "collapsed": true, "ExecuteTime": { - "end_time": "2024-11-26T08:55:53.124544800Z", - "start_time": "2024-11-26T08:55:52.761207200Z" + "end_time": "2024-12-04T17:26:08.492177800Z", + "start_time": "2024-12-04T17:26:08.025486300Z" } }, "source": [ @@ -45,10 +45,13 @@ "outputs": [ { "ename": "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException", - "evalue": "at Cell In[1], line 11, column 3: Unresolved reference: supportKtxNotebook", + "evalue": "at Cell In[1], line 4, column 18: Unresolved reference: cdimascio\nat Cell In[1], line 6, column 12: Unresolved reference: dotenv\nat Cell In[1], line 8, column 61: Unresolved reference: Dotenv\nat Cell In[1], line 23, column 35: Unresolved reference: dotenvVault", "output_type": "error", "traceback": [ - "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException: at Cell In[1], line 11, column 3: Unresolved reference: supportKtxNotebook", + "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException: at Cell In[1], line 4, column 18: Unresolved reference: cdimascio", + "at Cell In[1], line 6, column 12: Unresolved reference: dotenv", + "at Cell In[1], line 8, column 61: Unresolved reference: Dotenv", + "at Cell In[1], line 23, column 35: Unresolved reference: dotenvVault", "\tat org.jetbrains.kotlinx.jupyter.repl.impl.JupyterCompilerImpl.compileSync(JupyterCompilerImpl.kt:208)", "\tat org.jetbrains.kotlinx.jupyter.repl.impl.InternalEvaluatorImpl.eval(InternalEvaluatorImpl.kt:126)", "\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl$execute$1$result$1.invoke(CellExecutorImpl.kt:80)", diff --git a/temerity/build.gradle.kts b/temerity/build.gradle.kts index 4c08df2c332ec0323f42134d95020f8ede82371c..399af7dc7a0ac163b8357e687b3a69feb61f6a8d 100644 --- a/temerity/build.gradle.kts +++ b/temerity/build.gradle.kts @@ -88,6 +88,7 @@ kotlin { // TODO: Re-add cache4k when updated for Kotlin 2.0.21 implementation(libs.statelyConcurrentCollections) implementation(libs.kmpIo) + implementation(libs.kotlinx.io) } } val commonTest by getting { @@ -102,7 +103,7 @@ kotlin { val jvmMain by getting { dependencies { implementation(libs.ktor.client.java) - implementation(libs.slf4j) + implementation(libs.slf4j.api) } } val androidMain by getting { diff --git a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemActual.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemActual.kt new file mode 100644 index 0000000000000000000000000000000000000000..4cba6149f39b8faeefff8e9c0c915242c8fc07a6 --- /dev/null +++ b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemActual.kt @@ -0,0 +1,6 @@ +package edu.ucsc.its.temerity.extensions.log + +import kotlinx.io.files.FileSystem +import kotlinx.io.files.SystemFileSystem + +internal actual fun fileSystem() : FileSystem = SystemFileSystem diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt index 7f43c7a455cffb2dc97d7a5afa605f9056c3a290..d3caf43b6b3599adffeae0e23684371df94ff89e 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt @@ -44,6 +44,7 @@ public class TemClientConfig { public var serviceUrl: String? = null public var serviceToken: String? = null + public var optLoggingEnabled: Boolean = true public var optDebugEnabled: Boolean = false public var supportKtxNotebook: Boolean = false @@ -53,6 +54,7 @@ public class TemClientConfig { @Suppress("MemberVisibilityCanBePrivate") public var webTimeout: Duration? = null + @Suppress("MemberVisibilityCanBePrivate") // Default cache timeout is set to 15 minutes if this is left unspecified; @see [edu.ucsc.its.temerity.core.Temerity] init block public var cacheTimeout: Duration? = null diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt index ba2536d32eff5e99d84038f993c2a8474901385d..576562b997e701fb8ed6c78cde804b9cd9102812 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.Dispatchers internal object DispatcherFactory { - internal fun createDispatcher(dispatcherThreadPool: CoroutineDispatcher = Dispatchers.IO, threadCount: Int, dispatcherName: String): CoroutineDispatcher = dispatcherThreadPool.limitedParallelism(threadCount, dispatcherName) + internal fun createDispatcher(dispatcherThreadPool: CoroutineDispatcher, threadCount: Int, dispatcherName: String): CoroutineDispatcher = dispatcherThreadPool.limitedParallelism(threadCount, dispatcherName) internal fun createLibraryScope(dispatcher: CoroutineDispatcher): CoroutineScope = createJobScope(dispatcher, allowIndependentFailure = true) diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..1a3902befd259c65dcc2f55ec4d3ed22007c55bc --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt @@ -0,0 +1,46 @@ +package edu.ucsc.its.temerity.core + +import co.touchlab.kermit.CommonWriter +import co.touchlab.kermit.Logger +import co.touchlab.kermit.NoTagFormatter +import co.touchlab.kermit.Severity +import co.touchlab.kermit.loggerConfigInit +import co.touchlab.kermit.platformLogWriter +import edu.ucsc.its.temerity.TemClientConfig + +internal object LoggerFactory { + internal fun createLogger(tag: String?, config: TemClientConfig? = null, supportKtxNotebook: Boolean = false): Logger = + when (config){ + null -> { + Logger( + config = loggerConfigInit( + if (supportKtxNotebook) CommonWriter(NoTagFormatter) else platformLogWriter(NoTagFormatter), + minSeverity = Severity.Debug, + ), + tag = tag ?: "TemerityLib", + ) + } + else -> { + when (config.optDebugEnabled) { + // If debug logging is enabled, create a logger which color prints to the console + true -> { + Logger( + config = loggerConfigInit( + if (supportKtxNotebook) CommonWriter(NoTagFormatter) else platformLogWriter(NoTagFormatter), + minSeverity = Severity.Debug, + ), + tag = tag ?: "TemerityLib", + ) + } + // If debug logging is disabled, create a logger which prints to a log file + false -> { + Logger( + config = loggerConfigInit( + + ) + ) + } + } + } + } +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt index 935c401cf9db835cb735fff1b18b06362f38531e..0139a0a052296ce7e756278e7330a3584d72d95d 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt @@ -17,11 +17,6 @@ */ package edu.ucsc.its.temerity.core -import co.touchlab.kermit.CommonWriter -import co.touchlab.kermit.NoTagFormatter -import co.touchlab.kermit.Severity -import co.touchlab.kermit.loggerConfigInit -import co.touchlab.kermit.platformLogWriter import co.touchlab.stately.collections.ConcurrentMutableMap import com.skydoves.sandwich.ApiResponse import com.skydoves.sandwich.StatusCode @@ -37,13 +32,12 @@ import edu.ucsc.its.temerity.TemClientConfig import edu.ucsc.its.temerity.TemerityApi import edu.ucsc.its.temerity.api.PlatformApi import edu.ucsc.its.temerity.api.createPlatformApi -import edu.ucsc.its.temerity.core.DispatcherFactory.createDispatcher import edu.ucsc.its.temerity.core.DispatcherFactory.createLibraryScope import edu.ucsc.its.temerity.core.JsonFactory.buildJson import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_WEB_TIMEOUT import edu.ucsc.its.temerity.createJobScope -import edu.ucsc.its.temerity.extensions.applyAuditLogFormat -import edu.ucsc.its.temerity.extensions.applyScheduledSessionDateFormat +import edu.ucsc.its.temerity.extensions.time.applyAuditLogFormat +import edu.ucsc.its.temerity.extensions.time.applyScheduledSessionDateFormat import edu.ucsc.its.temerity.model.AuditLogEntry import edu.ucsc.its.temerity.model.Course import edu.ucsc.its.temerity.model.Device @@ -67,11 +61,9 @@ import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logging import io.ktor.client.request.header import io.ktor.client.statement.HttpResponse -import io.ktor.client.utils.clientDispatcher import io.ktor.http.URLProtocol import io.ktor.util.cio.toByteArray import io.ktor.utils.io.ByteReadChannel -import java.io.FileWriter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -100,7 +92,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.DurationUnit import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers +import org.koin.core.logger.Logger import co.touchlab.kermit.Logger as KermitLogger import io.ktor.client.plugins.logging.Logger as KtorLogger @@ -122,15 +114,6 @@ public class Temerity internal constructor( // TODO: Use this as cache4k expiration time public val DEFAULT_CACHE_EXPIRATION: Duration = 15.minutes internal const val DEFAULT_MINIMUM_THREAD_COUNT: Int = 2 - - internal fun createLogger(tag: String?, supportKtxNotebook: Boolean = false): co.touchlab.kermit.Logger = - co.touchlab.kermit.Logger( - config = loggerConfigInit( - if (supportKtxNotebook) CommonWriter(NoTagFormatter) else platformLogWriter(NoTagFormatter), - minSeverity = Severity.Debug, - ), - tag = tag ?: "TemerityLib", - ) } /** @@ -146,7 +129,7 @@ public class Temerity internal constructor( ) } factory { (config: TemClientConfig) -> - val client: HttpClient = get { parametersOf(config, createLogger("Temerity Library Web Request engine")) } + val client: HttpClient = get { parametersOf(config, createLogger(tag = "Temerity Library Web Request engine", config = config)) } val ktorfit = ktorfit { config.serviceUrl?.let { baseUrl(it) } httpClient(client) @@ -165,9 +148,9 @@ public class Temerity internal constructor( * via factories defined as part of the lib Module. */ private fun createKoinApp() = object : TemerityKoinContext() { - val logger = createLogger("TemerityLib Koin DI Context") + override val logger = createLogger(tag = "TemerityLib Koin DI Context") override val koinApp = koinApplication { - logger(object : org.koin.core.logger.Logger() { + logger(object : Logger() { override fun display(level: Level, msg: MESSAGE) { when (level) { DEBUG -> logger.d(msg) @@ -181,7 +164,7 @@ public class Temerity internal constructor( modules( libModule, platformModule() - ) + ) } override val koin: Koin = koinApp.koin } @@ -189,6 +172,7 @@ public class Temerity internal constructor( internal abstract class TemerityKoinContext { abstract val koinApp: KoinApplication abstract val koin: Koin + abstract val logger: KermitLogger } @Suppress("PropertyName") @@ -556,7 +540,7 @@ public class Temerity internal constructor( internal fun buildHttpClient( httpClientEngine: HttpClientEngine, config: TemClientConfig, - logger: co.touchlab.kermit.Logger, + logger: KermitLogger, ): HttpClient { val defaultConfig: HttpClientConfig<*>.() -> Unit = { // Can't use install(ContentNegotiation){ json() } here because YuJa's API returns a 406 error if the Accept header is set to application/json diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt new file mode 100644 index 0000000000000000000000000000000000000000..9c4f5388dded3be5f714a86c4635ab43c3e48a80 --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt @@ -0,0 +1,80 @@ +package edu.ucsc.its.temerity.extensions.log + +import co.touchlab.kermit.DefaultFormatter +import co.touchlab.kermit.LogWriter +import co.touchlab.kermit.Message +import co.touchlab.kermit.MessageStringFormatter +import co.touchlab.kermit.Severity +import co.touchlab.kermit.Tag +import kotlinx.io.buffered +import kotlinx.io.files.FileSystem +import kotlinx.io.files.Path +import kotlinx.io.writeString + +internal expect fun fileSystem(): FileSystem + +internal class FilesystemLogWriter internal constructor( + private val logPath: String, + private val logRoller: LogRoller? = null, + private val formatter: MessageStringFormatter = DefaultFormatter +) : LogWriter() { + + // Not called since we are "Context Aware" + override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { + val fileSystem = fileSystem() + val kotlinxIoPath = Path(logPath) + + logRoller?.rollLogs(kotlinxIoPath, fileSystem) + + val sink = fileSystem.sink(kotlinxIoPath, append = true).buffered() + + with(sink) { + writeString(formatter.formatMessage(severity, Tag(tag), Message(message))) + writeString("\n") + throwable?.let { + writeString(it.stackTraceToString()) + } + flush() + } + } + + companion object { + operator fun invoke(block: Builder.() -> Unit) = + with(FilesystemLogWriterBuilder()) { + block(this) + build() + } + } + + interface Builder { + fun rollLogAtSize(size: Long): Builder + fun logPath(path: String): Builder + fun build(): FilesystemLogWriter + } + + class FilesystemLogWriterBuilder() : Builder { + private var maxFileSize: Long? = null + private var logPath: String? = null + + override fun rollLogAtSize(size: Long): Builder { + maxFileSize = size + return this + } + + override fun logPath(path: String): Builder { + logPath = path + return this + } + + override fun build(): FilesystemLogWriter { + if (logPath == null) throw NullPointerException("Invalid / missing log path") + + // Can you resist the urge to Rick-roll the logs? + val rick = maxFileSize?.let { + FileSizeLogRoller(logPath!!, maxFileSize!!) + } + + return FilesystemLogWriter(logPath!!, rick) + } + } +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt new file mode 100644 index 0000000000000000000000000000000000000000..68b050cc5c46c2f2df730508a6e3265158bc2202 --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt @@ -0,0 +1,29 @@ +package edu.ucsc.its.temerity.extensions.log + +import kotlinx.io.files.FileSystem +import kotlinx.io.files.Path + +internal interface LogRoller { + fun rollLogs(kotlinxIoPath: Path, fileSystem: FileSystem) +} + +internal class FileSizeLogRoller( + private val logPath: String, + private val maxFileSize: Long +) : LogRoller { + override fun rollLogs(kotlinxIoPath: Path, fileSystem: FileSystem) { + val metadata = fileSystem.metadataOrNull(kotlinxIoPath) + metadata?.let { + it.size.let { size -> + if (size >= maxFileSize) { + val filename = kotlinxIoPath.name + val count = fileSystem.list(kotlinxIoPath.parent!!).count { p: Path -> + p.name.contains(filename) + } + val to = Path("$logPath.$count") + fileSystem.atomicMove(kotlinxIoPath, to) + } + } + } + } +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt similarity index 97% rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt index bfb164a31af64f1e313fb4c280d0e6161b6ff0ba..7ed64322b5d44f0fd76206566d0b49c49e09f028 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package edu.ucsc.its.temerity.extensions +package edu.ucsc.its.temerity.extensions.time import edu.ucsc.its.temerity.AUDIT_LOG_REQUEST_DATE_FORMAT import edu.ucsc.its.temerity.SCHEDULED_SESSION_DATE_FORMAT diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt similarity index 97% rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt index b33286c63652fd4129abecd17b1e13b064aea7af..96695252d3ce72acaa684e88cb9f2a96b62533bb 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package edu.ucsc.its.temerity.extensions +package edu.ucsc.its.temerity.extensions.time import edu.ucsc.its.temerity.AUDIT_LOG_TIMESTAMP_FORMAT import edu.ucsc.its.temerity.model.AuditLogEntry diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt index 538b45508ef65fd405ae3a10fbc0583fec87b7a5..ed163d057075a7e1c37d9060733833a6952ace73 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt @@ -17,7 +17,7 @@ */ package edu.ucsc.its.temerity.model -import edu.ucsc.its.temerity.extensions.LocalDateTimeSerializer +import edu.ucsc.its.temerity.extensions.time.LocalDateTimeSerializer import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..0010197ea7ae91755baa602f0d1feb199a88f1ce --- /dev/null +++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt @@ -0,0 +1,55 @@ +@file:Suppress("unused") + +package edu.ucsc.its.temerity.extensions.log + +import co.touchlab.kermit.CommonWriter +import co.touchlab.kermit.LogWriter +import co.touchlab.kermit.Severity +import co.touchlab.kermit.StaticConfig +import kotlinx.io.files.FileSystem +import kotlinx.io.files.SystemFileSystem +import org.slf4j.ILoggerFactory +import org.slf4j.helpers.BasicMarkerFactory +import org.slf4j.helpers.NOPMDCAdapter + +internal actual fun fileSystem() : FileSystem = SystemFileSystem + +internal class KermitServiceProvider : org.slf4j.spi.SLF4JServiceProvider { + private val markerFactory = BasicMarkerFactory() + private val mdcAdapter = NOPMDCAdapter() + override fun getLoggerFactory() = ILoggerFactory { + Slf4jKermitLogger(it, config) + } + + override fun getMarkerFactory() = markerFactory + + override fun getMDCAdapter() = mdcAdapter + + override fun getRequestedApiVersion() = "2.0.99" + + override fun initialize() = Unit + + companion object { + private val writers = mutableListOf<LogWriter>().apply { + add(CommonWriter()) + } + var config: StaticConfig = StaticConfig(logWriterList = writers) + + var minSeverity: Severity + get() = config.minSeverity + set(value) { + config = config.copy(minSeverity = value) + } + + fun addWriter(writer: LogWriter) { + writers.add(writer) + config = config.copy(logWriterList = writers) + } + + fun setWriters(vararg writer: LogWriter) { + writers.clear() + writers.addAll(writer) + config = config.copy(logWriterList = writers) + } + } +} diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt new file mode 100644 index 0000000000000000000000000000000000000000..4ffbc2053e5345de72ee129959198bb3d6c884cd --- /dev/null +++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt @@ -0,0 +1,60 @@ +@file:Suppress("unused") + +package edu.ucsc.its.temerity.extensions.log + +import co.touchlab.kermit.BaseLogger +import co.touchlab.kermit.LoggerConfig +import co.touchlab.kermit.Severity +import org.slf4j.Marker +import org.slf4j.event.Level +import org.slf4j.event.Level.* +import org.slf4j.helpers.AbstractLogger + +internal class Slf4jKermitLogger(private val name: String, config: LoggerConfig) : AbstractLogger() { + private val logger = BaseLogger(config) + override fun getName(): String = "slf4j-over-kermit" + + //region Is Logging enabled at various levels + override fun isTraceEnabled() = logger.config.minSeverity <= Severity.Verbose + override fun isTraceEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Verbose + override fun isDebugEnabled() = logger.config.minSeverity <= Severity.Debug + override fun isDebugEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Debug + override fun isInfoEnabled() = logger.config.minSeverity <= Severity.Info + override fun isInfoEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Info + override fun isWarnEnabled() = logger.config.minSeverity <= Severity.Warn + override fun isWarnEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Warn + override fun isErrorEnabled() = logger.config.minSeverity <= Severity.Error + override fun isErrorEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Error + //endregion + + override fun getFullyQualifiedCallerName(): String? = null + + override fun handleNormalizedLoggingCall( + level: Level?, + marker: Marker?, + messagePattern: String?, + arguments: Array<out Any>?, + throwable: Throwable? + ) { + val severity = when (level) { + ERROR -> Severity.Error + WARN -> Severity.Warn + INFO -> Severity.Info + DEBUG -> Severity.Debug + else -> Severity.Verbose + } + + val formatted = if (messagePattern != null && arguments != null) { + String.format(messagePattern, *(arguments.toList().toTypedArray())) + } else null + + messagePattern.let { + logger.log( + severity, + marker?.toString() ?: name, + throwable, + formatted ?: (messagePattern ?: "") + ) + } + } +}