Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,32 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
id(ThunderbirdPlugins.Library.kmp)
}
android {
namespace = "net.thunderbird.core.logging.console"
}
kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyDefaultHierarchyTemplate {
common {
group("commonJvm") {
withAndroidTarget()
withJvm()
}
}
}
sourceSets {
val commonJvmMain by getting
commonMain.dependencies {
implementation(projects.core.logging.api)
}
androidMain.dependencies {
implementation(libs.timber)
}
}
}

View file

@ -0,0 +1,37 @@
package net.thunderbird.core.logging.console
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
import timber.log.Timber
actual fun ConsoleLogSink(level: LogLevel): ConsoleLogSink = AndroidConsoleLogSink(level)
private class AndroidConsoleLogSink(
override val level: LogLevel,
) : ConsoleLogSink {
override fun log(event: LogEvent) {
val timber = event.tag
?.let { Timber.tag(it) }
?: Timber.tag(event.composeTag(ignoredClasses = IGNORE_CLASSES) ?: this::class.java.name)
when (event.level) {
LogLevel.VERBOSE -> timber.v(event.throwable, event.message)
LogLevel.DEBUG -> timber.d(event.throwable, event.message)
LogLevel.INFO -> timber.i(event.throwable, event.message)
LogLevel.WARN -> timber.w(event.throwable, event.message)
LogLevel.ERROR -> timber.e(event.throwable, event.message)
}
}
companion object {
private val IGNORE_CLASSES = setOf(
Timber::class.java.name,
Timber.Forest::class.java.name,
Timber.Tree::class.java.name,
Timber.DebugTree::class.java.name,
AndroidConsoleLogSink::class.java.name,
// Add other classes to ignore if needed
)
}
}

View file

@ -0,0 +1,101 @@
package net.thunderbird.core.logging.console
import android.util.Log
import assertk.assertThat
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import kotlin.test.Test
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
import timber.log.Timber
class ConsoleLogSinkTest {
@Test
fun shouldHaveCorrectLogLevel() {
// Arrange
val testSubject = ConsoleLogSink(LogLevel.INFO)
// Act & Assert
assertThat(testSubject.level).isEqualTo(LogLevel.INFO)
}
@Test
fun shouldLogMessages() {
// Arrange
val testTree = TestTree()
Timber.plant(testTree)
val eventVerbose = LogEvent(
level = LogLevel.VERBOSE,
tag = "TestTag",
message = "This is a verbose message",
throwable = null,
timestamp = 0L,
)
val eventDebug = LogEvent(
level = LogLevel.DEBUG,
tag = "TestTag",
message = "This is a debug message",
throwable = null,
timestamp = 0L,
)
val eventInfo = LogEvent(
level = LogLevel.INFO,
tag = "TestTag",
message = "This is a info message",
throwable = null,
timestamp = 0L,
)
val eventWarn = LogEvent(
level = LogLevel.WARN,
tag = "TestTag",
message = "This is a warning message",
throwable = null,
timestamp = 0L,
)
val eventError = LogEvent(
level = LogLevel.ERROR,
tag = "TestTag",
message = "This is an error message",
throwable = null,
timestamp = 0L,
)
val testSubject = ConsoleLogSink(LogLevel.VERBOSE)
// Act
testSubject.log(eventVerbose)
testSubject.log(eventDebug)
testSubject.log(eventInfo)
testSubject.log(eventWarn)
testSubject.log(eventError)
// Assert
assertThat(testTree.events).hasSize(5)
assertThat(testTree.events[0]).isEqualTo(eventVerbose)
assertThat(testTree.events[1]).isEqualTo(eventDebug)
assertThat(testTree.events[2]).isEqualTo(eventInfo)
assertThat(testTree.events[3]).isEqualTo(eventWarn)
assertThat(testTree.events[4]).isEqualTo(eventError)
}
class TestTree : Timber.DebugTree() {
val events = mutableListOf<LogEvent>()
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
events.add(LogEvent(mapPriorityToLogLevel(priority), tag, message, t, 0L))
}
private fun mapPriorityToLogLevel(priority: Int): LogLevel {
return when (priority) {
Log.VERBOSE -> LogLevel.VERBOSE
Log.DEBUG -> LogLevel.DEBUG
Log.INFO -> LogLevel.INFO
Log.WARN -> LogLevel.WARN
Log.ERROR -> LogLevel.ERROR
else -> throw IllegalArgumentException("Unknown log priority: $priority")
}
}
}
}

View file

