Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
205
core/logging/README.md
Normal file
205
core/logging/README.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# Thunderbird Core Logging Module
|
||||
|
||||
This module provides a flexible and extensible logging system for Thunderbird for Android.
|
||||
|
||||
## Architecture
|
||||
|
||||
The logging system is organized into several modules:
|
||||
|
||||
- **api**: Core interfaces and classes
|
||||
- **impl-console**: Console logging implementation
|
||||
- **impl-composite**: Composite logging (multiple sinks)
|
||||
- **impl-legacy**: Legacy logging system compatibility
|
||||
- **testing**: Testing utilities
|
||||
|
||||
### Core Components
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Logger {
|
||||
+verbose(tag, throwable, message: () -> LogMessage)
|
||||
+debug(tag, throwable, message: () -> LogMessage)
|
||||
+info(tag, throwable, message: () -> LogMessage)
|
||||
+warn(tag, throwable, message: () -> LogMessage)
|
||||
+error(tag, throwable, message: () -> LogMessage)
|
||||
}
|
||||
|
||||
class DefaultLogger {
|
||||
-sink: LogSink
|
||||
-clock: Clock
|
||||
}
|
||||
|
||||
class LogSink {
|
||||
+level: LogLevel
|
||||
+canLog(level): boolean
|
||||
+log(event: LogEvent)
|
||||
}
|
||||
|
||||
class LogEvent {
|
||||
+level: LogLevel
|
||||
+tag: LogTag?
|
||||
+message: LogMessage
|
||||
+throwable: Throwable?
|
||||
+timestamp: Long
|
||||
}
|
||||
|
||||
class LogLevel {
|
||||
VERBOSE
|
||||
DEBUG
|
||||
INFO
|
||||
WARN
|
||||
ERROR
|
||||
}
|
||||
|
||||
Logger <|-- DefaultLogger
|
||||
DefaultLogger --> LogSink
|
||||
LogSink --> LogEvent
|
||||
LogSink --> LogLevel
|
||||
LogEvent --> LogLevel
|
||||
```
|
||||
|
||||
### Implementation Modules
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class LogSink {
|
||||
+level: LogLevel
|
||||
+canLog(level): boolean
|
||||
+log(event: LogEvent)
|
||||
}
|
||||
|
||||
class ConsoleLogSink {
|
||||
+level: LogLevel
|
||||
}
|
||||
|
||||
class CompositeLogSink {
|
||||
+level: LogLevel
|
||||
-manager: LogSinkManager
|
||||
}
|
||||
|
||||
LogSink <|-- ConsoleLogSink
|
||||
LogSink <|-- CompositeLogSink
|
||||
|
||||
CompositeLogSink --> LogSinkManager
|
||||
|
||||
class LogSinkManager {
|
||||
+getAll(): List<LogSink>
|
||||
+add(sink: LogSink)
|
||||
+addAll(sinks: List<LogSink>)
|
||||
+remove(sink: LogSink)
|
||||
+removeAll()
|
||||
}
|
||||
|
||||
class DefaultLogSinkManager {
|
||||
-sinks: MutableList<LogSink>
|
||||
}
|
||||
|
||||
LogSinkManager <|-- DefaultLogSinkManager
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Basic Setup
|
||||
|
||||
To start using the logging system, you need to:
|
||||
|
||||
1. Add the necessary dependencies to your module's build.gradle.kts file
|
||||
2. Create a LogSink
|
||||
3. Create a Logger
|
||||
4. Start logging!
|
||||
|
||||
### Basic Logging
|
||||
|
||||
```kotlin
|
||||
// Create a log sink
|
||||
val sink = ConsoleLogSink(LogLevel.DEBUG)
|
||||
|
||||
// Create a logger
|
||||
val logger = DefaultLogger(sink)
|
||||
|
||||
// Log messages
|
||||
logger.debug(tag = "MyTag") { "Debug message" }
|
||||
logger.info { "Info message" }
|
||||
logger.warn { "Warning message" }
|
||||
logger.error(throwable = exception) { "Error message with exception" }
|
||||
```
|
||||
|
||||
Note that the message parameter is a lambda that returns a String. This allows for lazy evaluation of the message, which can improve performance when the log level is set to filter out certain messages.
|
||||
|
||||
### Composite Logging (Multiple Sinks)
|
||||
|
||||
If you want to send logs to multiple destinations, use the CompositeLogSink:
|
||||
|
||||
```kotlin
|
||||
// Create log sinks
|
||||
val consoleSink = ConsoleLogSink(LogLevel.INFO)
|
||||
val otherSink = YourCustomLogSink(LogLevel.DEBUG)
|
||||
|
||||
// Create a composite sink
|
||||
val compositeSink = CompositeLogSink(
|
||||
level = LogLevel.DEBUG,
|
||||
sinks = listOf(
|
||||
consoleSink,
|
||||
otherSink
|
||||
)
|
||||
)
|
||||
|
||||
// Create a logger
|
||||
val logger = DefaultLogger(compositeSink)
|
||||
|
||||
// Log messages (will go to both sinks if level is appropriate)
|
||||
logger.debug { "This goes only to otherSink if its level is DEBUG or lower" }
|
||||
logger.info { "This goes to both sinks if their levels are INFO or lower" }
|
||||
```
|
||||
|
||||
## Creating Custom Log Sinks
|
||||
|
||||
You can create your own log sink by implementing the LogSink interface:
|
||||
|
||||
```kotlin
|
||||
class MyCustomLogSink(
|
||||
override val level: LogLevel,
|
||||
// Add any other parameters you need
|
||||
) : LogSink {
|
||||
override fun log(event: LogEvent) {
|
||||
// Implement your custom logging logic here
|
||||
// For example, send logs to a remote server, write to a database, etc.
|
||||
val formattedMessage = "${event.timestamp} [${event.level}] ${event.tag ?: ""}: ${event.message}"
|
||||
|
||||
// Handle the throwable if present
|
||||
event.throwable?.let {
|
||||
// Process the throwable
|
||||
}
|
||||
|
||||
// Send or store the log message
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Log Levels
|
||||
|
||||
Use appropriate log levels for different types of messages:
|
||||
|
||||
- **VERBOSE**: Detailed information, typically useful only for debugging
|
||||
- **DEBUG**: Debugging information, useful during development
|
||||
- **INFO**: General information about application operation
|
||||
- **WARN**: Potential issues that aren't errors but might need attention
|
||||
- **ERROR**: Errors and exceptions that should be investigated
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **No logs appearing**:
|
||||
- Check that the log level of your sink is appropriate for the messages you're logging
|
||||
- Verify that your logger is properly initialized
|
||||
|
||||
### Debugging the Logging System
|
||||
|
||||
To debug issues with the logging system itself:
|
||||
|
||||
1. Create a simple ConsoleLogSink with VERBOSE level
|
||||
2. Log test messages at different levels
|
||||
3. Check if messages appear as expected
|
||||
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
|
||||
}
|
||||
}
|
||||
15
core/logging/impl-composite/build.gradle.kts
Normal file
15
core/logging/impl-composite/build.gradle.kts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
32
core/logging/impl-console/build.gradle.kts
Normal file
32
core/logging/impl-console/build.gradle.kts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
16
core/logging/impl-file/build.gradle.kts
Normal file
16
core/logging/impl-file/build.gradle.kts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.kmp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.thunderbird.core.logging.file"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.kotlinx.io.core)
|
||||
implementation(projects.core.logging.api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.io.Buffer
|
||||
import kotlinx.io.RawSink
|
||||
import kotlinx.io.asSink
|
||||
import net.thunderbird.core.logging.LogEvent
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
|
||||
private const val BUFFER_SIZE = 8192 // 8KB buffer size
|
||||
private const val LOG_BUFFER_COUNT = 4
|
||||
|
||||
open class AndroidFileLogSink(
|
||||
override val level: LogLevel,
|
||||
fileName: String,
|
||||
fileLocation: String,
|
||||
private val fileSystemManager: FileSystemManager,
|
||||
coroutineContext: CoroutineContext = Dispatchers.IO,
|
||||
) : FileLogSink {
|
||||
|
||||
private val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
private val logFile = File(fileLocation, "$fileName.txt")
|
||||
private val accumulatedLogs = ArrayList<String>()
|
||||
private val mutex: Mutex = Mutex()
|
||||
|
||||
// Make sure the directory exists
|
||||
init {
|
||||
val directory = File(fileLocation)
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
logFile.createNewFile()
|
||||
}
|
||||
|
||||
override fun log(event: LogEvent) {
|
||||
coroutineScope.launch {
|
||||
mutex.withLock {
|
||||
accumulatedLogs.add(
|
||||
"${convertLongToTime(event.timestamp)} priority = ${event.level}, ${event.message}",
|
||||
)
|
||||
}
|
||||
if (accumulatedLogs.size > LOG_BUFFER_COUNT) {
|
||||
writeToLogFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
private fun convertLongToTime(long: Long): String {
|
||||
val instant = Instant.fromEpochMilliseconds(long)
|
||||
val dateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
return LocalDateTime.Formats.ISO.format(dateTime)
|
||||
}
|
||||
|
||||
private suspend fun writeToLogFile() {
|
||||
val outputStream = FileOutputStream(logFile, true)
|
||||
val sink = outputStream.asSink()
|
||||
var content: String
|
||||
try {
|
||||
mutex.withLock {
|
||||
content = accumulatedLogs.joinToString("\n", postfix = "\n")
|
||||
accumulatedLogs.clear()
|
||||
}
|
||||
val buffer = Buffer()
|
||||
val contentBytes = content.toByteArray(Charsets.UTF_8)
|
||||
buffer.write(contentBytes)
|
||||
sink.write(buffer, buffer.size)
|
||||
|
||||
sink.flush()
|
||||
} finally {
|
||||
sink.close()
|
||||
outputStream.close()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun flushAndCloseBuffer() {
|
||||
if (accumulatedLogs.isNotEmpty()) {
|
||||
writeToLogFile()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun export(uriString: String) {
|
||||
if (accumulatedLogs.isNotEmpty()) {
|
||||
writeToLogFile()
|
||||
}
|
||||
val sink = fileSystemManager.openSink(uriString, "wt")
|
||||
?: error("Error opening contentUri for writing")
|
||||
|
||||
copyInternalFileToExternal(sink)
|
||||
|
||||
// Clear the log file after export
|
||||
val outputStream = FileOutputStream(logFile)
|
||||
val clearSink = outputStream.asSink()
|
||||
|
||||
try {
|
||||
// Write empty string to clear the file
|
||||
val buffer = Buffer()
|
||||
clearSink.write(buffer, 0)
|
||||
clearSink.flush()
|
||||
} finally {
|
||||
clearSink.close()
|
||||
outputStream.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyInternalFileToExternal(sink: RawSink) {
|
||||
val inputStream = FileInputStream(logFile)
|
||||
|
||||
try {
|
||||
val buffer = Buffer()
|
||||
val byteArray = ByteArray(BUFFER_SIZE)
|
||||
var bytesRead: Int
|
||||
|
||||
while (inputStream.read(byteArray).also { bytesRead = it } != -1) {
|
||||
buffer.write(byteArray, 0, bytesRead)
|
||||
sink.write(buffer, buffer.size)
|
||||
buffer.clear()
|
||||
}
|
||||
|
||||
sink.flush()
|
||||
} finally {
|
||||
inputStream.close()
|
||||
sink.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.io.RawSink
|
||||
import kotlinx.io.asSink
|
||||
|
||||
/**
|
||||
* Android implementation of [FileSystemManager] that uses [ContentResolver] to perform file operations.
|
||||
*/
|
||||
class AndroidFileSystemManager(
|
||||
private val contentResolver: ContentResolver,
|
||||
) : FileSystemManager {
|
||||
override fun openSink(uriString: String, mode: String): RawSink? {
|
||||
val uri: Uri = uriString.toUri()
|
||||
return contentResolver.openOutputStream(uri, mode)?.asSink()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
|
||||
actual fun FileLogSink(
|
||||
level: LogLevel,
|
||||
fileName: String,
|
||||
fileLocation: String,
|
||||
fileSystemManager: FileSystemManager,
|
||||
): FileLogSink {
|
||||
return AndroidFileLogSink(level, fileName, fileLocation, fileSystemManager)
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNotNull
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import net.thunderbird.core.logging.LogEvent
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.rules.TemporaryFolder
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AndroidFileLogSinkTest {
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val folder = TemporaryFolder()
|
||||
|
||||
private val initialTimestamp = 1234567890L
|
||||
private lateinit var logFile: File
|
||||
private lateinit var fileLocation: String
|
||||
private lateinit var fileManager: FakeFileSystemManager
|
||||
private lateinit var testSubject: AndroidFileLogSink
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
fileLocation = folder.newFolder().absolutePath
|
||||
logFile = File(fileLocation, "test_log.txt")
|
||||
fileManager = FakeFileSystemManager()
|
||||
testSubject = AndroidFileLogSink(
|
||||
level = LogLevel.INFO,
|
||||
fileName = "test_log",
|
||||
fileLocation = fileLocation,
|
||||
fileSystemManager = fileManager,
|
||||
coroutineContext = UnconfinedTestDispatcher(),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun timeSetup(timeStamp: Long): String {
|
||||
val instant = Instant.fromEpochMilliseconds(timeStamp)
|
||||
val dateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
return LocalDateTime.Formats.ISO.format(dateTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldHaveCorrectLogLevel() {
|
||||
assertThat(testSubject.level).isEqualTo(LogLevel.INFO)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldLogMessageToFile() {
|
||||
// Arrange
|
||||
val message = "Test log message"
|
||||
val event = LogEvent(
|
||||
timestamp = initialTimestamp,
|
||||
level = LogLevel.INFO,
|
||||
tag = "TestTag",
|
||||
message = message,
|
||||
throwable = null,
|
||||
)
|
||||
|
||||
// Act
|
||||
testSubject.log(event)
|
||||
runBlocking {
|
||||
testSubject.flushAndCloseBuffer()
|
||||
}
|
||||
|
||||
// Arrange
|
||||
val logFile = File(fileLocation, "test_log.txt")
|
||||
assertThat(logFile.exists()).isEqualTo(true)
|
||||
assertThat(logFile.readText())
|
||||
.isEqualTo("${timeSetup(initialTimestamp)} priority = INFO, Test log message\n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldLogMultipleMessagesToFile() {
|
||||
// Arrange
|
||||
val message = "Test log message"
|
||||
var fiveLogString: String = ""
|
||||
for (num in 0..3) {
|
||||
val event = LogEvent(
|
||||
timestamp = initialTimestamp + num,
|
||||
level = LogLevel.INFO,
|
||||
tag = "TestTag",
|
||||
message = message + num,
|
||||
throwable = null,
|
||||
)
|
||||
testSubject.log(event)
|
||||
fiveLogString = fiveLogString + "${timeSetup(event.timestamp)} priority = INFO, ${event.message}\n"
|
||||
}
|
||||
|
||||
val logFile = File(fileLocation, "test_log.txt")
|
||||
assertThat(logFile.exists()).isEqualTo(true)
|
||||
assertThat(logFile.readText())
|
||||
.isEqualTo("")
|
||||
|
||||
val eventTippingBuffer = LogEvent(
|
||||
timestamp = initialTimestamp + 6,
|
||||
level = LogLevel.INFO,
|
||||
tag = "TestTag",
|
||||
message = message + "buffered",
|
||||
throwable = null,
|
||||
)
|
||||
fiveLogString =
|
||||
fiveLogString +
|
||||
"${timeSetup(eventTippingBuffer.timestamp)} priority = INFO, ${eventTippingBuffer.message}\n"
|
||||
testSubject.log(eventTippingBuffer)
|
||||
|
||||
// Arrange
|
||||
assertThat(logFile.exists()).isEqualTo(true)
|
||||
assertThat(logFile.readText())
|
||||
.isEqualTo(fiveLogString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldExportLogFile() {
|
||||
// Arrange
|
||||
val event = LogEvent(
|
||||
timestamp = initialTimestamp,
|
||||
level = LogLevel.INFO,
|
||||
tag = "TestTag",
|
||||
message = "Test log message for export",
|
||||
throwable = null,
|
||||
)
|
||||
testSubject.log(event)
|
||||
runBlocking {
|
||||
// Act
|
||||
testSubject.flushAndCloseBuffer()
|
||||
val exportUri = "content://test/export.txt"
|
||||
testSubject.export(exportUri)
|
||||
}
|
||||
|
||||
// Arrange
|
||||
val exportedContent = fileManager.exportedContent
|
||||
assertThat(exportedContent).isNotNull()
|
||||
assertThat(exportedContent!!)
|
||||
.isEqualTo("${timeSetup(initialTimestamp)} priority = INFO, Test log message for export\n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldClearBufferAndExportToFile() {
|
||||
// Arrange
|
||||
val message = "Test log message"
|
||||
var logString1: String = ""
|
||||
|
||||
for (num in 0..4) {
|
||||
val event = LogEvent(
|
||||
timestamp = initialTimestamp + num,
|
||||
level = LogLevel.INFO,
|
||||
tag = "TestTag",
|
||||
message = message + num,
|
||||
throwable = null,
|
||||
)
|
||||
testSubject.log(event)
|
||||
logString1 = logString1 + "${timeSetup(event.timestamp)} priority = INFO, ${event.message}\n"
|
||||
}
|
||||
val event = LogEvent(
|
||||
timestamp = initialTimestamp + 5,
|
||||
level = LogLevel.INFO,
|
||||
tag = "TestTag",
|
||||
message = message + 5,
|
||||
throwable = null,
|
||||
)
|
||||
testSubject.log(event)
|
||||
|
||||
var logString2: String = logString1 + "${timeSetup(event.timestamp)} priority = INFO, ${event.message}\n"
|
||||
|
||||
// Arrange
|
||||
val logFile = File(fileLocation, "test_log.txt")
|
||||
assertThat(logFile.exists()).isEqualTo(true)
|
||||
assertThat(logFile.readText())
|
||||
.isEqualTo(logString1)
|
||||
runBlocking {
|
||||
val exportUri = "content://test/export.txt"
|
||||
testSubject.export(exportUri)
|
||||
}
|
||||
|
||||
// Arrange
|
||||
val exportedContent = fileManager.exportedContent
|
||||
assertThat(exportedContent).isNotNull()
|
||||
assertThat(exportedContent!!)
|
||||
.isEqualTo(logString2)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlinx.io.Buffer
|
||||
import kotlinx.io.RawSink
|
||||
|
||||
class FakeFileSystemManager : FileSystemManager {
|
||||
|
||||
var exportedContent: String? = null
|
||||
private val outputStream = ByteArrayOutputStream()
|
||||
|
||||
override fun openSink(uriString: String, mode: String): RawSink? {
|
||||
return object : RawSink {
|
||||
override fun write(source: Buffer, byteCount: Long) {
|
||||
val bytes = ByteArray(byteCount.toInt())
|
||||
|
||||
for (i in 0 until byteCount.toInt()) {
|
||||
bytes[i] = source.readByte()
|
||||
}
|
||||
|
||||
outputStream.write(bytes)
|
||||
|
||||
exportedContent = String(outputStream.toByteArray(), StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
override fun flush() = Unit
|
||||
|
||||
override fun close() = Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
import net.thunderbird.core.logging.LogSink
|
||||
|
||||
interface FileLogSink : LogSink {
|
||||
/**
|
||||
* Exports from the logging method to the requested external file
|
||||
* @param uriString The [String] for the URI to export the log to
|
||||
*
|
||||
**/
|
||||
suspend fun export(uriString: String)
|
||||
|
||||
/**
|
||||
* On a crash or close, flushes buffer to file fo avoid log loss
|
||||
*
|
||||
**/
|
||||
suspend fun flushAndCloseBuffer()
|
||||
}
|
||||
|
||||
/**
|
||||
* A [LogSink] implementation that logs messages to a specified internal file.
|
||||
*
|
||||
* This sink uses the platform-specific implementations to handle logging.
|
||||
*
|
||||
* @param level The minimum [LogLevel] for messages to be logged.
|
||||
* @param fileName The [String] fileName to log to
|
||||
* @param fileLocation The [String] fileLocation for the log file
|
||||
* @param fileSystemManager The [FileSystemManager] abstraction for opening the file stream
|
||||
*/
|
||||
expect fun FileLogSink(
|
||||
level: LogLevel,
|
||||
fileName: String,
|
||||
fileLocation: String,
|
||||
fileSystemManager: FileSystemManager,
|
||||
): FileLogSink
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import kotlinx.io.RawSink
|
||||
|
||||
/**
|
||||
* An interface for file system operations that are platform-specific.
|
||||
*/
|
||||
interface FileSystemManager {
|
||||
/**
|
||||
* Opens a sink for writing to a URI.
|
||||
*
|
||||
* @param uriString The URI string to open a sink for
|
||||
* @param mode The mode to open the sink in (e.g., "wt" for write text)
|
||||
* @return A sink for writing to the URI, or null if the URI couldn't be opened
|
||||
*/
|
||||
fun openSink(uriString: String, mode: String): RawSink?
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
|
||||
/**
|
||||
* A [LogSink] implementation that logs messages to a specified internal file.
|
||||
*
|
||||
* This sink uses the platform-specific implementations to handle logging.
|
||||
*
|
||||
* @param level The minimum [LogLevel] for messages to be logged.
|
||||
* @param fileName The [String] fileName to log to
|
||||
*/
|
||||
actual fun FileLogSink(
|
||||
level: LogLevel,
|
||||
fileName: String,
|
||||
fileLocation: String,
|
||||
fileSystemManager: FileSystemManager,
|
||||
): FileLogSink {
|
||||
return JvmFileLogSink(level, fileName, fileLocation)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import net.thunderbird.core.logging.LogEvent
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
|
||||
internal class JvmFileLogSink(
|
||||
override val level: LogLevel,
|
||||
fileName: String,
|
||||
fileLocation: String,
|
||||
) : FileLogSink {
|
||||
|
||||
override fun log(event: LogEvent) {
|
||||
println("[$level] ${composeMessage(event)}")
|
||||
event.throwable?.printStackTrace()
|
||||
}
|
||||
|
||||
override suspend fun export(uriString: String) {
|
||||
// TODO: Implementation https://github.com/thunderbird/thunderbird-android/issues/9435
|
||||
}
|
||||
|
||||
override suspend fun flushAndCloseBuffer() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
private fun composeMessage(event: LogEvent): String {
|
||||
return if (event.tag != null) {
|
||||
"[${event.tag}] ${event.message}"
|
||||
} else {
|
||||
event.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package net.thunderbird.core.logging.file
|
||||
|
||||
import kotlinx.io.RawSink
|
||||
|
||||
/**
|
||||
* Android implementation of [FileSystemManager] that uses [ContentResolver] to perform file operations.
|
||||
*/
|
||||
class JvmFileSystemManager() : FileSystemManager {
|
||||
override fun openSink(uriString: String, mode: String): RawSink? {
|
||||
// TODO: Implementation https://github.com/thunderbird/thunderbird-android/issues/9435
|
||||
return TODO("Provide the return value")
|
||||
}
|
||||
}
|
||||
26
core/logging/impl-legacy/build.gradle.kts
Normal file
26
core/logging/impl-legacy/build.gradle.kts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.kmp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.thunderbird.core.logging.legacy"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
androidMain.dependencies {
|
||||
implementation(libs.timber)
|
||||
implementation(projects.core.logging.implComposite)
|
||||
implementation(projects.core.logging.implFile)
|
||||
}
|
||||
|
||||
commonMain.dependencies {
|
||||
api(projects.core.logging.api)
|
||||
api(libs.androidx.annotation)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(projects.core.logging.testing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package net.thunderbird.core.logging.legacy
|
||||
|
||||
import net.thunderbird.core.logging.composite.CompositeLogSink
|
||||
import net.thunderbird.core.logging.file.FileLogSink
|
||||
import timber.log.Timber
|
||||
import timber.log.Timber.DebugTree
|
||||
|
||||
// TODO: Implementation https://github.com/thunderbird/thunderbird-android/issues/9573
|
||||
class DebugLogConfigurator(
|
||||
private val syncDebugCompositeSink: CompositeLogSink,
|
||||
private val syncDebugFileLogSink: FileLogSink,
|
||||
) {
|
||||
fun updateLoggingStatus(isDebugLoggingEnabled: Boolean) {
|
||||
Timber.uprootAll()
|
||||
if (isDebugLoggingEnabled) {
|
||||
Timber.plant(DebugTree())
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSyncLogging(isSyncLoggingEnabled: Boolean) {
|
||||
if (isSyncLoggingEnabled) {
|
||||
syncDebugCompositeSink.manager.add(syncDebugFileLogSink)
|
||||
} else {
|
||||
syncDebugCompositeSink.manager.remove(syncDebugFileLogSink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package net.thunderbird.core.logging.legacy
|
||||
|
||||
import androidx.annotation.Discouraged
|
||||
import net.thunderbird.core.logging.LogMessage
|
||||
import net.thunderbird.core.logging.LogTag
|
||||
import net.thunderbird.core.logging.Logger
|
||||
|
||||
/**
|
||||
* A static logging utility that implements [net.thunderbird.core.logging.Logger] and delegates to a [net.thunderbird.core.logging.Logger] implementation.
|
||||
*
|
||||
* You can initialize it in your application startup code, for example:
|
||||
*
|
||||
* ```kotlin
|
||||
* import net.thunderbird.core.logging.Log
|
||||
* import net.thunderbird.core.logging.DefaultLogger // or any other Logger implementation
|
||||
* fun main() {
|
||||
* val sink: LogSink = // Your LogSink implementation
|
||||
* val logger: Logger = DefaultLogger(sink)
|
||||
*
|
||||
* Log.logger = logger
|
||||
* Log.i("Application started")
|
||||
* // Your application code here
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Discouraged(
|
||||
message = "Use a net.thunderbird.core.logging.Logger instance via dependency injection instead. " +
|
||||
"This class will be removed in a future release.",
|
||||
)
|
||||
object Log : Logger {
|
||||
|
||||
lateinit var logger: Logger
|
||||
|
||||
override fun verbose(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
logger.verbose(
|
||||
tag = tag,
|
||||
throwable = throwable,
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
|
||||
override fun debug(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
logger.debug(
|
||||
tag = tag,
|
||||
throwable = throwable,
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
|
||||
override fun info(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
logger.info(
|
||||
tag = tag,
|
||||
throwable = throwable,
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
|
||||
override fun warn(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
logger.warn(
|
||||
tag = tag,
|
||||
throwable = throwable,
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
logger.error(
|
||||
tag = tag,
|
||||
throwable = throwable,
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
|
||||
// Legacy Logger implementation
|
||||
|
||||
@JvmStatic
|
||||
fun v(message: String?, vararg args: Any?) {
|
||||
logger.verbose(message = { formatMessage(message, args) })
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun v(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
logger.verbose(message = { formatMessage(message, args) }, throwable = t)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun d(message: String?, vararg args: Any?) {
|
||||
logger.debug(message = { formatMessage(message, args) })
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun d(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
logger.debug(message = { formatMessage(message, args) }, throwable = t)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun i(message: String?, vararg args: Any?) {
|
||||
logger.info(message = { formatMessage(message, args) })
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun i(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
logger.info(message = { formatMessage(message, args) }, throwable = t)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun w(message: String?, vararg args: Any?) {
|
||||
logger.warn(message = { formatMessage(message, args) })
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun w(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
logger.warn(message = { formatMessage(message, args) }, throwable = t)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun e(message: String?, vararg args: Any?) {
|
||||
logger.error(message = { formatMessage(message, args) })
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun e(t: Throwable?, message: String?, vararg args: Any?) {
|
||||
logger.error(message = { formatMessage(message, args) }, throwable = t)
|
||||
}
|
||||
|
||||
private fun formatMessage(message: String?, args: Array<out Any?>): String {
|
||||
return if (message == null) {
|
||||
""
|
||||
} else if (args.isEmpty()) {
|
||||
message
|
||||
} else {
|
||||
try {
|
||||
String.format(message, *args)
|
||||
} catch (e: Exception) {
|
||||
"$message (Error formatting message: $e, args: ${args.joinToString()})"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
package net.thunderbird.core.logging.legacy
|
||||
|
||||
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 net.thunderbird.core.logging.testing.TestLogger
|
||||
import net.thunderbird.core.logging.testing.TestLogger.Companion.TIMESTAMP
|
||||
|
||||
class LogTest {
|
||||
|
||||
@Test
|
||||
fun `init should set logger`() {
|
||||
// Arrange
|
||||
val logger = TestLogger()
|
||||
|
||||
// Act
|
||||
Log.logger = logger
|
||||
Log.info(
|
||||
tag = "Test tag",
|
||||
message = { "Test message" },
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(logger.events).hasSize(1)
|
||||
assertThat(logger.events[0]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.INFO,
|
||||
tag = "Test tag",
|
||||
message = "Test message",
|
||||
throwable = null,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log should add all event to the logger`() {
|
||||
// Arrange
|
||||
val logger = TestLogger()
|
||||
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")
|
||||
|
||||
Log.logger = logger
|
||||
|
||||
// Act
|
||||
Log.verbose(
|
||||
tag = "Verbose tag",
|
||||
throwable = exceptionVerbose,
|
||||
message = { "Verbose message" },
|
||||
)
|
||||
Log.debug(
|
||||
tag = "Debug tag",
|
||||
throwable = exceptionDebug,
|
||||
message = { "Debug message" },
|
||||
)
|
||||
Log.info(
|
||||
tag = "Info tag",
|
||||
throwable = exceptionInfo,
|
||||
message = { "Info message" },
|
||||
)
|
||||
Log.warn(
|
||||
tag = "Warn tag",
|
||||
throwable = exceptionWarn,
|
||||
message = { "Warn message" },
|
||||
)
|
||||
Log.error(
|
||||
tag = "Error tag",
|
||||
throwable = exceptionError,
|
||||
message = { "Error message" },
|
||||
)
|
||||
|
||||
// Assert
|
||||
val events = logger.events
|
||||
assertThat(events).hasSize(5)
|
||||
assertThat(events[0]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.VERBOSE,
|
||||
tag = "Verbose tag",
|
||||
message = "Verbose message",
|
||||
throwable = exceptionVerbose,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
assertThat(events[1]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.DEBUG,
|
||||
tag = "Debug tag",
|
||||
message = "Debug message",
|
||||
throwable = exceptionDebug,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
assertThat(events[2]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.INFO,
|
||||
tag = "Info tag",
|
||||
message = "Info message",
|
||||
throwable = exceptionInfo,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
assertThat(events[3]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.WARN,
|
||||
tag = "Warn tag",
|
||||
message = "Warn message",
|
||||
throwable = exceptionWarn,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
assertThat(events[4]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.ERROR,
|
||||
tag = "Error tag",
|
||||
message = "Error message",
|
||||
throwable = exceptionError,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `legacy methods should log correctly`() {
|
||||
// Arrange
|
||||
val logger = TestLogger()
|
||||
val exception = Exception("Test exception")
|
||||
Log.logger = logger
|
||||
|
||||
// Act - Test all legacy method signatures for each log level
|
||||
|
||||
// Verbose methods
|
||||
Log.v("Verbose message %s", "arg1")
|
||||
Log.v(exception, "Verbose message with exception %s", "arg1")
|
||||
|
||||
// Debug methods
|
||||
Log.d("Debug message %s", "arg1")
|
||||
Log.d(exception, "Debug message with exception %s", "arg1")
|
||||
|
||||
// Info methods
|
||||
Log.i("Info message %s", "arg1")
|
||||
Log.i(exception, "Info message with exception %s", "arg1")
|
||||
|
||||
// Warn methods
|
||||
Log.w("Warn message %s", "arg1")
|
||||
Log.w(exception, "Warn message with exception %s", "arg1")
|
||||
|
||||
// Error methods
|
||||
Log.e("Error message %s", "arg1")
|
||||
Log.e(exception, "Error message with exception %s", "arg1")
|
||||
|
||||
// Assert
|
||||
val events = logger.events
|
||||
assertThat(events).hasSize(10)
|
||||
|
||||
// Verify verbose events
|
||||
assertThat(events[0]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.VERBOSE,
|
||||
tag = null,
|
||||
message = "Verbose message arg1",
|
||||
throwable = null,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
assertThat(events[1]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.VERBOSE,
|
||||
tag = null,
|
||||
message = "Verbose message with exception arg1",
|
||||
throwable = exception,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify debug events
|
||||
assertThat(events[2]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.DEBUG,
|
||||
tag = null,
|
||||
message = "Debug message arg1",
|
||||
throwable = null,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
assertThat(events[3]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.DEBUG,
|
||||
tag = null,
|
||||
message = "Debug message with exception arg1",
|
||||
throwable = exception,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify info events
|
||||
assertThat(events[4]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.INFO,
|
||||
tag = null,
|
||||
message = "Info message arg1",
|
||||
throwable = null,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
assertThat(events[5]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.INFO,
|
||||
tag = null,
|
||||
message = "Info message with exception arg1",
|
||||
throwable = exception,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify warn events
|
||||
assertThat(events[6]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.WARN,
|
||||
tag = null,
|
||||
message = "Warn message arg1",
|
||||
throwable = null,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
assertThat(events[7]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.WARN,
|
||||
tag = null,
|
||||
message = "Warn message with exception arg1",
|
||||
throwable = exception,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify error events
|
||||
assertThat(events[8]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.ERROR,
|
||||
tag = null,
|
||||
message = "Error message arg1",
|
||||
throwable = null,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
assertThat(events[9]).isEqualTo(
|
||||
LogEvent(
|
||||
level = LogLevel.ERROR,
|
||||
tag = null,
|
||||
message = "Error message with exception arg1",
|
||||
throwable = exception,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
15
core/logging/testing/build.gradle.kts
Normal file
15
core/logging/testing/build.gradle.kts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.kmp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.thunderbird.core.logging.testing"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
api(projects.core.logging.api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package net.thunderbird.core.logging.testing
|
||||
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
import net.thunderbird.core.logging.LogLevelManager
|
||||
|
||||
class TestLogLevelManager : LogLevelManager {
|
||||
var logLevel = LogLevel.VERBOSE
|
||||
override fun override(level: LogLevel) {
|
||||
logLevel = level
|
||||
}
|
||||
|
||||
override fun restoreDefault() {
|
||||
logLevel = LogLevel.VERBOSE
|
||||
}
|
||||
|
||||
override fun current(): LogLevel = logLevel
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package net.thunderbird.core.logging.testing
|
||||
|
||||
import net.thunderbird.core.logging.LogEvent
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
import net.thunderbird.core.logging.LogMessage
|
||||
import net.thunderbird.core.logging.LogTag
|
||||
import net.thunderbird.core.logging.Logger
|
||||
|
||||
/**
|
||||
* A test logger that captures all log events in a list.
|
||||
*/
|
||||
class TestLogger() : Logger {
|
||||
|
||||
val events: MutableList<LogEvent> = mutableListOf()
|
||||
|
||||
override fun verbose(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
events.add(
|
||||
LogEvent(
|
||||
level = LogLevel.VERBOSE,
|
||||
tag = tag,
|
||||
message = message(),
|
||||
throwable = throwable,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun debug(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
events.add(
|
||||
LogEvent(
|
||||
level = LogLevel.DEBUG,
|
||||
tag = tag,
|
||||
message = message(),
|
||||
throwable = throwable,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun info(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
events.add(
|
||||
LogEvent(
|
||||
level = LogLevel.INFO,
|
||||
tag = tag,
|
||||
message = message(),
|
||||
throwable = throwable,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun warn(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
events.add(
|
||||
LogEvent(
|
||||
level = LogLevel.WARN,
|
||||
tag = tag,
|
||||
message = message(),
|
||||
throwable = throwable,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(
|
||||
tag: LogTag?,
|
||||
throwable: Throwable?,
|
||||
message: () -> LogMessage,
|
||||
) {
|
||||
events.add(
|
||||
LogEvent(
|
||||
level = LogLevel.ERROR,
|
||||
tag = tag,
|
||||
message = message(),
|
||||
throwable = throwable,
|
||||
timestamp = TIMESTAMP,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TIMESTAMP = 1000L
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue