diff --git a/.gitignore b/.gitignore index f06dfad..d49a8b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .gradle -build \ No newline at end of file +build +.idea diff --git a/build.gradle b/build.gradle index 4afd890..77485a7 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ kotlin { // For MacOS, preset should be changed to e.g. presets.macosX64 //fromPreset(presets.linuxX64, 'linux') } + macosX64("macos") {} sourceSets { commonMain { dependencies { @@ -59,9 +60,12 @@ kotlin { implementation 'org.jetbrains.kotlin:kotlin-test-js' } } -// linuxMain { -// } -// linuxTest { -// } + macosMain { + } + macosTest { + dependencies { + implementation(kotlin("test-annotations-common")) + } + } } } diff --git a/src/macosMain/kotlin/klog/Formatter.kt b/src/macosMain/kotlin/klog/Formatter.kt new file mode 100644 index 0000000..1a5753f --- /dev/null +++ b/src/macosMain/kotlin/klog/Formatter.kt @@ -0,0 +1,151 @@ +package klog + +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pointed +import kotlinx.cinterop.ptr +import kotlinx.cinterop.value +import platform.posix.gmtime +import platform.posix.time +import platform.posix.time_tVar +import platform.posix.tm + +class Formatter() { + companion object { + private const val loggerMainClass = "kfun:klog.KLogger" + } + + constructor(body: Formatter.() -> Unit): this() { + body.invoke(this) + } + + private val formatters: MutableList<(KLoggingLevels, Throwable?, msg: Any?) -> String> = ArrayList() + + fun formatMessage( + level: KLoggingLevels, + t: Throwable?, + msg: Any? + ): String { + return formatters.joinToString(separator = "") { it.invoke(level, t, msg) } + } + + fun level() { + formatters.add{ value, _, _ -> value.name} + } + + fun dateTime() { + formatters.add{ _, _, _ -> + getDateStruct()?.let { + "${year(it)}.${month(it)}.${day(it)}-${hour(it)}:${minute(it)}:${second(it)}" + } ?:"" + } + } + + private fun year(t: tm) = (t.tm_year + 1900).toString() + private fun month(t: tm) = (t.tm_mon + 1).toString().let { if (it.length > 1) it else "0$it" } + private fun day(t: tm) = t.tm_mday.toString() + private fun hour(t: tm) = t.tm_hour.toString().let { if (it.length > 1) it else "0$it" } + private fun minute(t: tm) = t.tm_min.toString().let { if (it.length > 1) it else "0$it" } + private fun second(t: tm) = t.tm_sec.toString().let { if (it.length > 1) it else "0$it" } + + private fun getDateStruct() = memScoped { + val t = alloc() + t.value = time(null) + val local = gmtime(t.ptr)?.pointed + local + } + + fun message() { + formatters.add{ _, _, value -> value.toString()} + } + + fun text(value: String) { + formatters.add{ _, _, _ -> value} + } + + fun method(withLine: Boolean = false, compactClassName: Boolean = true) { + formatters.add { _, _, _ -> + val stackLine = getMethodBeforeLogger(Throwable().getStackTrace()) + val funLine = if (stackLine.contains("kfun:")) { + stackLine.substringAfter("kfun:") + } else { + stackLine + } + var methodName = if (funLine.contains("(")) { + funLine.substringBefore("(") + } else { + funLine + } + methodName = if (methodName.contains("#")) { + methodName.substringBefore("#") + } else { + methodName + } + methodName = if (methodName.contains("$")) { + methodName.substringBefore("$") + } else { + methodName + } + if (compactClassName) { + val methodParts = methodName.split(".") + val partsCount = methodParts.size + if (partsCount > 2) { + val b = StringBuilder() + for (i in 0 until partsCount - 2) { + b.append(methodParts[i][0]).append('.') + } + b.append(methodParts[partsCount - 2]).append('.').append(methodParts[partsCount - 1]) + methodName = b.toString() + } + } + if (withLine) { + val parts = stackLine.split(":") + if (parts.size > 2) { + "$methodName(${parts[parts.size-2]})" + } else { + "$methodName(?)" + } + } else { + methodName + } + } + } + + private fun getMethodBeforeLogger(strings: Array): String { + var kotlinNativeArrived = false + for (string in strings) { + if (kotlinNativeArrived) { + if (!string.contains(loggerMainClass)) { + return string + } + } else { + kotlinNativeArrived = string.contains(loggerMainClass) + } + } + return "" + } + + fun throwable(prefix: String = "\n ", separator: String = "\n, ") { + formatters.add { _, value, _ -> + if (value != null) { + var msg = "${prefix}${throwableLabel(value)}" + var previous = value + var current = previous.cause + while (current != null && previous != current) { + msg += "${separator}Caused by: ${throwableLabel(current)}" + previous = current + current = previous.cause + } + msg + } else { + "" + } + } + } + + private fun throwableLabel(current: Throwable): String { + val className = current::class.qualifiedName ?: current::class.simpleName ?: "" + return "$className: ${current.message}" + } + +} diff --git a/src/macosMain/kotlin/klog/KLoggers.kt b/src/macosMain/kotlin/klog/KLoggers.kt new file mode 100644 index 0000000..268b9ff --- /dev/null +++ b/src/macosMain/kotlin/klog/KLoggers.kt @@ -0,0 +1,103 @@ +package klog + +import platform.Foundation.NSLog +import kotlin.native.concurrent.AtomicReference +import kotlin.native.concurrent.freeze +import kotlin.reflect.KClass + +actual object KLoggers { + val isDebug = Throwable().getStackTrace()[0].contains("Throwable") + + private val levels = HashMap() + private val formatters = HashMap() + private val defaultLoggingLevelAtomic + = AtomicReference(if (isDebug) KLoggingLevels.DEBUG else KLoggingLevels.INFO) + + // case with println + private val defaultFormatterAtomic = + AtomicReference( + Formatter { + dateTime() + text(" - ") + level() + text(":") + if (isDebug) { + text(" ") + method(true) + } + text(" ") + message() + throwable() + }.freeze() + ) + private fun log(line: String) { println(line) } + +// case with NSLog +// private val defaultFormatterAtomic = +// AtomicReference( +// Formatter { +// level() +// text(":") +// if (isDebug) { +// text(" ") +// method(true) +// } +// text(" ") +// message() +// throwable() +// }.freeze() +// ) +// private fun log(line: String) { NSLog(line) } + + private val writerAtomic: AtomicReference<(String) -> Any> = + AtomicReference(::log.freeze()) + + var defaultLoggingLevel: KLoggingLevels + get() = defaultLoggingLevelAtomic.value + set(value) { defaultLoggingLevelAtomic.value = value } + + var defaultFormatter: Formatter + get() = defaultFormatterAtomic.value + set(value) { defaultFormatterAtomic.value = value.freeze() } + + var writer: (String) -> Any + get() = writerAtomic.value + set(value) { writerAtomic.value = value.freeze() } + + actual fun logger(owner: Any): KLogger = when (owner) { + is String -> KLogger(NativeLogger(calcLevel(owner), calcFormatter(owner), writer)) + is KClass<*> -> logger(name(owner::class)) + else -> logger(name(owner::class)) + } + + fun name(forClass: KClass): String { + val name = forClass.qualifiedName ?: forClass.simpleName ?: "" + val slicedName = when { + name.contains("Kt$") -> name.substringBefore("Kt$") + name.contains("$") -> name.substringBefore("$") + else -> name + } + return when { + slicedName.endsWith(".") -> slicedName.substring(0, slicedName.length - 1) + else -> slicedName + } + } + + fun loggingLevel(regex: Regex, level: KLoggingLevels) { + levels.put(regex, level) + } + + private fun calcLevel(name: String) = + levels + .filter { it.key.matches(name) } + .maxBy { it.value } + ?.value + ?: defaultLoggingLevel + + private fun calcFormatter(name: String) = + formatters + .filter { it.key.matches(name) } + .maxBy { it.value.toString() } + ?.value + ?: defaultFormatter +} diff --git a/src/macosMain/kotlin/klog/KLoggingLevels.kt b/src/macosMain/kotlin/klog/KLoggingLevels.kt new file mode 100644 index 0000000..b962925 --- /dev/null +++ b/src/macosMain/kotlin/klog/KLoggingLevels.kt @@ -0,0 +1,6 @@ +package klog + +@Suppress("unused") +enum class KLoggingLevels { + NONE, ERROR, WARN, INFO, DEBUG, TRACE +} diff --git a/src/macosMain/kotlin/klog/NativeLogger.kt b/src/macosMain/kotlin/klog/NativeLogger.kt new file mode 100644 index 0000000..f893ec8 --- /dev/null +++ b/src/macosMain/kotlin/klog/NativeLogger.kt @@ -0,0 +1,71 @@ +package klog + +class NativeLogger(private val level: KLoggingLevels, + private val formatter: Formatter, + private val writer: (String) -> Any) : BaseLogger { + override val isTraceEnabled: Boolean get() = level >= KLoggingLevels.TRACE + override val isDebugEnabled: Boolean get() = level >= KLoggingLevels.DEBUG + override val isInfoEnabled: Boolean get() = level >= KLoggingLevels.INFO + override val isWarnEnabled: Boolean get() = level >= KLoggingLevels.WARN + override val isErrorEnabled: Boolean get() = level >= KLoggingLevels.ERROR + + override fun trace(message: Any?) { + if (isTraceEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.TRACE, null, message)) + } + } + + override fun debug(message: Any?) { + if (isDebugEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.DEBUG, null, message)) + } + } + + override fun info(message: Any?) { + if (isInfoEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.INFO, null, message)) + } + } + + override fun warn(message: Any?) { + if (isWarnEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.WARN, null, message)) + } + } + + override fun error(message: Any?) { + if (isErrorEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.ERROR, null, message)) + } + } + + override fun trace(t: Throwable, message: Any?) { + if (isTraceEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.TRACE, t, message)) + } + } + + override fun debug(t: Throwable, message: Any?) { + if (isDebugEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.DEBUG, t, message)) + } + } + + override fun info(t: Throwable, message: Any?) { + if (isInfoEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.INFO, t, message)) + } + } + + override fun warn(t: Throwable, message: Any?) { + if (isWarnEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.WARN, t, message)) + } + } + + override fun error(t: Throwable, message: Any?) { + if (isErrorEnabled) { + writer.invoke(formatter.formatMessage(KLoggingLevels.ERROR, t, message)) + } + } +} diff --git a/src/macosTest/kotlin/klog/LogTests.kt b/src/macosTest/kotlin/klog/LogTests.kt new file mode 100644 index 0000000..ad459e0 --- /dev/null +++ b/src/macosTest/kotlin/klog/LogTests.kt @@ -0,0 +1,55 @@ +package klog + +import kotlin.native.concurrent.AtomicReference +import kotlin.native.concurrent.freeze +import kotlin.test.Test +import kotlin.test.assertEquals + +class SimpleLog: WithLogging by KLoggerHolder() { + fun test() { + log.trace { "Trace logging" } + log.debug { "Debug logging" } + log.info { "Info logging" } + log.warn { "Warn logging" } + log.error { "Error logging" } + } +} + +class LogTests { + + private val logResult = AtomicReference>(listOf()) + + private fun addLine(line: String) { + val list = ArrayList(logResult.value) + list.add(line) + logResult.value = list.freeze() + } + + init { + KLoggers.writer = ::addLine + } + + @Test + fun testLevel() { + logResult.value = listOf() + SimpleLog().test() + assertEquals(4, logResult.value.size) + checkLogString("DEBUG: k.SimpleLog.test(11) Debug logging", logResult.value[0]) + checkLogString("INFO: k.SimpleLog.test(12) Info logging", logResult.value[1]) + checkLogString("WARN: k.SimpleLog.test(13) Warn logging", logResult.value[2]) + checkLogString("ERROR: k.SimpleLog.test(14) Error logging", logResult.value[3]) + } + + fun checkLogString(expected: String, actual: String, message: String? = null) { + assertEquals('.', actual[4]) + assertEquals('.', actual[7]) + assertEquals('-', actual[10]) + assertEquals(':', actual[13]) + assertEquals(':', actual[16]) + assertEquals(' ', actual[19]) + assertEquals('-', actual[20]) + assertEquals(' ', actual[21]) + assertEquals(expected, actual.substring(22), message) + } +} +