@ -0,0 +1,67 @@
package net.thunderbird.core.logging.console
import net.thunderbird.core.logging.DefaultLogger
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.Logger
/**
* Composes a tag for the given [LogEvent].
*
* If the event has a tag, it is used; otherwise, a tag is extracted from the stack trace.
* The tag is processed using the [processTag] method before being returned.
*
* @receiver The [LogEvent] to compose a tag for.
* @param ignoredClasses The set of Class full name to be ignored.
* @param processTag Processes a tag before it is used for logging.
* @return The composed tag, or null if no tag could be determined.
*/
internal fun LogEvent.composeTag(
ignoredClasses: Set<String>,
processTag: (String) -> String? = { it },
): String? {
// If a tag is provided, use it; otherwise, extract it from the stack trace
val rawTag = tag ?: extractTagFromStackTrace(ignoredClasses)
// Process the tag before returning it
return rawTag?.let { processTag(it) }
}
/**
* Extracts a tag from the stack trace.
*
* @return The extracted tag, or null if no suitable tag could be found.
*/
private fun extractTagFromStackTrace(ignoredClasses: Set<String>): String? {
// Some classes are not available to this module, and we don't want
// to add the dependency just for class filtering.
val ignoredClasses = ignoredClasses + setOf(
"net.thunderbird.core.logging.console.ComposeLogTagKt",
"net.thunderbird.core.logging.composite.DefaultCompositeLogSink",
"net.thunderbird.core.logging.legacy.Log",
Logger::class.java.name,
DefaultLogger::class.java.name,
)
@Suppress("ThrowingExceptionsWithoutMessageOrCause")
val stackTrace = Throwable().stackTrace
return stackTrace
.firstOrNull { element ->
ignoredClasses.none { element.className.startsWith(it) }
}
?.let(::createStackElementTag)
}
/**
* Creates a tag from a stack trace element.
*
* @param element The stack trace element to create a tag from.
* @return The created tag.
*/
private fun createStackElementTag(element: StackTraceElement): String {
var tag = element.className.substringAfterLast('.')
val regex = "(\\$\\d+)+$".toRegex()
if (regex.containsMatchIn(input = tag)) {
tag = regex.replace(input = tag, replacement = "")
}
return tag
}

View file

@ -0,0 +1,23 @@
package net.thunderbird.core.logging.console
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogSink
/**
* A [LogSink] implementation that logs messages to the console.
*
* This sink uses the platform-specific implementations to handle logging.
*
* @param level The minimum [LogLevel] for messages to be logged.
*/
interface ConsoleLogSink : LogSink
/**
* Creates a [ConsoleLogSink] with the specified log level.
*
* @param level The minimum [LogLevel] for messages to be logged.
* @return A new instance of [ConsoleLogSink].
*/
expect fun ConsoleLogSink(
level: LogLevel = LogLevel.INFO,
): ConsoleLogSink

View file

@ -0,0 +1,32 @@
package net.thunderbird.core.logging.console
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
actual fun ConsoleLogSink(level: LogLevel): ConsoleLogSink = JvmConsoleLogSink(level)
private class JvmConsoleLogSink(
override val level: LogLevel,
) : ConsoleLogSink {
override fun log(event: LogEvent) {
println("[$level] ${composeMessage(event)}")
event.throwable?.printStackTrace()
}
private fun composeMessage(event: LogEvent): String {
val tag = event.tag ?: event.composeTag(ignoredClasses = IGNORE_CLASSES)
return if (tag != null) {
"[$tag] ${event.message}"
} else {
event.message
}
}
companion object {
private val IGNORE_CLASSES = setOf(
JvmConsoleLogSink::class.java.name,
// Add other classes to ignore if needed
)
}
}

View file

@ -0,0 +1,56 @@
package net.thunderbird.core.logging.console
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import kotlin.test.Test
import kotlin.test.assertEquals
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
class ConsoleLogSinkTest {
@Test
fun shouldHaveCorrectLogLevel() {
// Arrange
val testSubject = ConsoleLogSink(LogLevel.INFO)
// Act & Assert
assertEquals(LogLevel.INFO, testSubject.level)
}
@Test
fun shouldLogMessages() {
// Arrange
val originalOut = System.out
val outContent = ByteArrayOutputStream()
System.setOut(PrintStream(outContent))
try {
val eventInfo = LogEvent(
level = LogLevel.INFO,
tag = "TestTag",
message = "This is an info message",
throwable = null,
timestamp = 0L,
)
val testSubject = ConsoleLogSink(LogLevel.VERBOSE)
// Act
testSubject.log(eventInfo)
// Assert
val output = outContent.toString().trim()
println("[DEBUG_LOG] Actual output: '$output'")
// The expected format is: [VERBOSE] [TestTag] This is an info message
// Note: The log level in the output is the sink's level (VERBOSE), not the event's level (INFO)
val expectedOutput = "[VERBOSE] [TestTag] This is an info message"
println("[DEBUG_LOG] Expected output: '$expectedOutput'")
assertEquals(expectedOutput, output)
} finally {
System.setOut(originalOut)
}
}
}