Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
19
core/logging/api/build.gradle.kts
Normal file
19
core/logging/api/build.gradle.kts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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),
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue