diff --git a/build.gradle.kts b/build.gradle.kts index 57c43e5..0fd33d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ group = "com.mindera.lodge" version = "1.0.0" kotlin { - jvmToolchain(8) + jvmToolchain(11) androidTarget() jvm() iosX64() diff --git a/src/androidMain/kotlin/com/mindera/lodge/appenders/LogcatAppender.kt b/src/androidMain/kotlin/com/mindera/lodge/appenders/LogcatAppender.kt index 9a047c6..191a3cb 100644 --- a/src/androidMain/kotlin/com/mindera/lodge/appenders/LogcatAppender.kt +++ b/src/androidMain/kotlin/com/mindera/lodge/appenders/LogcatAppender.kt @@ -56,12 +56,9 @@ class LogcatAppender( } else { val chunks = ceil((1f * length / maxLineLength).toDouble()).toInt() for (i in 1..chunks) { + val start = maxLineLength * (i - 1) val max = maxLineLength * i - if (max < length) { - it.add("[Chunk $i of $chunks] " + substring(maxLineLength * (i - 1), max)) - } else { - it.add("[Chunk $i of $chunks] " + substring(maxLineLength * (i - 1))) - } + it.add(if (max < length) substring(start, max) else substring(start)) } } } diff --git a/src/appleMain/kotlin/com/mindera/lodge/appenders/OSAppender.kt b/src/appleMain/kotlin/com/mindera/lodge/appenders/OSAppender.kt index 78895e2..10ac006 100644 --- a/src/appleMain/kotlin/com/mindera/lodge/appenders/OSAppender.kt +++ b/src/appleMain/kotlin/com/mindera/lodge/appenders/OSAppender.kt @@ -8,6 +8,8 @@ import com.mindera.lodge.LOG.SEVERITY.FATAL import com.mindera.lodge.LOG.SEVERITY.INFO import com.mindera.lodge.LOG.SEVERITY.VERBOSE import com.mindera.lodge.LOG.SEVERITY.WARN +import com.mindera.lodge.extensions.emoji +import com.mindera.lodge.extensions.initial import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ptr import platform.darwin.OS_LOG_DEFAULT @@ -41,10 +43,7 @@ class OSAppender( override fun log(severity: SEVERITY, tag: String, t: Throwable?, log: String) { with(severity.toLevel()) { val prefix = severity.prefix(tag) - log(prefix + log) - t?.let { - log(prefix + it.stackTraceToString()) - } + log(t?.let { "$prefix$log\n" + it.stackTraceToString() } ?: (prefix + log)) } } @@ -61,17 +60,5 @@ class OSAppender( ERROR -> OS_LOG_TYPE_ERROR FATAL -> OS_LOG_TYPE_ERROR } - - private fun SEVERITY.prefix(tag: String) = "$emoji $name: $tag" - - // Kudos to Napier https://github.com/AAkira/Napier#darwinios-macos-watchos-tvosintelapple-silicon - private val SEVERITY.emoji: String get()= when (this) { - VERBOSE -> "⚪" - DEBUG -> "🔵" - INFO -> "🟢" - WARN -> "🟡" - ERROR -> "🔴" - FATAL -> "🟤" - } - + private fun SEVERITY.prefix(tag: String) = "$emoji | $initial | $tag: " } diff --git a/src/commonMain/kotlin/com/mindera/lodge/LOG.kt b/src/commonMain/kotlin/com/mindera/lodge/LOG.kt index e87950e..f221fad 100644 --- a/src/commonMain/kotlin/com/mindera/lodge/LOG.kt +++ b/src/commonMain/kotlin/com/mindera/lodge/LOG.kt @@ -1,8 +1,19 @@ package com.mindera.lodge -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import com.mindera.lodge.LOG.SEVERITY.DEBUG +import com.mindera.lodge.LOG.SEVERITY.ERROR +import com.mindera.lodge.LOG.SEVERITY.FATAL +import com.mindera.lodge.LOG.SEVERITY.INFO +import com.mindera.lodge.LOG.SEVERITY.VERBOSE +import com.mindera.lodge.LOG.SEVERITY.WARN +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart.UNDISPATCHED +import kotlinx.coroutines.Dispatchers.Default +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * LOG static class. It is used to abstract the LOG and have multiple possible implementations @@ -15,28 +26,55 @@ object LOG { */ private val appenders: MutableSet = mutableSetOf() - private val mutex = Mutex() + /** + * Work queue + */ + private val tasks = Channel<() -> Unit>(UNLIMITED) - private fun Mutex.withLock(action: () -> T): T = runBlocking { - withLock(owner = null, action = action) + private var delayMillis = 0L + + /** + * A dedicated coroutine that pulls lambdas out of `tasks` and executes + * them one-by-one, preserving the exact order in which they were queued. + */ + @Suppress("unused") + private val job = CoroutineScope(SupervisorJob() + Default.limitedParallelism(1)).apply { + launch(start = UNDISPATCHED) { + for (task in tasks) { + task() + delay(delayMillis) + } + } } /** - * Enable log appender + * Enable log appender. No-op if an appender with the same [Appender.loggerId] is already registered. * * @param appender Log appender to enable */ - fun add(appender: Appender) = mutex.withLock { - this.appenders.add(appender) + fun add(appender: Appender) = tasks.trySend { + if (appenders.none { it.loggerId == appender.loggerId }) { + appenders.add(appender) + } else { + log("LOG", WARN, null) { "Appender '${appender.loggerId}' discarded: an appender with that id is already registered." } + } } /** - * Enable log appenders + * Enable log appenders. Skips any appender whose [Appender.loggerId] is already registered. * * @param appenders Log appenders to enable */ - fun add(appenders: List) = mutex.withLock { - this.appenders.addAll(appenders) + fun add(appenders: List) { + tasks.trySend { + appenders.forEach { candidate -> + if (this.appenders.none { it.loggerId == candidate.loggerId }) { + this.appenders.add(candidate) + } else { + log("LOG", WARN, null) { "Appender '${candidate.loggerId}' discarded: an appender with that id is already registered." } + } + } + } } /** @@ -44,8 +82,8 @@ object LOG { * * @param id Log id of the loggers to be removed */ - fun remove(ids: String) = mutex.withLock { - this.appenders.removeAll { ids == it.loggerId } + fun remove(id: String) { + tasks.trySend { appenders.removeAll { id == it.loggerId } } } /** @@ -53,8 +91,8 @@ object LOG { * * @param ids Log ids of each of the loggers enabled by the order sent */ - fun remove(ids: Set) = mutex.withLock { - this.appenders.removeAll { ids.contains(it.loggerId) } + fun remove(ids: Set) { + tasks.trySend { appenders.removeAll { ids.contains(it.loggerId) } } } /** @@ -64,7 +102,17 @@ object LOG { * @param text The message you would like logged. */ fun v(tag: String, text: String) { - log(tag, SEVERITY.VERBOSE, null, text) + log(tag, VERBOSE, null) { text } + } + + /** + * Log with a VERBOSE level + * + * @param tag Used to identify the source of a log message. + * @param message Lambda that returns the message to be logged. + */ + fun v(tag: String, message: () -> String) { + log(tag, VERBOSE, null, message) } /** @@ -75,7 +123,18 @@ object LOG { * @param text The message you would like logged. */ fun v(tag: String, t: Throwable, text: String) { - log(tag, SEVERITY.VERBOSE, t, text) + log(tag, VERBOSE, t) { text } + } + + /** + * Log with a VERBOSE level + * + * @param tag Used to identify the source of a log message. + * @param t Throwable + * @param message Lambda that returns the message to be logged. + */ + fun v(tag: String, t: Throwable, message: () -> String) { + log(tag, VERBOSE, t, message) } /** @@ -85,7 +144,17 @@ object LOG { * @param text The message you would like logged. */ fun d(tag: String, text: String) { - log(tag, SEVERITY.DEBUG, null, text) + log(tag, DEBUG, null) { text } + } + + /** + * Log with a DEBUG level + * + * @param tag Used to identify the source of a log message. + * @param message Lambda that returns the message to be logged. + */ + fun d(tag: String, message: () -> String) { + log(tag, DEBUG, null, message) } /** @@ -96,7 +165,18 @@ object LOG { * @param text The message you would like logged. */ fun d(tag: String, t: Throwable, text: String) { - log(tag, SEVERITY.DEBUG, t, text) + log(tag, DEBUG, t) { text } + } + + /** + * Log with a DEBUG level + * + * @param tag Used to identify the source of a log message. + * @param t Throwable + * @param message Lambda that returns the message to be logged. + */ + fun d(tag: String, t: Throwable, message: () -> String) { + log(tag, DEBUG, t, message) } /** @@ -106,7 +186,17 @@ object LOG { * @param text The message you would like logged. */ fun i(tag: String, text: String) { - log(tag, SEVERITY.INFO, null, text) + log(tag, INFO, null) { text } + } + + /** + * Log with a INFO level + * + * @param tag Used to identify the source of a log message. + * @param message Lambda that returns the message to be logged. + */ + fun i(tag: String, message: () -> String) { + log(tag, INFO, null, message) } /** @@ -117,7 +207,18 @@ object LOG { * @param text The message you would like logged. */ fun i(tag: String, t: Throwable, text: String) { - log(tag, SEVERITY.INFO, t, text) + log(tag, INFO, t) { text } + } + + /** + * Log with a INFO level + * + * @param tag Used to identify the source of a log message. + * @param t Throwable + * @param message Lambda that returns the message to be logged. + */ + fun i(tag: String, t: Throwable, message: () -> String) { + log(tag, INFO, t, message) } /** @@ -127,7 +228,17 @@ object LOG { * @param text The message you would like logged. */ fun w(tag: String, text: String) { - log(tag, SEVERITY.WARN, null, text) + log(tag, WARN, null) { text } + } + + /** + * Log with a WARN level + * + * @param tag Used to identify the source of a log message. + * @param message Lambda that returns the message to be logged. + */ + fun w(tag: String, message: () -> String) { + log(tag, WARN, null, message) } /** @@ -138,7 +249,18 @@ object LOG { * @param text The message you would like logged. */ fun w(tag: String, t: Throwable, text: String) { - log(tag, SEVERITY.WARN, t, text) + log(tag, WARN, t) { text } + } + + /** + * Log with a WARN level + * + * @param tag Used to identify the source of a log message. + * @param t Throwable + * @param message Lambda that returns the message to be logged. + */ + fun w(tag: String, t: Throwable, message: () -> String) { + log(tag, WARN, t, message) } /** @@ -148,7 +270,17 @@ object LOG { * @param text The message you would like logged. */ fun e(tag: String, text: String) { - log(tag, SEVERITY.ERROR, null, text) + log(tag, ERROR, null) { text } + } + + /** + * Log with a ERROR level + * + * @param tag Used to identify the source of a log message. + * @param message Lambda that returns the message to be logged. + */ + fun e(tag: String, message: () -> String) { + log(tag, ERROR, null, message) } /** @@ -159,7 +291,18 @@ object LOG { * @param text The message you would like logged. */ fun e(tag: String, t: Throwable, text: String) { - log(tag, SEVERITY.ERROR, t, text) + log(tag, ERROR, t) { text } + } + + /** + * Log with a ERROR level + * + * @param tag Used to identify the source of a log message. + * @param t Throwable + * @param message Lambda that returns the message to be logged. + */ + fun e(tag: String, t: Throwable, message: () -> String) { + log(tag, ERROR, t, message) } /** @@ -169,7 +312,17 @@ object LOG { * @param text The message you would like logged. */ fun wtf(tag: String, text: String) { - log(tag, SEVERITY.FATAL, null, text) + log(tag, FATAL, null) { text } + } + + /** + * Log a What a Terrible Failure: Report an exception that should never happen. + * + * @param tag Used to identify the source of a log message. + * @param message Lambda that returns the message to be logged. + */ + fun wtf(tag: String, message: () -> String) { + log(tag, FATAL, null, message) } /** @@ -180,25 +333,44 @@ object LOG { * @param text The message you would like logged. */ fun wtf(tag: String, t: Throwable, text: String) { - log(tag, SEVERITY.FATAL, t, text) + log(tag, FATAL, t) { text } + } + + /** + * Log a What a Terrible Failure: Report an exception that should never happen. + * + * @param tag Used to identify the source of a log message. + * @param t Throwable + * @param message Lambda that returns the message to be logged. + */ + fun wtf(tag: String, t: Throwable, message: () -> String) { + log(tag, FATAL, t, message) } private fun log( tag: String, severity: SEVERITY, t: Throwable?, - text: String - ) = mutex.withLock { - if(appenders.isNotEmpty()) { - val log = "[T#$threadName] | $text" - appenders.forEach { - if (it.minLogLevel.ordinal > severity.ordinal) return@forEach - it.log(severity, tag, t, log) + message: () -> String, + ) { + val originalThread = threadName + tasks.trySend { + if (appenders.isNotEmpty()) { + val log = "[T#$originalThread] | ${message()}" + appenders.forEach { + if (it.minLogLevel.ordinal > severity.ordinal) return@forEach + it.log(severity, tag, t, log) + } } } } enum class SEVERITY { - VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL + VERBOSE, + DEBUG, + INFO, + WARN, + ERROR, + FATAL, } } diff --git a/src/commonMain/kotlin/com/mindera/lodge/appenders/ColorPrintAppender.kt b/src/commonMain/kotlin/com/mindera/lodge/appenders/ColorPrintAppender.kt new file mode 100644 index 0000000..8d2cfdf --- /dev/null +++ b/src/commonMain/kotlin/com/mindera/lodge/appenders/ColorPrintAppender.kt @@ -0,0 +1,58 @@ +package com.mindera.lodge.appenders + +import com.mindera.lodge.Appender +import com.mindera.lodge.LOG.SEVERITY +import com.mindera.lodge.LOG.SEVERITY.DEBUG +import com.mindera.lodge.LOG.SEVERITY.ERROR +import com.mindera.lodge.LOG.SEVERITY.FATAL +import com.mindera.lodge.LOG.SEVERITY.INFO +import com.mindera.lodge.LOG.SEVERITY.VERBOSE +import com.mindera.lodge.LOG.SEVERITY.WARN +import com.mindera.lodge.extensions.emoji +import com.mindera.lodge.extensions.initial + +class ColorPrintAppender( + id: String, + level: SEVERITY, +) : Appender { + + constructor(id: String) : this (id = id, level = VERBOSE) + constructor(level: SEVERITY) : this (id = "ColorPrintAppender", level = level) + constructor() : this (id = "PrintAppender") + + /** + * Appender ID + */ + override val loggerId: String = id + + /** + * Minimum log severity for this appender. + */ + override val minLogLevel: SEVERITY = level + + override fun log(severity: SEVERITY, tag: String, t: Throwable?, log: String) { + val prefix = severity.prefix(tag) + println("$prefix$log$ANSI_RESET") + t?.let { println("$prefix${it.stackTraceToString()}$ANSI_RESET") } + } + + private fun SEVERITY.prefix(tag: String) = "$emoji$color | $initial | $tag: " + + private val SEVERITY.color: String get() = when (this) { + VERBOSE -> ANSI_FAINT + DEBUG -> ANSI_CYAN + INFO -> ANSI_GREEN + WARN -> ANSI_YELLOW + ERROR -> ANSI_RED + FATAL -> ANSI_PURPLE + } +} + +private const val ANSI_RESET = "\u001B[0m" +private const val ANSI_CYAN = "\u001B[36m" +private const val ANSI_GREEN = "\u001B[32m" +private const val ANSI_PURPLE = "\u001B[35m" +private const val ANSI_YELLOW = "\u001B[33m" +private const val ANSI_RED = "\u001B[31m" +private const val ANSI_BOLD = "\u001B[1m" +private const val ANSI_FAINT = "\u001B[2m" diff --git a/src/commonMain/kotlin/com/mindera/lodge/extensions/Severity.kt b/src/commonMain/kotlin/com/mindera/lodge/extensions/Severity.kt new file mode 100644 index 0000000..804636a --- /dev/null +++ b/src/commonMain/kotlin/com/mindera/lodge/extensions/Severity.kt @@ -0,0 +1,28 @@ +package com.mindera.lodge.extensions + +import com.mindera.lodge.LOG.SEVERITY +import com.mindera.lodge.LOG.SEVERITY.DEBUG +import com.mindera.lodge.LOG.SEVERITY.ERROR +import com.mindera.lodge.LOG.SEVERITY.FATAL +import com.mindera.lodge.LOG.SEVERITY.INFO +import com.mindera.lodge.LOG.SEVERITY.VERBOSE +import com.mindera.lodge.LOG.SEVERITY.WARN + +// Kudos to Napier https://github.com/AAkira/Napier#darwinios-macos-watchos-tvosintelapple-silicon +internal val SEVERITY.emoji: String get() = when (this) { + VERBOSE -> "⚪" + DEBUG -> "🔵" + INFO -> "🟢" + WARN -> "🟡" + ERROR -> "🔴" + FATAL -> "🟣" +} + +internal val SEVERITY.initial: String get() = when (this) { + VERBOSE -> "V" + DEBUG -> "D" + INFO -> "I" + WARN -> "W" + ERROR -> "E" + FATAL -> "F" +}