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,19 @@
plugins {
id(ThunderbirdPlugins.Library.kmp)
}
android {
namespace = "net.thunderbird.core.logging"
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.datetime)
}
commonTest.dependencies {
implementation(projects.core.testing)
}
}
}

View file

@ -0,0 +1,106 @@
package net.thunderbird.core.logging
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
/**
* Default implementation of [Logger] that logs messages to a [LogSink].
*
* @param sink The [LogSink] to which log events will be sent.
* @param clock The [Clock] used to get the current time for log events. Defaults to the system clock.
*/
class DefaultLogger
@OptIn(ExperimentalTime::class)
constructor(
private val sink: LogSink,
private val clock: Clock = Clock.System,
) : Logger {
private fun log(
level: LogLevel,
tag: LogTag? = null,
throwable: Throwable? = null,
message: () -> LogMessage,
) {
sink.let { currentSink ->
if (currentSink.canLog(level)) {
@OptIn(ExperimentalTime::class)
val timestamp = clock.now().toEpochMilliseconds()
currentSink.log(
event = LogEvent(
level = level,
tag = tag,
message = message(),
throwable = throwable,
timestamp = timestamp,
),
)
}
}
}
override fun verbose(
tag: LogTag?,
throwable: Throwable?,
message: () -> LogMessage,
) {
log(
level = LogLevel.VERBOSE,
tag = tag,
throwable = throwable,
message = message,
)
}
override fun debug(
tag: LogTag?,
throwable: Throwable?,
message: () -> LogMessage,
) {
log(
level = LogLevel.DEBUG,
tag = tag,
throwable = throwable,
message = message,
)
}
override fun info(
tag: LogTag?,
throwable: Throwable?,
message: () -> LogMessage,
) {
log(
level = LogLevel.INFO,
tag = tag,
throwable = throwable,
message = message,
)
}
override fun warn(
tag: LogTag?,
throwable: Throwable?,
message: () -> LogMessage,
) {
log(
level = LogLevel.WARN,
tag = tag,
throwable = throwable,
message = message,
)
}
override fun error(
tag: LogTag?,
throwable: Throwable?,
message: () -> LogMessage,
) {
log(
level = LogLevel.ERROR,
tag = tag,
throwable = throwable,
message = message,
)
}
}

View file

@ -0,0 +1,21 @@
package net.thunderbird.core.logging
typealias LogTag = String
typealias LogMessage = String
/**
* Represents a single log event
*
* @property level The [LogLevel] of the log event.
* @property tag An optional [LogTag] to categorize the log event.
* @property message The [LogMessage] associated with the log event.
* @property throwable An optional [Throwable] associated with the log event.
* @property timestamp The timestamp of the log event in milliseconds.
*/
data class LogEvent(
val level: LogLevel,
val tag: LogTag? = null,
val message: LogMessage,
val throwable: Throwable? = null,
val timestamp: Long,
)

View file

@ -0,0 +1,44 @@
package net.thunderbird.core.logging
/**
* Represents the different levels of logging used to filter log messages.
*
* The log levels are ordered by priority, where a lower number indicates a more verbose level.
* - [VERBOSE]: Most detailed log level, including all messages.
* - [DEBUG]: Detailed information, typically useful for diagnosing problems.
* - [INFO]: General information about the application state.
* - [WARN]: Indicates something unexpected but not necessarily an error.
* - [ERROR]: Indicates a failure or critical issue.
*
* Each log level has a priority, the higher the priority, the more important the log message is.
*
* @param priority The priority of the log level, where a lower priority indicates a more verbose level.
*/
enum class LogLevel(
val priority: Int,
) {
/**
* Verbose log level most detailed log level, including all messages.
*/
VERBOSE(1),
/**
* Debug log level detailed information, typically useful for diagnosing problems.
*/
DEBUG(2),
/**
* Informational log level general information about the application state.
*/
INFO(3),
/**
* Warning log level indicates something unexpected but not necessarily an error.
*/
WARN(4),
/**
* Error log level indicates a failure or critical issue.
*/
ERROR(5),
}

View file

@ -0,0 +1,28 @@
package net.thunderbird.core.logging
/**
* Manages the log level for the application.
*
* This interface provides a way to update the log level dynamically.
* Implementations of this interface are responsible for persisting the log level
* and notifying listeners of changes.
*/
interface LogLevelManager : LogLevelProvider {
/**
* Overrides the current log level.
*
* This function allows for a temporary change in the log level,
* typically for debugging or specific operational needs.
* The original log level can be restored by calling [restoreDefault] function
*
* @param level The new log level to set
*/
fun override(level: LogLevel)
/**
* Restores the log level to its default value.
*
* The default log level is defined by the specific implementation of this interface.
*/
fun restoreDefault()
}

