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,15 @@
plugins {
id(ThunderbirdPlugins.Library.kmp)
}
android {
namespace = "net.thunderbird.core.logging.composite"
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.core.logging.api)
}
}
}

View file

@ -0,0 +1,37 @@
package net.thunderbird.core.logging.composite
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogLevelProvider
import net.thunderbird.core.logging.LogSink
/**
* A [LogSink] that aggregates multiple [LogSink] and forwards log events to them.
*
* This [CompositeLogSink] is useful when you want to log messages to multiple destinations
* (e.g., console, file, etc.) without having to manage each [LogSink] individually.
*
* It checks the log level of each event against its own level and forwards the event
* to all managed sinks that can handle the event's level.
*
* @param level The minimum log level this sink will process. Log events with a lower priority will be ignored.
* @param manager The [CompositeLogSinkManager] that manages the collection of sinks.
*/
interface CompositeLogSink : LogSink {
val manager: CompositeLogSinkManager
}
/**
* Creates a [CompositeLogSink] with the specified log level and manager.
*
* @param logLevelProvider The minimum [LogLevel] for messages to be logged.
* @param manager The [CompositeLogSinkManager] that manages the collection of sinks.
* @param sinks A list of [LogSink] instances to be managed by this composite sink.
* @return A new instance of [CompositeLogSink].
*/
fun CompositeLogSink(
logLevelProvider: LogLevelProvider,
manager: CompositeLogSinkManager = DefaultLogSinkManager(),
sinks: List<LogSink> = emptyList(),
): CompositeLogSink {
return DefaultCompositeLogSink(logLevelProvider, manager, sinks)
}

View file

@ -0,0 +1,42 @@
package net.thunderbird.core.logging.composite
import net.thunderbird.core.logging.LogSink
/**
* CompositeLogSinkManager is responsible for managing a collection of [LogSink] instances.
*/
interface CompositeLogSinkManager {
/**
* Retrieves all [LogSink] instances managed by this manager.
*
* @return A list of all sinks.
*/
fun getAll(): List<LogSink>
/**
* Adds a [LogSink] to the manager.
*
* @param sink The [LogSink] to add.
*/
fun add(sink: LogSink)
/**
* Adds multiple [LogSink] instances to the manager.
*
* @param sinks The list of [LogSink] to add.
*/
fun addAll(sinks: List<LogSink>)
/**
* Removes a [LogSink] from the manager.
*
* @param sink The [LogSink] to remove.
*/
fun remove(sink: LogSink)
/**
* Removes all [LogSink] instances from the manager.
*/
fun removeAll()
}

View file

@ -0,0 +1,28 @@
package net.thunderbird.core.logging.composite
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogLevelProvider
import net.thunderbird.core.logging.LogSink
internal class DefaultCompositeLogSink(
private val logLevelProvider: LogLevelProvider,
override val manager: CompositeLogSinkManager = DefaultLogSinkManager(),
sinks: List<LogSink> = emptyList(),
) : CompositeLogSink {
override val level: LogLevel get() = logLevelProvider.current()
init {
manager.addAll(sinks)
}
override fun log(event: LogEvent) {
if (canLog(event.level)) {
manager.getAll().forEach { sink ->
if (sink.canLog(event.level)) {
sink.log(event)
}
}
}
}
}

View file

@ -0,0 +1,36 @@
package net.thunderbird.core.logging.composite
import net.thunderbird.core.logging.LogSink
/**
* Default implementation of [CompositeLogSinkManager] that manages a collection of [LogSink] instances.
*/
internal class DefaultLogSinkManager : CompositeLogSinkManager {
private val sinks: MutableList<LogSink> = mutableListOf()
override fun getAll(): List<LogSink> {
return sinks.toList()
}
override fun addAll(sinks: List<LogSink>) {
sinks.forEach {
add(it)
}
}
override fun add(sink: LogSink) {
if (sink !in sinks) {
sinks.add(sink)
}
}
override fun remove(sink: LogSink) {
if (sink in sinks) {
sinks.remove(sink)
}
}
override fun removeAll() {
sinks.clear()
}
}

View file