View file

@ -0,0 +1,15 @@
package net.thunderbird.core.logging
/**
* Provides the current [LogLevel].
*
* This can be used to dynamically change the log level during runtime.
*/
fun interface LogLevelProvider {
/**
* Gets the current log level.
*
* @return The current log level.
*/
fun current(): LogLevel
}

View file

@ -0,0 +1,35 @@
package net.thunderbird.core.logging
/**
* A sink that receives and processes log events.
*
* A `LogSink` determines whether to handle a log event based on its log level.
* Log events with a level lower than the sink's configured level will be ignored.
*/
interface LogSink {
/**
* The minimum log level this sink will process.
* Log events with a lower priority than this level will be ignored.
*/
val level: LogLevel
/**
* Checks whether the sink is enabled for the given log level.
*
* @param level The log level to check.
* @return `true` if this sink will process log events at this level or higher.
*/
fun canLog(level: LogLevel): Boolean {
return this.level <= level
}
/**
* Logs a [LogEvent].
*
* @param event The [LogEvent] to log.
*/
fun log(
event: LogEvent,
)
}

View file

@ -0,0 +1,71 @@
package net.thunderbird.core.logging
/**
* A logging interface that provides methods for logging messages at specific log levels.
*/
interface Logger {
/**
* Logs a message at the verbose log level.
*
* @param tag An optional [LogTag] to categorize the log message.
* @param throwable An optional throwable to log.
* @param message Lambda that returns the [LogMessage] to log.
*/
fun verbose(
tag: LogTag? = null,
throwable: Throwable? = null,
message: () -> LogMessage,
)
/**
* Logs a message at the debug log level.
*
* @param tag An optional [LogTag] to categorize the log message.
* @param throwable An optional throwable to log.
* @param message Lambda that returns the [LogMessage] to log.
*/
fun debug(
tag: LogTag? = null,
throwable: Throwable? = null,
message: () -> LogMessage,
)
/**
* Logs a message at the info log level.
*
* @param tag An optional [LogTag] to categorize the log message.
* @param throwable An optional throwable to log.
* @param message Lambda that returns the [LogMessage] to log.
*/
fun info(
tag: LogTag? = null,
throwable: Throwable? = null,
message: () -> LogMessage,
)
/**
* Logs a message at the warn log level.
*
* @param tag An optional [LogTag] to categorize the log message.
* @param throwable An optional throwable to log.
* @param message Lambda that returns the [LogMessage] to log.
*/
fun warn(
tag: LogTag? = null,
throwable: Throwable? = null,
message: () -> LogMessage,
)
/**
* Logs a message at the error log level.
*
* @param tag An optional [LogTag] to categorize the log message.
* @param throwable An optional throwable to log.
* @param message Lambda that returns the [LogMessage] to log.
*/
fun error(
tag: LogTag? = null,
throwable: Throwable? = null,
message: () -> LogMessage,
)
}

View file

@ -0,0 +1,167 @@
package net.thunderbird.core.logging
import assertk.assertThat
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import net.thunderbird.core.testing.TestClock
@OptIn(ExperimentalTime::class)
class DefaultLoggerTest {
@Test
fun `log should add all event to the sink`() {
// Arrange
val sink = FakeLogSink(LogLevel.VERBOSE)
val exceptionVerbose = Exception("Verbose exception")
val exceptionDebug = Exception("Debug exception")
val exceptionInfo = Exception("Info exception")
val exceptionWarn = Exception("Warn exception")
val exceptionError = Exception("Error exception")
val clock = TestClock(
currentTime = Instant.fromEpochMilliseconds(0L),
)
val testSubject = DefaultLogger(
sink = sink,
clock = clock,
)
// Act
testSubject.verbose(
tag = "Verbose tag",
throwable = exceptionVerbose,
message = { "Verbose message" },
)
clock.advanceTimeBy(1000.milliseconds)
testSubject.debug(
tag = "Debug tag",
throwable = exceptionDebug,
message = { "Debug message" },
)
clock.advanceTimeBy(1000.milliseconds)
testSubject.info(
tag = "Info tag",
throwable = exceptionInfo,
message = { "Info message" },
)
clock.advanceTimeBy(1000.milliseconds)
testSubject.warn(
tag = "Warn tag",
throwable = exceptionWarn,
message = { "Warn message" },
)
clock.advanceTimeBy(1000.milliseconds)
testSubject.error(
tag = "Error tag",
throwable = exceptionError,
message = { "Error message" },
)
// Assert
val events = sink.events
assertThat(events).hasSize(5)
assertThat(events[0]).isEqualTo(
LogEvent(
level = LogLevel.VERBOSE,
tag = "Verbose tag",
message = "Verbose message",
throwable = exceptionVerbose,
timestamp = 0,
),
)
assertThat(events[1]).isEqualTo(
LogEvent(
level = LogLevel.DEBUG,
tag = "Debug tag",
message = "Debug message",
throwable = exceptionDebug,
timestamp = 1000,
),
)
assertThat(events[2]).isEqualTo(
LogEvent(
level = LogLevel.INFO,
tag = "Info tag",
message = "Info message",
throwable = exceptionInfo,
timestamp = 2000,
),
)
assertThat(events[3]).isEqualTo(
LogEvent(
level = LogLevel.WARN,
tag = "Warn tag",
message = "Warn message",
throwable = exceptionWarn,
timestamp = 3000,
),
)
assertThat(events[4]).isEqualTo(
LogEvent(
level = LogLevel.ERROR,
tag = "Error tag",
message = "Error message",
throwable = exceptionError,
timestamp = 4000,
),
)
}
@Test
fun `log should not add event to the sink if the level is not allowed for the sink`() {
// Arrange
val sink = FakeLogSink(LogLevel.INFO)
val exceptionVerbose = Exception("Verbose exception")
val exceptionDebug = Exception("Debug exception")
val exceptionInfo = Exception("Info exception")
val clock = TestClock(
currentTime = Instant.fromEpochMilliseconds(0L),
)
val testSubject = DefaultLogger(
sink = sink,
clock = clock,
)
// Act
testSubject.verbose(
tag = "Verbose tag",
throwable = exceptionVerbose,
message = { "Verbose message" },
)
clock.advanceTimeBy(1000.milliseconds)
testSubject.debug(
tag = "Debug tag",
throwable = exceptionDebug,
message = { "Debug message" },
)
clock.advanceTimeBy(1000.milliseconds)
testSubject.info(
tag = "Info tag",
throwable = exceptionInfo,
message = { "Info message" },
)
// Assert
assertThat(sink.events).hasSize(1)
assertThat(sink.events[0]).isEqualTo(
LogEvent(
level = LogLevel.INFO,
tag = "Info tag",
message = "Info message",
throwable = exceptionInfo,
timestamp = 2000,
),
)
}
}

View file

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

View file

@ -0,0 +1,49 @@
package net.thunderbird.core.logging
class FakeLogger : Logger {
val events = mutableListOf<LogEvent>()
override fun verbose(
tag: String?,
throwable: Throwable?,
message: () -> String,
) {
events.add(LogEvent(LogLevel.VERBOSE, tag, message(), throwable, TIMESTAMP))
}
override fun debug(
tag: String?,
throwable: Throwable?,
message: () -> String,
) {
events.add(LogEvent(LogLevel.DEBUG, tag, message(), throwable, TIMESTAMP))
}
override fun info(
tag: String?,
throwable: Throwable?,
message: () -> String,
) {
events.add(LogEvent(LogLevel.INFO, tag, message(), throwable, TIMESTAMP))
}
override fun warn(
tag: String?,
throwable: Throwable?,
message: () -> String,
) {
events.add(LogEvent(LogLevel.WARN, tag, message(), throwable, TIMESTAMP))
}
override fun error(
tag: String?,
throwable: Throwable?,
message: () -> String,
) {
events.add(LogEvent(LogLevel.ERROR, tag, message(), throwable, TIMESTAMP))
}
private companion object {
const val TIMESTAMP = 0L
}
}

View file

@ -0,0 +1,41 @@
package net.thunderbird.core.logging
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.junit.Test
class LogSinkTest {
@Test
fun `canLog should return true for same level`() {
// Arrange
val testSubject = TestLogSink(LogLevel.INFO)
// Act && Assert
assertTrue { testSubject.canLog(LogLevel.INFO) }
}
@Test
fun `canLog should return false for level below sink level`() {
// Arrange
val testSubject = TestLogSink(LogLevel.INFO)
// Act && Assert
assertFalse { testSubject.canLog(LogLevel.DEBUG) }
}
@Test
fun `canLog should return true for level above sink level`() {
// Arrange
val testSubject = TestLogSink(LogLevel.INFO)
// Act && Assert
assertTrue { testSubject.canLog(LogLevel.WARN) }
}
private class TestLogSink(
override val level: LogLevel,
) : LogSink {
override fun log(event: LogEvent) = Unit
}
}