@ -0,0 +1,106 @@
package net.thunderbird.core.logging.composite
import assertk.assertThat
import assertk.assertions.hasSize
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
import org.junit.Test
class DefaultCompositeLogSinkTest {
@Test
fun `init should set initial sinks`() {
// Arrange
val sink1 = FakeLogSink(LogLevel.INFO)
val sink2 = FakeLogSink(LogLevel.INFO)
val sinkManager = FakeCompositeLogSinkManager()
// Act
DefaultCompositeLogSink(
logLevelProvider = { LogLevel.INFO },
manager = sinkManager,
sinks = listOf(sink1, sink2),
)
// Assert
assertThat(sinkManager.sinks).hasSize(2)
assertThat(sinkManager.sinks[0]).isEqualTo(sink1)
assertThat(sinkManager.sinks[1]).isEqualTo(sink2)
}
@Test
fun `log should log to all sinks`() {
// Arrange
val sink1 = FakeLogSink(LogLevel.INFO)
val sink2 = FakeLogSink(LogLevel.INFO)
val sinkManager = FakeCompositeLogSinkManager(mutableListOf(sink1, sink2))
val testSubject = DefaultCompositeLogSink(
logLevelProvider = { LogLevel.INFO },
manager = sinkManager,
)
// Act
testSubject.log(LOG_EVENT)
// Assert
assertThat(sink1.events).hasSize(1)
assertThat(sink2.events).hasSize(1)
assertThat(sink1.events[0]).isEqualTo(LOG_EVENT)
assertThat(sink2.events[0]).isEqualTo(LOG_EVENT)
}
@Test
fun `log should not log if level is below threshold`() {
// Arrange
val sink1 = FakeLogSink(LogLevel.INFO)
val sink2 = FakeLogSink(LogLevel.INFO)
val sinkManager = FakeCompositeLogSinkManager(mutableListOf(sink1, sink2))
val testSubject = DefaultCompositeLogSink(
logLevelProvider = { LogLevel.WARN },
manager = sinkManager,
)
// Act
testSubject.log(LOG_EVENT)
// Assert
assertThat(sink1.events).isEmpty()
assertThat(sink2.events).isEmpty()
}
@Test
fun `log should not log if sink level is below threshold`() {
// Arrange
val sink1 = FakeLogSink(LogLevel.WARN)
val sink2 = FakeLogSink(LogLevel.INFO)
val sinkManager = FakeCompositeLogSinkManager(mutableListOf(sink1, sink2))
val testSubject = DefaultCompositeLogSink(
logLevelProvider = { LogLevel.INFO },
manager = sinkManager,
)
// Act
testSubject.log(LOG_EVENT)
// Assert
assertThat(sink1.events).isEmpty()
assertThat(sink2.events).hasSize(1)
assertThat(sink2.events[0]).isEqualTo(LOG_EVENT)
}
private companion object Companion {
const val TIMESTAMP = 0L
val LOG_EVENT = LogEvent(
level = LogLevel.INFO,
tag = "TestTag",
message = "Test message",
timestamp = TIMESTAMP,
)
}
}

View file

@ -0,0 +1,115 @@
package net.thunderbird.core.logging.composite
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasSize
import assertk.assertions.isEmpty
import kotlin.test.Test
import net.thunderbird.core.logging.LogLevel
class DefaultLogSinkManagerTest {
@Test
fun `should have no sinks initially`() {
// Arrange
val sinkManager = DefaultLogSinkManager()
// Act
val sinks = sinkManager.getAll()
// Assert
assertThat(sinks).isEmpty()
}
@Test
fun `should add and retrieve sinks`() {
// Arrange
val sinkManager = DefaultLogSinkManager()
val sink = FakeLogSink(LogLevel.INFO)
sinkManager.add(sink)
// Act
val sinks = sinkManager.getAll()
// Assert
assertThat(sinks.contains(sink))
}
@Test
fun `should add multiple sinks`() {
// Arrange
val sinkManager = DefaultLogSinkManager()
val sink1 = FakeLogSink(LogLevel.INFO)
val sink2 = FakeLogSink(LogLevel.DEBUG)
sinkManager.addAll(listOf(sink1, sink2))
// Act
val sinks = sinkManager.getAll()
// Assert
assertThat(sinks).hasSize(2)
assertThat(sinks).contains(sink1)
assertThat(sinks).contains(sink2)
}
@Test
fun `should remove sink`() {
// Arrange
val sinkManager = DefaultLogSinkManager()
val sink = FakeLogSink(LogLevel.INFO)
sinkManager.add(sink)
// Act
sinkManager.remove(sink)
val sinks = sinkManager.getAll()
// Assert
assertThat(sinks).isEmpty()
}
@Test
fun `should clear all sinks`() {
// Arrange
val sinkManager = DefaultLogSinkManager()
val sink1 = FakeLogSink(LogLevel.INFO)
val sink2 = FakeLogSink(LogLevel.DEBUG)
sinkManager.add(sink1)
sinkManager.add(sink2)
// Act
sinkManager.removeAll()
val sinks = sinkManager.getAll()
// Assert
assertThat(sinks).isEmpty()
}
@Test
fun `should not add duplicate sinks`() {
// Arrange
val sinkManager = DefaultLogSinkManager()
val sink = FakeLogSink(LogLevel.INFO)
sinkManager.add(sink)
// Act
sinkManager.add(sink)
val sinks = sinkManager.getAll()
// Assert
assertThat(sinks).hasSize(1)
}
@Test
fun `should not remove non-existent sinks`() {
// Arrange
val sinkManager = DefaultLogSinkManager()
val sink = FakeLogSink(LogLevel.INFO)
// Act
sinkManager.remove(sink)
val sinks = sinkManager.getAll()
// Assert
assertThat(sinks).isEmpty()
}
}

View file

@ -0,0 +1,20 @@
package net.thunderbird.core.logging.composite
import net.thunderbird.core.logging.LogSink
class FakeCompositeLogSinkManager(
val sinks: MutableList<LogSink> = mutableListOf(),
) : CompositeLogSinkManager {
override fun getAll(): List<LogSink> = sinks
override fun add(sink: LogSink) = Unit
override fun addAll(sinks: List<LogSink>) {
this.sinks.addAll(sinks)
}
override fun remove(sink: LogSink) = Unit
override fun removeAll() = Unit
}

View file

@ -0,0 +1,14 @@
package net.thunderbird.core.logging.composite
import net.thunderbird.core.logging.LogEvent
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogSink
class FakeLogSink(override val level: LogLevel) : LogSink {
val events = mutableListOf<LogEvent>()
override fun log(event: LogEvent) {
events.add(event)
}
}