diff --git a/KmpLogger.podspec b/KmpLogger.podspec
index 6101ccf..c2f607f 100644
--- a/KmpLogger.podspec
+++ b/KmpLogger.podspec
@@ -1,11 +1,11 @@
Pod::Spec.new do |s|
s.name = 'KmpLogger'
- s.version = '1.1.0'
+ s.version = '1.2.0'
s.summary = 'Kotlin Multiplatform logging library for iOS'
s.homepage = 'https://github.com/Scarlet-Pan/logger'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.authors = { 'Scarlet Pan' => 'scarletpan@qq.com' }
- s.source = { :git => 'https://github.com/Scarlet-Pan/logger.git', :tag => '1.1.0' }
+ s.source = { :git => 'https://github.com/Scarlet-Pan/logger.git', :tag => '1.2.0' }
s.vendored_frameworks = 'KmpLogger.xcframework'
s.ios.deployment_target = '12.0'
s.swift_version = '5.0'
diff --git a/KmpLogger.xcframework/Info.plist b/KmpLogger.xcframework/Info.plist
index 973a456..08969a0 100644
--- a/KmpLogger.xcframework/Info.plist
+++ b/KmpLogger.xcframework/Info.plist
@@ -8,32 +8,32 @@
BinaryPath
KmpLogger.framework/KmpLogger
LibraryIdentifier
- ios-arm64_x86_64-simulator
+ ios-arm64
LibraryPath
KmpLogger.framework
SupportedArchitectures
arm64
- x86_64
SupportedPlatform
ios
- SupportedPlatformVariant
- simulator
BinaryPath
KmpLogger.framework/KmpLogger
LibraryIdentifier
- ios-arm64
+ ios-arm64_x86_64-simulator
LibraryPath
KmpLogger.framework
SupportedArchitectures
arm64
+ x86_64
SupportedPlatform
ios
+ SupportedPlatformVariant
+ simulator
CFBundlePackageType
diff --git a/KmpLogger.xcframework/ios-arm64/KmpLogger.framework/Headers/KmpLogger.h b/KmpLogger.xcframework/ios-arm64/KmpLogger.framework/Headers/KmpLogger.h
index c079b7c..086b207 100644
--- a/KmpLogger.xcframework/ios-arm64/KmpLogger.framework/Headers/KmpLogger.h
+++ b/KmpLogger.xcframework/ios-arm64/KmpLogger.framework/Headers/KmpLogger.h
@@ -6,9 +6,9 @@
#import
#import
-@class KmpLoggerKotlinThrowable, KmpLoggerLoggerLevel, KmpLoggerAbsLogger, SharedLogger, KmpLoggerKotlinEnumCompanion, KmpLoggerKotlinEnum, KmpLoggerKotlinArray;
+@class KmpLoggerKotlinThrowable, KmpLoggerLoggerLevel, KmpLoggerContentCompanion, KmpLoggerAbsLogger, SharedLogger, KmpLoggerKotlinEnumCompanion, KmpLoggerKotlinEnum, KmpLoggerKotlinArray, KmpLoggerFilterCompanion;
-@protocol KmpLoggerLogger, KmpLoggerKotlinComparable, KmpLoggerKotlinIterator;
+@protocol KmpLoggerLogger, KmpLoggerContent, KmpLoggerKotlinComparable, KmpLoggerFilter, KmpLoggerKotlinIterator;
NS_ASSUME_NONNULL_BEGIN
#pragma clang diagnostic push
@@ -169,6 +169,29 @@ __attribute__((swift_name("AbsLogger")))
- (void)wTag:(NSString *)tag msg:(NSString *)msg tr:(KmpLoggerKotlinThrowable * _Nullable)tr __attribute__((swift_name("w(tag:msg:tr:)")));
@end
+__attribute__((swift_name("Content")))
+@protocol KmpLoggerContent
+@required
+@property (readonly) NSString *message __attribute__((swift_name("message")));
+@property (readonly) KmpLoggerKotlinThrowable * _Nullable throwable __attribute__((swift_name("throwable")));
+@end
+
+__attribute__((objc_subclassing_restricted))
+__attribute__((swift_name("ContentCompanion")))
+@interface KmpLoggerContentCompanion : KmpLoggerBase
++ (instancetype)alloc __attribute__((unavailable));
++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
++ (instancetype)companion __attribute__((swift_name("init()")));
+@property (class, readonly, getter=shared) KmpLoggerContentCompanion *shared __attribute__((swift_name("shared")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmStatic
+*/
+- (id)ofMsg:(NSString *)msg tr:(KmpLoggerKotlinThrowable *)tr __attribute__((swift_name("of(msg:tr:)")));
+- (id)with:(NSString *)receiver exception:(KmpLoggerKotlinThrowable *)exception __attribute__((swift_name("with(_:exception:)")));
+@end
+
__attribute__((objc_subclassing_restricted))
@interface SharedLogger : KmpLoggerAbsLogger
+ (instancetype)alloc __attribute__((unavailable));
@@ -228,10 +251,62 @@ __attribute__((swift_name("LoggerLevel")))
@property (class, readonly) NSArray *entries __attribute__((swift_name("entries")));
@end
+__attribute__((swift_name("Filter")))
+@protocol KmpLoggerFilter
+@required
+@end
+
+__attribute__((swift_name("AnyFilter")))
+@protocol KmpLoggerAnyFilter
+@required
+- (BOOL)filter __attribute__((swift_name("filter()")));
+@end
+
+__attribute__((objc_subclassing_restricted))
+__attribute__((swift_name("FilterCompanion")))
+@interface KmpLoggerFilterCompanion : KmpLoggerBase
++ (instancetype)alloc __attribute__((unavailable));
++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
++ (instancetype)companion __attribute__((swift_name("init()")));
+@property (class, readonly, getter=shared) KmpLoggerFilterCompanion *shared __attribute__((swift_name("shared")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmStatic
+*/
+- (id)atLeastLevel:(KmpLoggerLoggerLevel *)level __attribute__((swift_name("atLeast(level:)")));
+@property (readonly) id ALL __attribute__((swift_name("ALL")));
+@property (readonly) id NONE __attribute__((swift_name("NONE")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmStatic
+*/
+@property (getter=default, setter=setDefault:) id default_ __attribute__((swift_name("default_")));
+@end
+
+__attribute__((swift_name("LevelFilter")))
+@protocol KmpLoggerLevelFilter
+@required
+- (BOOL)filterLevel:(KmpLoggerLoggerLevel *)level __attribute__((swift_name("filter(level:)")));
+@end
+
+__attribute__((swift_name("TagFilter")))
+@protocol KmpLoggerTagFilter
+@required
+- (BOOL)filterTag:(NSString *)tag __attribute__((swift_name("filter(tag:)")));
+@end
+
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("CompositeLoggerKt")))
@interface KmpLoggerCompositeLoggerKt : KmpLoggerBase
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="exclude")
+*/
++ (id)minus:(id)receiver other:(id)other __attribute__((swift_name("minus(_:other:)")));
+
/**
* @note annotations
* kotlin.jvm.JvmName(name="combine")
@@ -239,6 +314,80 @@ __attribute__((swift_name("CompositeLoggerKt")))
+ (id)plus:(id)receiver other:(id)other __attribute__((swift_name("plus(_:other:)")));
@end
+__attribute__((objc_subclassing_restricted))
+__attribute__((swift_name("FilterKt")))
+@interface KmpLoggerFilterKt : KmpLoggerBase
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="both")
+*/
++ (id)and:(id)receiver other:(id)other __attribute__((swift_name("and(_:other:)")));
++ (id)minus:(id)receiver other:(id)other __attribute__((swift_name("minus(_:other:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="any")
+*/
++ (id)or:(id)receiver other:(id)other __attribute__((swift_name("or(_:other:)")));
++ (id)plus:(id)receiver other:(id)other __attribute__((swift_name("plus(_:other:)")));
++ (id)withFilter:(id)receiver filter:(id)filter __attribute__((swift_name("withFilter(_:filter:)")));
++ (id)withoutFilter:(id)receiver __attribute__((swift_name("withoutFilter(_:)")));
+@end
+
+__attribute__((objc_subclassing_restricted))
+__attribute__((swift_name("LazyLoggerKt")))
+@interface KmpLoggerLazyLoggerKt : KmpLoggerBase
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="debugLazyContent")
+*/
++ (void)d:(id)receiver tag:(NSString *)tag lazy:(id (^)(void))lazy __attribute__((swift_name("d(_:tag:lazy:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="debugLazyMessage")
+*/
++ (void)d:(id)receiver tag:(NSString *)tag lazy_:(NSString *(^)(void))lazy __attribute__((swift_name("d(_:tag:lazy_:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="errorLazyContent")
+*/
++ (void)e:(id)receiver tag:(NSString *)tag lazy:(id (^)(void))lazy __attribute__((swift_name("e(_:tag:lazy:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="errorLazyMessage")
+*/
++ (void)e:(id)receiver tag:(NSString *)tag lazy_:(NSString *(^)(void))lazy __attribute__((swift_name("e(_:tag:lazy_:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="infoLazyContent")
+*/
++ (void)i:(id)receiver tag:(NSString *)tag lazy:(id (^)(void))lazy __attribute__((swift_name("i(_:tag:lazy:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="infoLazyMessage")
+*/
++ (void)i:(id)receiver tag:(NSString *)tag lazy_:(NSString *(^)(void))lazy __attribute__((swift_name("i(_:tag:lazy_:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="warnLazyContent")
+*/
++ (void)w:(id)receiver tag:(NSString *)tag lazy:(id (^)(void))lazy __attribute__((swift_name("w(_:tag:lazy:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="warnLazyMessage")
+*/
++ (void)w:(id)receiver tag:(NSString *)tag lazy_:(NSString *(^)(void))lazy __attribute__((swift_name("w(_:tag:lazy_:)")));
+@end
+
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("LoggersKt")))
@interface KmpLoggerLoggersKt : KmpLoggerBase
diff --git a/KmpLogger.xcframework/ios-arm64/KmpLogger.framework/KmpLogger b/KmpLogger.xcframework/ios-arm64/KmpLogger.framework/KmpLogger
index 85c3a79..c470868 100755
Binary files a/KmpLogger.xcframework/ios-arm64/KmpLogger.framework/KmpLogger and b/KmpLogger.xcframework/ios-arm64/KmpLogger.framework/KmpLogger differ
diff --git a/KmpLogger.xcframework/ios-arm64_x86_64-simulator/KmpLogger.framework/Headers/KmpLogger.h b/KmpLogger.xcframework/ios-arm64_x86_64-simulator/KmpLogger.framework/Headers/KmpLogger.h
index c079b7c..086b207 100644
--- a/KmpLogger.xcframework/ios-arm64_x86_64-simulator/KmpLogger.framework/Headers/KmpLogger.h
+++ b/KmpLogger.xcframework/ios-arm64_x86_64-simulator/KmpLogger.framework/Headers/KmpLogger.h
@@ -6,9 +6,9 @@
#import
#import
-@class KmpLoggerKotlinThrowable, KmpLoggerLoggerLevel, KmpLoggerAbsLogger, SharedLogger, KmpLoggerKotlinEnumCompanion, KmpLoggerKotlinEnum, KmpLoggerKotlinArray;
+@class KmpLoggerKotlinThrowable, KmpLoggerLoggerLevel, KmpLoggerContentCompanion, KmpLoggerAbsLogger, SharedLogger, KmpLoggerKotlinEnumCompanion, KmpLoggerKotlinEnum, KmpLoggerKotlinArray, KmpLoggerFilterCompanion;
-@protocol KmpLoggerLogger, KmpLoggerKotlinComparable, KmpLoggerKotlinIterator;
+@protocol KmpLoggerLogger, KmpLoggerContent, KmpLoggerKotlinComparable, KmpLoggerFilter, KmpLoggerKotlinIterator;
NS_ASSUME_NONNULL_BEGIN
#pragma clang diagnostic push
@@ -169,6 +169,29 @@ __attribute__((swift_name("AbsLogger")))
- (void)wTag:(NSString *)tag msg:(NSString *)msg tr:(KmpLoggerKotlinThrowable * _Nullable)tr __attribute__((swift_name("w(tag:msg:tr:)")));
@end
+__attribute__((swift_name("Content")))
+@protocol KmpLoggerContent
+@required
+@property (readonly) NSString *message __attribute__((swift_name("message")));
+@property (readonly) KmpLoggerKotlinThrowable * _Nullable throwable __attribute__((swift_name("throwable")));
+@end
+
+__attribute__((objc_subclassing_restricted))
+__attribute__((swift_name("ContentCompanion")))
+@interface KmpLoggerContentCompanion : KmpLoggerBase
++ (instancetype)alloc __attribute__((unavailable));
++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
++ (instancetype)companion __attribute__((swift_name("init()")));
+@property (class, readonly, getter=shared) KmpLoggerContentCompanion *shared __attribute__((swift_name("shared")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmStatic
+*/
+- (id)ofMsg:(NSString *)msg tr:(KmpLoggerKotlinThrowable *)tr __attribute__((swift_name("of(msg:tr:)")));
+- (id)with:(NSString *)receiver exception:(KmpLoggerKotlinThrowable *)exception __attribute__((swift_name("with(_:exception:)")));
+@end
+
__attribute__((objc_subclassing_restricted))
@interface SharedLogger : KmpLoggerAbsLogger
+ (instancetype)alloc __attribute__((unavailable));
@@ -228,10 +251,62 @@ __attribute__((swift_name("LoggerLevel")))
@property (class, readonly) NSArray *entries __attribute__((swift_name("entries")));
@end
+__attribute__((swift_name("Filter")))
+@protocol KmpLoggerFilter
+@required
+@end
+
+__attribute__((swift_name("AnyFilter")))
+@protocol KmpLoggerAnyFilter
+@required
+- (BOOL)filter __attribute__((swift_name("filter()")));
+@end
+
+__attribute__((objc_subclassing_restricted))
+__attribute__((swift_name("FilterCompanion")))
+@interface KmpLoggerFilterCompanion : KmpLoggerBase
++ (instancetype)alloc __attribute__((unavailable));
++ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
++ (instancetype)companion __attribute__((swift_name("init()")));
+@property (class, readonly, getter=shared) KmpLoggerFilterCompanion *shared __attribute__((swift_name("shared")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmStatic
+*/
+- (id)atLeastLevel:(KmpLoggerLoggerLevel *)level __attribute__((swift_name("atLeast(level:)")));
+@property (readonly) id ALL __attribute__((swift_name("ALL")));
+@property (readonly) id NONE __attribute__((swift_name("NONE")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmStatic
+*/
+@property (getter=default, setter=setDefault:) id default_ __attribute__((swift_name("default_")));
+@end
+
+__attribute__((swift_name("LevelFilter")))
+@protocol KmpLoggerLevelFilter
+@required
+- (BOOL)filterLevel:(KmpLoggerLoggerLevel *)level __attribute__((swift_name("filter(level:)")));
+@end
+
+__attribute__((swift_name("TagFilter")))
+@protocol KmpLoggerTagFilter
+@required
+- (BOOL)filterTag:(NSString *)tag __attribute__((swift_name("filter(tag:)")));
+@end
+
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("CompositeLoggerKt")))
@interface KmpLoggerCompositeLoggerKt : KmpLoggerBase
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="exclude")
+*/
++ (id)minus:(id)receiver other:(id)other __attribute__((swift_name("minus(_:other:)")));
+
/**
* @note annotations
* kotlin.jvm.JvmName(name="combine")
@@ -239,6 +314,80 @@ __attribute__((swift_name("CompositeLoggerKt")))
+ (id)plus:(id)receiver other:(id)other __attribute__((swift_name("plus(_:other:)")));
@end
+__attribute__((objc_subclassing_restricted))
+__attribute__((swift_name("FilterKt")))
+@interface KmpLoggerFilterKt : KmpLoggerBase
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="both")
+*/
++ (id)and:(id)receiver other:(id)other __attribute__((swift_name("and(_:other:)")));
++ (id)minus:(id)receiver other:(id)other __attribute__((swift_name("minus(_:other:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="any")
+*/
++ (id)or:(id)receiver other:(id)other __attribute__((swift_name("or(_:other:)")));
++ (id)plus:(id)receiver other:(id)other __attribute__((swift_name("plus(_:other:)")));
++ (id)withFilter:(id)receiver filter:(id)filter __attribute__((swift_name("withFilter(_:filter:)")));
++ (id)withoutFilter:(id)receiver __attribute__((swift_name("withoutFilter(_:)")));
+@end
+
+__attribute__((objc_subclassing_restricted))
+__attribute__((swift_name("LazyLoggerKt")))
+@interface KmpLoggerLazyLoggerKt : KmpLoggerBase
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="debugLazyContent")
+*/
++ (void)d:(id)receiver tag:(NSString *)tag lazy:(id (^)(void))lazy __attribute__((swift_name("d(_:tag:lazy:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="debugLazyMessage")
+*/
++ (void)d:(id)receiver tag:(NSString *)tag lazy_:(NSString *(^)(void))lazy __attribute__((swift_name("d(_:tag:lazy_:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="errorLazyContent")
+*/
++ (void)e:(id)receiver tag:(NSString *)tag lazy:(id (^)(void))lazy __attribute__((swift_name("e(_:tag:lazy:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="errorLazyMessage")
+*/
++ (void)e:(id)receiver tag:(NSString *)tag lazy_:(NSString *(^)(void))lazy __attribute__((swift_name("e(_:tag:lazy_:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="infoLazyContent")
+*/
++ (void)i:(id)receiver tag:(NSString *)tag lazy:(id (^)(void))lazy __attribute__((swift_name("i(_:tag:lazy:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="infoLazyMessage")
+*/
++ (void)i:(id)receiver tag:(NSString *)tag lazy_:(NSString *(^)(void))lazy __attribute__((swift_name("i(_:tag:lazy_:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="warnLazyContent")
+*/
++ (void)w:(id)receiver tag:(NSString *)tag lazy:(id (^)(void))lazy __attribute__((swift_name("w(_:tag:lazy:)")));
+
+/**
+ * @note annotations
+ * kotlin.jvm.JvmName(name="warnLazyMessage")
+*/
++ (void)w:(id)receiver tag:(NSString *)tag lazy_:(NSString *(^)(void))lazy __attribute__((swift_name("w(_:tag:lazy_:)")));
+@end
+
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("LoggersKt")))
@interface KmpLoggerLoggersKt : KmpLoggerBase
diff --git a/KmpLogger.xcframework/ios-arm64_x86_64-simulator/KmpLogger.framework/KmpLogger b/KmpLogger.xcframework/ios-arm64_x86_64-simulator/KmpLogger.framework/KmpLogger
index 5a1d5a4..052fcd3 100755
Binary files a/KmpLogger.xcframework/ios-arm64_x86_64-simulator/KmpLogger.framework/KmpLogger and b/KmpLogger.xcframework/ios-arm64_x86_64-simulator/KmpLogger.framework/KmpLogger differ
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c5ce62c..444fc05 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -20,12 +20,15 @@ androidEspresso = "3.6.1"
[libraries]
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin-runitme" }
+kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "1.6.4" }
+
logger = { group = "io.github.scarlet-pan", name = "logger", version.ref = "logger" }
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kotlin" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidTestCore" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidTestRunner" }
diff --git a/logger/build.gradle.kts b/logger/build.gradle.kts
index 6078259..bb86603 100644
--- a/logger/build.gradle.kts
+++ b/logger/build.gradle.kts
@@ -8,7 +8,7 @@ plugins {
}
group = "io.github.scarlet-pan"
-version = "1.1.0"
+version = "1.2.0"
val xcfName = "KmpLogger"
@@ -72,6 +72,7 @@ kotlin {
commonMain {
dependencies {
implementation(libs.kotlin.stdlib)
+ implementation(libs.kotlinx.coroutines.core)
}
}
@@ -89,6 +90,7 @@ kotlin {
getByName("androidUnitTest") {
dependencies {
+ implementation(libs.junit)
implementation(libs.robolectric)
}
}
diff --git a/logger/src/androidMain/kotlin/dev/scarlet/logger/filter/Filter.android.kt b/logger/src/androidMain/kotlin/dev/scarlet/logger/filter/Filter.android.kt
new file mode 100644
index 0000000..f685046
--- /dev/null
+++ b/logger/src/androidMain/kotlin/dev/scarlet/logger/filter/Filter.android.kt
@@ -0,0 +1,5 @@
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.Logger.Level.DEBUG
+
+internal actual val platformFilter: Filter = Filter.atLeast(DEBUG)
\ No newline at end of file
diff --git a/logger/src/commonMain/kotlin/dev/scarlet/logger/CompositeLogger.kt b/logger/src/commonMain/kotlin/dev/scarlet/logger/CompositeLogger.kt
index 53954a0..1cc7b80 100644
--- a/logger/src/commonMain/kotlin/dev/scarlet/logger/CompositeLogger.kt
+++ b/logger/src/commonMain/kotlin/dev/scarlet/logger/CompositeLogger.kt
@@ -1,5 +1,6 @@
@file:JvmName("Loggers")
@file:JvmMultifileClass
+
package dev.scarlet.logger
import kotlin.jvm.JvmMultifileClass
@@ -11,11 +12,11 @@ import kotlin.jvm.JvmName
* All log calls are delegated to both [head] and [tail].
*
* @author Scarlet Pan
- * @version 1.0.1
+ * @version 1.1.0
*/
-internal class CompositeLogger(
- private val head: Logger,
- private val tail: Logger,
+internal data class CompositeLogger(
+ val head: Logger,
+ val tail: Logger,
) : AbsLogger() {
private val properties: List by lazy {
@@ -53,3 +54,111 @@ internal class CompositeLogger(
*/
@JvmName("combine")
operator fun Logger.plus(other: Logger): Logger = CompositeLogger(this, other)
+
+/**
+ * Returns a new [Logger] that excludes the **first occurrence** of a logger equal to the specified [other],
+ * as determined by the `==` operator.
+ *
+ * This is typically used with **singleton logger objects** (e.g., `object NetworkLogger : AbsLogger()`),
+ * where `other` is the singleton itself.
+ *
+ * - If `this` is a single logger and `this == other`, returns [EmptyLogger].
+ * - If `this` is a composite logger, only the **first matching logger** (`logger == other`) is removed.
+ * - If no match is found, returns the original logger unchanged.
+ * - If removal results in zero loggers, returns [EmptyLogger].
+ * - If exactly one logger remains, it is returned directly (unwrapped).
+ *
+ * This operation is **non-destructive**.
+ *
+ * Example setup:
+ * ```kotlin
+ * Logger.default = Logger.SYSTEM + FileLogger + NetworkLogger
+ * ```
+ *
+ * Later usage:
+ * ```kotlin
+ * val logger = Logger.default - NetworkLogger // removes the NetworkLogger singleton
+ * ```
+ *
+ * > Note: Since most logger singletons do not override `equals()`, `==` behaves as reference equality.
+ *
+ * @param other The logger instance to remove (e.g., a singleton object). Only the first `==` match is removed.
+ * @return A new [Logger] with the first matching occurrence excluded.
+ */
+@JvmName("exclude")
+operator fun Logger.minus(other: Logger): Logger = when (this) {
+ other -> EmptyLogger
+ is CompositeLogger -> this - other
+ else -> this
+}
+
+private operator fun CompositeLogger.minus(other: Logger): Logger {
+ if (other is CompositeLogger) {
+ return this - other.head - other.tail
+ }
+ val head = head - other
+ if (head == EmptyLogger) {
+ return tail
+ }
+ val tail = tail - other
+ return when {
+ tail == EmptyLogger -> head
+ head == this.head && tail == this.tail -> this
+ else -> CompositeLogger(head, tail)
+ }
+}
+
+/**
+ * Returns a new [Logger] that excludes **all loggers** associated with the given [key].
+ *
+ * This is designed for **logger types that may have multiple instances**, where the [key] is typically
+ * their `companion object` implementing [Key]. All instances whose logical identity matches [key] are removed.
+ *
+ * Behavior:
+ * - **All matching instances** are removed (not just the first).
+ * - Matching is based on logical identity: a logger matches if its class's companion `== key`.
+ * - If no logger matches, the original logger is returned unchanged.
+ * - If all loggers are removed, returns [EmptyLogger].
+ * - If one logger remains, it is returned directly.
+ *
+ * This operation is **non-destructive**.
+ *
+ * Example setup:
+ * ```kotlin
+ * Logger.default = Logger.SYSTEM + FileLogger("debug") + FileLogger("release")
+ * ```
+ *
+ * Later usage:
+ * ```kotlin
+ * val logger = Logger.default - FileLogger // removes all FileLogger instances
+ * ```
+ *
+ * @param key A [Key] representing a logger type (typically a companion object like `FileLogger`).
+ * @return A new [Logger] with all instances matching [key] excluded.
+ */
+internal operator fun Logger.minus(key: Key): Logger = when {
+ key.`class`.isInstance(this) -> EmptyLogger
+ this is CompositeLogger -> this - key
+ else -> this
+}
+
+private operator fun CompositeLogger.minus(key: Key): Logger {
+ val head = head - key
+ val tail = tail - key
+ return when {
+ head == this.head && tail == this.tail -> this
+ head == EmptyLogger && tail == EmptyLogger -> EmptyLogger
+ head == EmptyLogger -> tail
+ tail == EmptyLogger -> head
+ else -> CompositeLogger(head, tail)
+ }
+}
+
+/**
+ * A no-op logger that discards all log messages.
+ * Used as the result when a composite logger becomes empty.
+ */
+internal object EmptyLogger : AbsLogger() {
+ override fun toString(): String = "EmptyLogger"
+ override fun log(level: Logger.Level, tag: String, msg: String, tr: Throwable?) = Unit
+}
\ No newline at end of file
diff --git a/logger/src/commonMain/kotlin/dev/scarlet/logger/Content.kt b/logger/src/commonMain/kotlin/dev/scarlet/logger/Content.kt
new file mode 100644
index 0000000..6963bcb
--- /dev/null
+++ b/logger/src/commonMain/kotlin/dev/scarlet/logger/Content.kt
@@ -0,0 +1,113 @@
+package dev.scarlet.logger
+
+import kotlin.jvm.JvmInline
+import kotlin.jvm.JvmStatic
+
+/**
+ * Represents a structured log content that may include a message and an optional associated [Throwable].
+ *
+ * This sealed interface enables type-safe handling of different kinds of log payloads.
+ * It supports two variants:
+ * - A simple [Message] containing only a string.
+ * - An [Error] containing a message and a non-null [Throwable] (e.g., for exceptions).
+ *
+ * ### Usage Examples
+ * ```kotlin
+ * // Simple message
+ * val content1 = Content.of("User logged in")
+ *
+ * // Message with exception
+ * val content2 = Content.of("Failed to load data", IOException("Network down"))
+ *
+ * // Infix-style creation
+ * val content3 = "Database connection failed" with SQLException("Timeout")
+ * ```
+ *
+ * The companion object provides factory methods to construct instances safely and idiomatically.
+ *
+ * @see Message
+ * @see Error
+ *
+ * @author Scarlet Pan
+ * @version 1.0.0
+ */
+sealed interface Content {
+
+ companion object {
+
+ /**
+ * Creates a [Content] instance from a plain log message.
+ *
+ * This internal method is used by the logging framework to wrap string-only logs.
+ * External users should prefer public overloads or extension functions.
+ *
+ * @param msg the log message string
+ * @return a [Message] instance wrapping the given string
+ */
+ internal fun of(msg: String): Content = Message(msg)
+
+ /**
+ * Creates a [Content] instance that includes both a message and a [Throwable].
+ *
+ * Useful for logging exceptions while preserving stack trace information.
+ *
+ * @param msg the descriptive log message
+ * @param tr the associated exception or error
+ * @return an [Error] instance containing both message and throwable
+ */
+ @JvmStatic
+ fun of(msg: String, tr: Throwable): Content = Error(msg, tr)
+
+ /**
+ * Extension function that associates a [Throwable] with a log message string.
+ *
+ * Provides a fluent, readable way to create error logs:
+ * ```kotlin
+ * "Failed to parse config" with e
+ * ```
+ *
+ * @receiver the log message
+ * @param exception the [Throwable] to attach
+ * @return a [Error] instance
+ */
+ infix fun String.with(exception: Throwable) = of(this, exception)
+
+ }
+
+ /**
+ * The primary log message text.
+ */
+ val message: String
+
+ /**
+ * An optional [Throwable] associated with this log entry (e.g., an exception).
+ *
+ * Returns `null` if no throwable is present (as in [Message]).
+ * Non-null for [Error] instances.
+ */
+ val throwable: Throwable? get() = null
+
+}
+
+/**
+ * A lightweight wrapper for a plain log message without any associated exception.
+ *
+ * Implemented as a `@JvmInline value class` to avoid runtime allocation overhead on JVM,
+ * while still being compatible with Kotlin/Native and JS.
+ *
+ * @property message the log message string
+ */
+@JvmInline
+private value class Message(override val message: String) : Content {
+ override fun toString(): String = message
+}
+
+/**
+ * Represents a log entry that includes both a message and a non-null [Throwable].
+ *
+ * Typically used when logging caught exceptions or system errors.
+ *
+ * @property message the descriptive log message
+ * @property throwable the associated exception or error (never null)
+ */
+private data class Error(override val message: String, override val throwable: Throwable) : Content
diff --git a/logger/src/commonMain/kotlin/dev/scarlet/logger/Key.kt b/logger/src/commonMain/kotlin/dev/scarlet/logger/Key.kt
new file mode 100644
index 0000000..cb92d33
--- /dev/null
+++ b/logger/src/commonMain/kotlin/dev/scarlet/logger/Key.kt
@@ -0,0 +1,47 @@
+package dev.scarlet.logger
+
+import kotlin.jvm.JvmInline
+import kotlin.jvm.JvmStatic
+import kotlin.reflect.KClass
+
+/**
+ * A logical identity for a family of [Logger] instances.
+ *
+ * This interface is typically implemented by a logger's `companion object` to enable
+ * type-based operations (e.g., removing all instances of that logger type).
+ *
+ * Example:
+ * ```kotlin
+ * class FilterLogger : Logger {
+ * companion object : Key by Key.key()
+ * }
+ *
+ * // Removes all FilterLogger instances from the composite logger
+ * val logger = Logger.default - FilterLogger
+ * ```
+ *
+ * @author Scarlet Pan
+ * @version 1.0.0
+ */
+internal interface Key {
+
+ /**
+ * The Kotlin class associated with the logger type identified by this key.
+ */
+ val `class`: KClass
+
+ companion object {
+
+ /**
+ * Creates a [Key] for the reified logger type [T].
+ * Intended for use in `companion object` delegates.
+ */
+ @JvmStatic
+ /* protected */ inline fun key(): Key = Impl(T::class)
+
+ @JvmInline
+ protected value class Impl(override val `class`: KClass) : Key
+
+ }
+
+}
\ No newline at end of file
diff --git a/logger/src/commonMain/kotlin/dev/scarlet/logger/filter/Filter.kt b/logger/src/commonMain/kotlin/dev/scarlet/logger/filter/Filter.kt
new file mode 100644
index 0000000..a5983f3
--- /dev/null
+++ b/logger/src/commonMain/kotlin/dev/scarlet/logger/filter/Filter.kt
@@ -0,0 +1,328 @@
+@file:JvmName("Filters")
+@file:JvmMultifileClass
+
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.Logger
+import dev.scarlet.logger.Logger.Level
+import dev.scarlet.logger.filter.Filter.Companion.ALL
+import dev.scarlet.logger.filter.Filter.Companion.NONE
+import dev.scarlet.logger.filter.Filter.Companion.default
+import dev.scarlet.logger.minus
+import kotlin.jvm.JvmInline
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmStatic
+
+/**
+ * A log filter that determines whether a log message should be recorded.
+ *
+ * [Filter] is a sealed interface with concrete behavior defined by its functional subtypes:
+ * - [AnyFilter]: context-free boolean decision (e.g., constant filters);
+ * - [LevelFilter]: filters based on log level;
+ * - [TagFilter]: filters based on log tag.
+ *
+ * Predefined common instances are provided:
+ * - [ALL]: passes all log messages;
+ * - [NONE]: discards all log messages;
+ * - [default]: the global default filter, initially set to [ALL].
+ *
+ * Logger functions such as [Logger.d] and [Logger.i] consult the [default] filter
+ * to decide whether to process a log message. Messages rejected by the filter are
+ * silently ignored with no runtime overhead.
+ *
+ * Filters can be combined using operators:
+ * - `+` (**OR**): passes if **either** filter allows the message;
+ * - `-` (**AND**): passes only if **both** filters allow the message.
+ *
+ * Example setup:
+ * ```kotlin
+ * // Allow WARN+ logs from "Network" OR any ERROR+ logs
+ * val networkWarn = LevelFilter { it >= Level.WARN } - TagFilter { it == "Network" }
+ * val errors = LevelFilter { it >= Level.ERROR }
+ * Filter.default = networkWarn + errors
+ * ```
+ *
+ * Later usage:
+ * ```kotlin
+ * Logger.w("Network") { "Logged (WARN + Network)" }
+ * Logger.w("Database") { "Not logged (WARN but not Network, and not ERROR)" }
+ * Logger.e("Auth") { "Logged (ERROR+, regardless of tag)" }
+ * ```
+ *
+ * @author Scarlet Pan
+ * @version 1.0.0
+ */
+sealed interface Filter {
+
+ companion object {
+
+ private const val TAG = "Filter"
+
+ /**
+ * A filter that allows all log messages to pass through.
+ */
+ val ALL: Filter by lazy { SwitchFilter(true) }
+
+ /**
+ * A filter that discards all log messages.
+ */
+ val NONE: Filter by lazy { SwitchFilter(false) }
+
+ @Suppress("ObjectPropertyName")
+ /* VisibleForTesting */internal var _default: Filter? = null
+
+ /**
+ * The global default filter.
+ *
+ * Logger functions such as [Logger.d] and [Logger.i] use this filter to determine
+ * whether a log message should be recorded. Messages that do not pass the filter
+ * are silently skipped with no runtime overhead.
+ *
+ * The initial value is chosen based on platform and typical usage:
+ * - **Android**: allows all levels (including [Level.DEBUG]);
+ * - **iOS/macOS**: allows [Level.INFO] and above;
+ * - **JVM (server)**: allows [Level.INFO] and above;
+ * - **JavaScript**: allows [Level.WARN] and above.
+ *
+ * > ⚠️ These defaults are provided for convenience and may be adjusted in future releases
+ * > to better align with platform best practices or security considerations.
+ * > For stable behavior, explicitly set [default] during application startup.
+ *
+ * You can assign a custom filter at any time to adjust logging behavior dynamically.
+ */
+ @JvmStatic
+ var default: Filter
+ get() = _default ?: platformFilter.also { _default = it }
+ set(value) {
+ _default = value.also {
+ val logger = Logger.default - FilterLogger
+ logger.i(TAG, "Default filter change to $it.")
+ }
+ }
+
+ /**
+ * Creates a filter that accepts logs at or above the specified [level].
+ *
+ * For example, [atLeast(Level.INFO)] allows INFO, WARN, and ERROR messages,
+ * but excludes DEBUG and VERBOSE.
+ *
+ * @param level The minimum log level to allow.
+ */
+ @JvmStatic
+ fun atLeast(level: Level): Filter = when (level) {
+ Level.DEBUG -> ALL
+ else -> LevelFilterImpl(level)
+ }
+
+ }
+
+}
+
+/**
+ * Platform-specific default filter.
+ *
+ * Initialized according to platform conventions (e.g., more verbose on Android Debug,
+ * stricter on JS or iOS Release).
+ */
+internal expect val platformFilter: Filter
+
+/**
+ * A context-free log filter.
+ *
+ * This functional interface is used when no log metadata (such as level or tag) is needed—
+ * typically for constant or state-based decisions.
+ *
+ * Commonly used to implement [Filter.ALL] and [Filter.NONE].
+ *
+ * Example:
+ * ```kotlin
+ * val alwaysOn = AnyFilter { true }
+ * val featureEnabled = AnyFilter { FeatureFlags.loggingEnabled }
+ * ```
+ */
+fun interface AnyFilter : Filter {
+
+ /**
+ * Determines whether the current log message should be recorded.
+ *
+ * Since no contextual information is available, this method usually returns a constant
+ * or depends on external state.
+ *
+ * @return `true` to allow the log, `false` to discard it.
+ */
+ fun filter(): Boolean
+
+}
+
+@JvmInline
+private value class SwitchFilter(val enabled: Boolean) : AnyFilter {
+ override fun filter(): Boolean = enabled
+ override fun toString(): String = when {
+ enabled -> "Filter.ALL"
+ else -> "Filter.NONE"
+ }
+}
+
+/**
+ * A log filter based on log level.
+ *
+ * Use this interface when filtering decisions depend on the [Level] of the log message.
+ * Typically created via factory methods like [Filter.atLeast].
+ *
+ * Example:
+ * ```kotlin
+ * val warnOnly = LevelFilter { it == Level.WARN }
+ * val infoOrAbove = LevelFilter { it >= Level.INFO }
+ * ```
+ */
+fun interface LevelFilter : Filter {
+
+ /**
+ * Determines whether a log message with the given [level] should be recorded.
+ *
+ * @param level The log level of the current message.
+ * @return `true` to allow the log, `false` to discard it.
+ */
+ fun filter(level: Level): Boolean
+
+}
+
+@JvmInline
+private value class LevelFilterImpl(val level: Level) : LevelFilter {
+ override fun filter(level: Level): Boolean = level >= this.level
+ override fun toString(): String = "Filter.atLeast($level)"
+}
+
+/**
+ * A log filter based on log tag.
+ *
+ * Useful for filtering logs by module, component, or custom categories using string tags.
+ *
+ * Example:
+ * ```kotlin
+ * val networkOnly = TagFilter { it == "Network" }
+ * val excludeDebugTags = TagFilter { !it.startsWith("DEBUG_") }
+ * ```
+ */
+fun interface TagFilter : Filter {
+
+ /**
+ * Determines whether a log message with the given [tag] should be recorded.
+ *
+ * @param tag The tag associated with the current log message.
+ * @return `true` to allow the log, `false` to discard it.
+ */
+ fun filter(tag: String): Boolean
+
+}
+
+private fun interface CompositeFilter : Filter {
+
+ fun filter(level: Level, tag: String): Boolean
+
+}
+
+/**
+ * Combines this filter with [other] using logical **OR**:
+ * a log message is allowed if **either** this filter **or** [other] allows it.
+ *
+ * This operator is equivalent to calling [or].
+ *
+ * Example:
+ * ```kotlin
+ * val isInfo = LevelFilter { it == Level.INFO }
+ * val isWarn = LevelFilter { it == Level.WARN }
+ * val infoOrWarn = isInfo + isWarn // Passes INFO or WARN logs
+ * ```
+ * @see or
+ */
+operator fun Filter.plus(other: Filter): Filter = or(other)
+
+/**
+ * Combines this filter with [other] using logical **OR**.
+ *
+ * A log message is recorded if it passes **this filter** or the [other] filter.
+ *
+ * Example:
+ * ```kotlin
+ * val errors = LevelFilter { it >= Level.ERROR }
+ * val networkDebug = LevelFilter { it == Level.DEBUG } - TagFilter { it == "Network" }
+ * val combined = errors.or(networkDebug) // ERROR+ anywhere, or DEBUG from "Network"
+ * ```
+ * @see plus
+ */
+@JvmName("any")
+fun Filter.or(other: Filter): Filter =
+ CompositeFilter { level, tag -> this.filter(level, tag) || other.filter(level, tag) }
+
+/**
+ * Combines this filter with [other] using logical **AND**:
+ * a log message is allowed only if **both** this filter **and** [other] allow it.
+ *
+ * This operator is equivalent to calling [and].
+ *
+ * Example:
+ * ```kotlin
+ * val atLeastWarn = Filter.atLeast(Level.WARN)
+ * val fromNetwork = TagFilter { it == "Network" }
+ * val networkWarn = atLeastWarn - fromNetwork // WARN+ logs AND tag=="Network"
+ * ```
+ * @see and
+ */
+operator fun Filter.minus(other: Filter): Filter = and(other)
+
+/**
+ * Combines this filter with [other] using logical **AND**.
+ *
+ * A log message is recorded only if it passes **both** this filter and the [other] filter.
+ *
+ * Example:
+ * ```kotlin
+ * val errorsOnly = LevelFilter.atLeast(Level.ERROR)
+ * val authModule = TagFilter { it == "Auth" }
+ * val authErrors = errorsOnly.and(authModule) // Only ERROR logs from "Auth"
+ * ```
+ * @see minus
+ */
+@JvmName("both")
+fun Filter.and(other: Filter): Filter =
+ CompositeFilter { level, tag -> this.filter(level, tag) && other.filter(level, tag) }
+
+internal fun Filter.filter(level: Level, tag: String) = when (this) {
+ is AnyFilter -> filter()
+ is LevelFilter -> filter(level)
+ is TagFilter -> filter(tag)
+ is CompositeFilter -> filter(level, tag)
+}
+
+/**
+ * Returns a logger that applies the given [filter] to all log messages.
+ *
+ * Messages rejected by the filter will not be logged.
+ *
+ * Example:
+ * ```kotlin
+ * val logger = ConsoleLogger().withFilter(Filter.atLeast(Level.WARN))
+ * logger.d { "Not logged" }
+ * logger.w { "Logged" }
+ * ```
+ */
+fun Logger.withFilter(filter: Filter): Logger = FilterLogger.of(this, filter)
+
+/**
+ * Returns a logger with all filtering layers removed.
+ *
+ * This restores the original logging behavior without any filters applied.
+ *
+ * Example:
+ * ```kotlin
+ * val base = ConsoleLogger()
+ * val filtered = base.withFilter(Filter.atLeast(Level.INFO))
+ * val restored = filtered.withoutFilter() // behaves like [base]
+ * ```
+ */
+fun Logger.withoutFilter(): Logger = when (this) {
+ is FilterLogger -> logger
+ else -> this
+}
\ No newline at end of file
diff --git a/logger/src/commonMain/kotlin/dev/scarlet/logger/filter/FilterLogger.kt b/logger/src/commonMain/kotlin/dev/scarlet/logger/filter/FilterLogger.kt
new file mode 100644
index 0000000..072aeb8
--- /dev/null
+++ b/logger/src/commonMain/kotlin/dev/scarlet/logger/filter/FilterLogger.kt
@@ -0,0 +1,84 @@
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.Content
+import dev.scarlet.logger.Key
+import dev.scarlet.logger.Key.Companion.key
+import dev.scarlet.logger.Logger
+import dev.scarlet.logger.Logger.Level.DEBUG
+import dev.scarlet.logger.Logger.Level.ERROR
+import dev.scarlet.logger.Logger.Level.INFO
+import dev.scarlet.logger.Logger.Level.WARN
+import dev.scarlet.logger.log
+import kotlin.jvm.JvmInline
+
+/**
+ * A filtering [Logger] that wraps another logger and applies a filter to log messages.
+ * This interface allows for the creation of filtered loggers, where log messages can be conditionally processed or ignored based on a provided filter.
+ *
+ * @author Scarlet Pan
+ * @version 1.0.0
+ */
+internal sealed interface FilterLogger : Logger {
+
+ companion object : Key by key() {
+
+ fun of(logger: Logger, filter: Filter? = null) =
+ when (filter) {
+ null -> Default(logger)
+ else -> Impl(filter, logger)
+ }
+
+ }
+
+ /**
+ * The filter applied to log messages.
+ */
+ val filter: Filter
+
+ /**
+ * The [Logger] that this filter is applied to.
+ */
+ val logger: Logger
+
+ override fun d(tag: String, msg: String, tr: Throwable?) = log(DEBUG, tag, msg, tr)
+
+ fun d(tag: String, lazy: () -> Content) = log(DEBUG, tag, lazy)
+
+ override fun i(tag: String, msg: String, tr: Throwable?) = log(INFO, tag, msg, tr)
+
+ fun i(tag: String, lazy: () -> Content) = log(INFO, tag, lazy)
+
+ override fun w(tag: String, msg: String, tr: Throwable?) = log(WARN, tag, msg, tr)
+
+ fun w(tag: String, lazy: () -> Content) = log(WARN, tag, lazy)
+
+ override fun e(tag: String, msg: String, tr: Throwable?) = log(ERROR, tag, msg, tr)
+
+ fun e(tag: String, lazy: () -> Content) = log(ERROR, tag, lazy)
+
+ fun log(level: Logger.Level, tag: String, msg: String, tr: Throwable?) {
+ if (filter.filter(level, tag)) logger.log(level, tag, msg, tr)
+ }
+
+ fun log(level: Logger.Level, tag: String, lazy: () -> Content) {
+ if (filter.filter(level, tag)) lazy().let {
+ logger.log(level, tag, it.message, it.throwable)
+ }
+ }
+
+ @JvmInline
+ private value class Default(override val logger: Logger) : FilterLogger {
+
+ override val filter: Filter get() = Filter.default
+
+ override fun toString(): String = "FilterLogger(filter=$filter, logger=$logger)"
+
+ }
+
+ private class Impl(override val filter: Filter, override val logger: Logger) : FilterLogger {
+
+ override fun toString(): String = "FilterLogger(filter=$filter, logger=$logger)"
+
+ }
+
+}
\ No newline at end of file
diff --git a/logger/src/commonMain/kotlin/dev/scarlet/logger/filter/LazyLogger.kt b/logger/src/commonMain/kotlin/dev/scarlet/logger/filter/LazyLogger.kt
new file mode 100644
index 0000000..1d03ae5
--- /dev/null
+++ b/logger/src/commonMain/kotlin/dev/scarlet/logger/filter/LazyLogger.kt
@@ -0,0 +1,197 @@
+@file:JvmName("Filters")
+@file:JvmMultifileClass
+@file:OptIn(ExperimentalTypeInference::class)
+
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.Content
+import dev.scarlet.logger.Content.Companion.with
+import dev.scarlet.logger.Logger
+import kotlin.experimental.ExperimentalTypeInference
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
+/**
+ * Logs a debug message lazily using a lambda that returns a [String].
+ *
+ * The [lazy] lambda is evaluated **only if the effective [Filter] permits this log entry**.
+ *
+ * The effective filter is determined as follows:
+ * - If the current [Logger] holds a dedicated [Filter] (e.g., created via [Logger.withFilter]), that filter is used;
+ * - Otherwise, [Filter.default] is used.
+ *
+ * This avoids unnecessary computation (e.g., string formatting or object serialization) when the log would be discarded.
+ *
+ * Example:
+ * ```kotlin
+ * Logger.d("Network") { "Request body: $largePayload" }
+ * ```
+ *
+ * @param tag A label identifying the source of the log message.
+ * @param lazy A lambda producing the log message. Not invoked if the effective filter rejects the log.
+ */
+@JvmName("debugLazyMessage")
+fun Logger.d(tag: String, lazy: () -> String) = d(tag, lazy.asContent())
+
+/**
+ * Logs a debug message lazily using a lambda that returns a [dev.scarlet.logger.Content].
+ *
+ * The [lazy] lambda is evaluated **only if the effective [Filter] permits this log entry**.
+ *
+ * The effective filter is determined as follows:
+ * - If the current [Logger] holds a dedicated [Filter] (e.g., created via [Logger.withFilter]), that filter is used;
+ * - Otherwise, [Filter.default] is used.
+ *
+ * This overload supports structured logging with exceptions or metadata.
+ *
+ * Example:
+ * ```kotlin
+ * val e = IOException("Timeout")
+ * Logger.d("Network") { "Failed to connect" with e }
+ * ```
+ *
+ * @param tag A label identifying the source of the log message.
+ * @param lazy A lambda returning a [dev.scarlet.logger.Content] instance, typically built using [dev.scarlet.logger.Content.Companion.with].
+ */
+@OverloadResolutionByLambdaReturnType
+@JvmName("debugLazyContent")
+fun Logger.d(tag: String, lazy: () -> Content) = asFilter().d(tag, lazy)
+
+/**
+ * Logs an info message lazily using a lambda that returns a [String].
+ *
+ * The [lazy] lambda is evaluated **only if the effective [Filter] permits this log entry**.
+ *
+ * The effective filter is determined as follows:
+ * - If the current [Logger] holds a dedicated [Filter] (e.g., created via [Logger.withFilter]), that filter is used;
+ * - Otherwise, [Filter.default] is used.
+ *
+ * Example:
+ * ```kotlin
+ * Logger.i("App") { "User ${user.id} logged in from ${user.ip}" }
+ * ```
+ *
+ * @param tag A label identifying the source of the log message.
+ * @param lazy A lambda producing the log message. Not invoked if the effective filter rejects the log.
+ */
+@JvmName("infoLazyMessage")
+fun Logger.i(tag: String, lazy: () -> String) = i(tag, lazy.asContent())
+
+/**
+ * Logs an info message lazily using a lambda that returns a [Content].
+ *
+ * The [lazy] lambda is evaluated **only if the effective [Filter] permits this log entry**.
+ *
+ * The effective filter is determined as follows:
+ * - If the current [Logger] holds a dedicated [Filter] (e.g., created via [Logger.withFilter]), that filter is used;
+ * - Otherwise, [Filter.default] is used.
+ *
+ * Useful for attaching contextual data or diagnostics at info level.
+ *
+ * Example:
+ * ```kotlin
+ * Logger.i("Migration") { "Database upgraded to v3" with e }
+ * ```
+ *
+ * @param tag A label identifying the source of the log message.
+ * @param lazy A lambda returning a [Content] instance, often built with [Content.Companion.with].
+ */
+@OverloadResolutionByLambdaReturnType
+@JvmName("infoLazyContent")
+fun Logger.i(tag: String, lazy: () -> Content) = asFilter().i(tag, lazy)
+
+/**
+ * Logs a warning message lazily using a lambda that returns a [String].
+ *
+ * The [lazy] lambda is evaluated **only if the effective [Filter] permits this log entry**.
+ *
+ * The effective filter is determined as follows:
+ * - If the current [Logger] holds a dedicated [Filter] (e.g., created via [Logger.withFilter]), that filter is used;
+ * - Otherwise, [Filter.default] is used.
+ *
+ * Example:
+ * ```kotlin
+ * Logger.w("Cache") { "Evicted ${cache.size} entries due to memory pressure" }
+ * ```
+ *
+ * @param tag A label identifying the component issuing the warning.
+ * @param lazy A lambda producing the warning message. Skipped if the effective filter rejects the log.
+ */
+@JvmName("warnLazyMessage")
+fun Logger.w(tag: String, lazy: () -> String) = w(tag, lazy.asContent())
+
+/**
+ * Logs a warning message lazily using a lambda that returns a [Content].
+ *
+ * The [lazy] lambda is evaluated **only if the effective [Filter] permits this log entry**.
+ *
+ * The effective filter is determined as follows:
+ * - If the current [Logger] holds a dedicated [Filter] (e.g., created via [Logger.withFilter]), that filter is used;
+ * - Otherwise, [Filter.default] is used.
+ *
+ * Ideal for warnings that include recoverable errors or diagnostic context.
+ *
+ * Example:
+ * ```kotlin
+ * val fallback = loadFallbackConfig()
+ * Logger.w("Config") { "Primary config missing, using fallback" with e }
+ * ```
+ *
+ * @param tag A label identifying the source of the warning.
+ * @param lazy A lambda returning a [Content] object containing message and optional exception.
+ */
+@OverloadResolutionByLambdaReturnType
+@JvmName("warnLazyContent")
+fun Logger.w(tag: String, lazy: () -> Content) = asFilter().w(tag, lazy)
+
+/**
+ * Logs an error message lazily using a lambda that returns a [String].
+ *
+ * The [lazy] lambda is evaluated **only if the effective [Filter] permits this log entry**.
+ *
+ * The effective filter is determined as follows:
+ * - If the current [Logger] holds a dedicated [Filter] (e.g., created via [Logger.withFilter]), that filter is used;
+ * - Otherwise, [Filter.default] is used.
+ *
+ * Example:
+ * ```kotlin
+ * Logger.e("Payment") { "Transaction failed for user ${userId}: ${transaction.details}" }
+ * ```
+ *
+ * @param tag A label identifying the component where the error occurred.
+ * @param lazy A lambda producing the error description. Evaluated only if the effective filter permits the log.
+ */
+@JvmName("errorLazyMessage")
+fun Logger.e(tag: String, lazy: () -> String) = e(tag, lazy.asContent())
+
+/**
+ * Logs an error message lazily using a lambda that returns a [Content].
+ *
+ * The [lazy] lambda is evaluated **only if the effective [Filter] permits this log entry**.
+ *
+ * The effective filter is determined as follows:
+ * - If the current [Logger] holds a dedicated [Filter] (e.g., created via [Logger.withFilter]), that filter is used;
+ * - Otherwise, [Filter.default] is used.
+ *
+ * This is the recommended way to log errors with associated exceptions,
+ * as it defers message construction and exception processing until necessary.
+ *
+ * Example:
+ * ```kotlin
+ * try {
+ * process(data)
+ * } catch (e: Exception) {
+ * Logger.e("Processor") { "Failed to handle input" with e }
+ * }
+ * ```
+ *
+ * @param tag A label identifying the error source.
+ * @param lazy A lambda returning a [Content] encapsulating the message and exception.
+ */
+@OverloadResolutionByLambdaReturnType
+@JvmName("errorLazyContent")
+fun Logger.e(tag: String, lazy: () -> Content) = asFilter().e(tag, lazy)
+
+private fun Logger.asFilter() = this as? FilterLogger ?: FilterLogger.of(this)
+
+private fun (() -> String).asContent() = { Content.of(msg = this()) }
\ No newline at end of file
diff --git a/logger/src/commonTest/kotlin/dev/scarlet/logger/filter/CompositeLoggerTest.kt b/logger/src/commonTest/kotlin/dev/scarlet/logger/filter/CompositeLoggerTest.kt
new file mode 100644
index 0000000..1cdd4ef
--- /dev/null
+++ b/logger/src/commonTest/kotlin/dev/scarlet/logger/filter/CompositeLoggerTest.kt
@@ -0,0 +1,358 @@
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.AbsLogger
+import dev.scarlet.logger.EmptyLogger
+import dev.scarlet.logger.Key
+import dev.scarlet.logger.Key.Companion.key
+import dev.scarlet.logger.Logger
+import dev.scarlet.logger.minus
+import dev.scarlet.logger.plus
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+/**
+ * @author Scarlet Pan
+ * @version 1.0.0
+ */
+class CompositeLoggerTest {
+
+ @Test
+ fun should_log_to_both_loggers_when_combined_with_plus() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val logger = a + b
+
+ logger.i("TEST", "hello")
+
+ assertEquals(listOf("A: hello"), a.logs)
+ assertEquals(listOf("B: hello"), b.logs)
+ }
+
+ @Test
+ fun should_support_chaining_multiple_loggers_with_plus() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val c = TestLogger("C")
+ val logger = a + b + c
+
+ logger.d("TAG", "world")
+
+ assertEquals(listOf("A: world"), a.logs)
+ assertEquals(listOf("B: world"), b.logs)
+ assertEquals(listOf("C: world"), c.logs)
+ }
+
+ @Test
+ fun should_preserve_order_of_execution_in_composite_logger() {
+ val order = mutableListOf()
+ val first = object : AbsLogger() {
+ override fun log(level: Logger.Level, tag: String, msg: String, tr: Throwable?) {
+ order.add("first")
+ }
+ }
+ val second = object : AbsLogger() {
+ override fun log(level: Logger.Level, tag: String, msg: String, tr: Throwable?) {
+ order.add("second")
+ }
+ }
+
+ val logger = first + second
+ logger.i("ORDER", "test")
+
+ assertEquals(listOf("first", "second"), order)
+ }
+
+ @Test
+ fun should_remove_single_logger_from_two_logger_composite() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val logger = a + b
+
+ val result = logger - b
+ result.w("RM", "msg")
+
+ assertEquals(listOf("A: msg"), a.logs)
+ assertTrue(b.logs.isEmpty())
+ }
+
+ @Test
+ fun should_remove_head_logger_and_return_tail_one() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val logger = a + b
+
+ val result = logger - a
+ result.e("ERR", "test")
+
+ assertTrue(a.logs.isEmpty())
+ assertEquals(listOf("B: test"), b.logs)
+ }
+
+ @Test
+ fun should_remove_logger_from_three_way_chain() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val c = TestLogger("C")
+ val logger = a + b + c
+
+ val result = logger - b
+ result.i("CHAIN", "hi")
+
+ assertEquals(listOf("A: hi"), a.logs)
+ assertTrue(b.logs.isEmpty())
+ assertEquals(listOf("C: hi"), c.logs)
+ }
+
+ @Test
+ fun should_do_nothing_when_removing_non_existent_logger() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val fake = TestLogger("Fake")
+ val logger = a + b
+
+ val result = logger - fake
+ result.d("KEEP", "keep")
+
+ assertEquals(listOf("A: keep"), a.logs)
+ assertEquals(listOf("B: keep"), b.logs)
+ }
+
+ @Test
+ fun should_return_empty_logger_when_all_loggers_are_removed() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val composite = a + b
+
+ val empty = composite - a - b
+
+ assertSame(EmptyLogger, empty)
+
+ empty.d("TAG", "should be silent")
+ empty.i("TAG", "still silent")
+ empty.w("TAG", "no effect")
+ empty.e("TAG", "nothing logged")
+
+ assertTrue(a.logs.isEmpty())
+ assertTrue(b.logs.isEmpty())
+ }
+
+ @Test
+ fun should_return_the_remaining_logger_directly_when_only_one_head() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val logger = a + b
+
+ val result = logger - b
+
+ assertSame(a, result)
+ }
+
+ @Test
+ fun should_not_modify_original_composite_when_removing_loggers() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val original = a + b
+
+ val modified = original - b
+
+ original.i("ORIG", "original")
+ modified.i("MOD", "modified")
+
+ assertEquals(listOf("A: original", "A: modified"), a.logs)
+ assertEquals(listOf("B: original"), b.logs) // B not called by 'modified'
+ }
+
+ @Test
+ fun should_remove_all_instances_of_a_logger_if_duplicated() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val logger = a + b + a // A → B → A
+
+ val result = logger - a // remove all A
+ result.w("DUP", "only_b")
+
+ assertTrue(a.logs.isEmpty())
+ assertEquals(listOf("B: only_b"), b.logs)
+ }
+
+ @Test
+ fun should_remove_multiple_loggers_by_subtracting_a_composite_logger() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val c = TestLogger("C")
+ val logger = a + b + c
+
+ val result = logger - (b + c)
+ result.i("GROUP", "final")
+
+ assertEquals(listOf("A: final"), a.logs)
+ assertTrue(b.logs.isEmpty())
+ assertTrue(c.logs.isEmpty())
+ }
+
+ @Test
+ fun should_remove_same_loggers_regardless_of_composite_order() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val c = TestLogger("C")
+ val logger = a + b + c
+
+ // Remove in reverse order: c + b
+ val result = logger - (c + b)
+ result.d("REV", "msg")
+
+ assertEquals(listOf("A: msg"), a.logs)
+ assertTrue(b.logs.isEmpty())
+ assertTrue(c.logs.isEmpty())
+ }
+
+ @Test
+ fun should_treat_duplicate_loggers_in_removal_set_as_single_entry() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val c = TestLogger("C")
+ val logger = a + b + c + b // two B's
+
+ val result = logger - (b + b + c)
+
+ result.e("DEDUP", "end")
+
+ assertEquals(listOf("A: end"), a.logs)
+ assertTrue(b.logs.isEmpty()) // both B instances gone
+ assertTrue(c.logs.isEmpty())
+ }
+
+ @Test
+ fun should_ignore_removal_of_composite_that_has_no_overlap() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val x = TestLogger("X")
+ val y = TestLogger("Y")
+
+ val logger = a + b
+
+ val result = logger - (x + y)
+ result.w("UNREL", "unchanged")
+
+ assertEquals(listOf("A: unchanged"), a.logs)
+ assertEquals(listOf("B: unchanged"), b.logs)
+ }
+
+ @Test
+ fun should_remove_logger_by_key_using_companion_as_identity() {
+ val a = TestLogger("A")
+ val file1 = FileLogger()
+ val file2 = FileLogger()
+ val logger = a + file1 + file2
+
+ // Remove all loggers whose companion == FileLogger.Companion (i.e., all FileLogger instances)
+ val result = logger - FileLogger
+
+ // Only 'a' remains
+ result.i("KEY", "test")
+
+ assertEquals(listOf("A: test"), a.logs)
+ assertTrue(file1.logs.isEmpty())
+ assertTrue(file2.logs.isEmpty())
+ }
+
+ @Test
+ fun should_remove_single_instance_logger_by_key() {
+ val console = TestLogger("Console")
+ val file = FileLogger()
+ val logger = console + file
+
+ val result = logger - FileLogger
+
+ assertSame(console, result) // unwrapped to single logger
+ result.d("SINGLE", "ok")
+
+ assertEquals(listOf("Console: ok"), console.logs)
+ assertTrue(file.logs.isEmpty())
+ }
+
+ @Test
+ fun should_return_empty_logger_when_removing_all_by_key() {
+ val f1 = FileLogger()
+ val f2 = FileLogger()
+ val logger = f1 + f2
+
+ val result = logger - FileLogger
+
+ assertSame(EmptyLogger, result)
+
+ result.w("EMPTY", "silent")
+ assertTrue(f1.logs.isEmpty())
+ assertTrue(f2.logs.isEmpty())
+ }
+
+ @Test
+ fun should_not_remove_if_key_does_not_match() {
+ val a = TestLogger("A")
+ val b = TestLogger("B")
+ val logger = a + b
+
+ // Try to remove FileLogger, but none exists
+ val result = logger - FileLogger
+
+ assertSame(logger, result) // unchanged composite
+
+ result.e("NOOP", "msg")
+ assertEquals(listOf("A: msg"), a.logs)
+ assertEquals(listOf("B: msg"), b.logs)
+ }
+
+ @Test
+ fun should_remove_multiple_types_by_chaining_key_subtractions() {
+ val a = TestLogger("A")
+ val f = FileLogger()
+ val logger = a + f
+
+ val result = logger - FileLogger - TestLogger
+
+ assertSame(EmptyLogger, result)
+
+ result.i("CHAIN_KEY", "gone")
+ assertTrue(a.logs.isEmpty())
+ assertTrue(f.logs.isEmpty())
+ }
+
+ @Test
+ fun should_work_with_mixed_removal_styles() {
+ val console = TestLogger("Console")
+ val file = FileLogger()
+ val logger = console + file
+
+ // Remove by instance and by key in same chain
+ val result = logger - console - FileLogger
+
+ assertSame(EmptyLogger, result)
+ }
+
+ private class TestLogger(val name: String) : AbsLogger() {
+
+ companion object : Key by key()
+
+ val logs = mutableListOf()
+
+ override fun log(level: Logger.Level, tag: String, msg: String, tr: Throwable?) {
+ logs.add("$name: $msg")
+ }
+ }
+
+ private class FileLogger : AbsLogger() {
+
+ companion object : Key by key()
+
+ val logs = mutableListOf()
+
+ override fun log(level: Logger.Level, tag: String, msg: String, tr: Throwable?) {
+ logs.add("$tag: $msg")
+ }
+ }
+}
+
+
+
diff --git a/logger/src/commonTest/kotlin/dev/scarlet/logger/filter/FilterLoggerTest.kt b/logger/src/commonTest/kotlin/dev/scarlet/logger/filter/FilterLoggerTest.kt
new file mode 100644
index 0000000..34cb05a
--- /dev/null
+++ b/logger/src/commonTest/kotlin/dev/scarlet/logger/filter/FilterLoggerTest.kt
@@ -0,0 +1,139 @@
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.AbsLogger
+import dev.scarlet.logger.Key
+import dev.scarlet.logger.Key.Companion.key
+import dev.scarlet.logger.Logger.Level
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+/**
+ * @author Scarlet Pan
+ * @version 1.0.0
+ */
+class FilterLoggerTest {
+
+ private lateinit var originalDefaultFilter: Filter
+
+ @BeforeTest
+ fun setUp() {
+ originalDefaultFilter = Filter.default
+ Filter._default = Filter.ALL
+ }
+
+ @AfterTest
+ fun tearDown() {
+ Filter._default = originalDefaultFilter
+ }
+
+ @Test
+ fun with_filter_blocks_debug_but_allows_warn() {
+ val testLogger = TestLogger()
+ val filteredLogger = testLogger.withFilter(Filter.atLeast(Level.WARN))
+
+ var debugLambdaExecuted = false
+ var warnLambdaExecuted = false
+
+ filteredLogger.d("TAG") {
+ debugLambdaExecuted = true
+ "debug message"
+ }
+
+ filteredLogger.w("TAG") {
+ warnLambdaExecuted = true
+ "warn message"
+ }
+
+ assertFalse(debugLambdaExecuted)
+ assertTrue(warnLambdaExecuted)
+ assertEquals(1, testLogger.logs.size)
+ assertEquals("TAG: warn message", testLogger.logs[0])
+ }
+
+ @Test
+ fun without_filter_restores_full_logging() {
+ val testLogger = TestLogger()
+ val filtered = testLogger.withFilter(Filter.NONE)
+ val restored = filtered.withoutFilter()
+
+ var debugExecuted = false
+ restored.d("TAG") {
+ debugExecuted = true
+ "debug after restore"
+ }
+
+ assertTrue(debugExecuted)
+ assertEquals(1, testLogger.logs.size)
+ assertEquals("TAG: debug after restore", testLogger.logs[0])
+ }
+
+ @Test
+ fun without_filter_on_non_wrapped_logger_is_no_op() {
+ val testLogger = TestLogger()
+ val result = testLogger.withoutFilter()
+ assertSame(testLogger, result)
+ }
+
+ @Test
+ fun multiple_filters_chain_correctly() {
+ val testLogger = TestLogger()
+ val doubleFiltered = testLogger
+ .withFilter(Filter.atLeast(Level.INFO)) // blocks DEBUG
+ .withFilter(Filter.atLeast(Level.ERROR)) // blocks INFO/WARN
+
+ var debugExec = false
+ var infoExec = false
+ var errorExec = false
+
+ doubleFiltered.d("T") { debugExec = true; "d" }
+ doubleFiltered.i("T") { infoExec = true; "i" }
+ doubleFiltered.e("T") { errorExec = true; "e" }
+
+ assertFalse(debugExec)
+ assertFalse(infoExec)
+ assertTrue(errorExec)
+ assertEquals(1, testLogger.logs.size)
+ assertEquals("T: e", testLogger.logs[0])
+ }
+
+ @Test
+ fun with_filter_does_not_respect_global_default_when_overridden() {
+ // Set global default to block DEBUG
+ Filter._default = Filter.atLeast(Level.INFO)
+
+ val testLogger = TestLogger()
+ // Apply a permissive local filter that allows DEBUG
+ val loggerWithPermissiveFilter = testLogger.withFilter(Filter.ALL)
+
+ var debugExec = false
+ loggerWithPermissiveFilter.d("TAG") {
+ debugExec = true
+ "this should be logged because local filter overrides global"
+ }
+
+ // Since the wrapped logger uses its own filter (not global),
+ // the lambda should execute and log.
+ assertTrue(debugExec)
+ assertEquals(1, testLogger.logs.size)
+ assertEquals(
+ "TAG: this should be logged because local filter overrides global",
+ testLogger.logs[0]
+ )
+ }
+
+ private class TestLogger : AbsLogger() {
+
+ companion object : Key by key()
+
+ val logs = mutableListOf()
+
+ override fun log(level: Level, tag: String, msg: String, tr: Throwable?) {
+ logs.add("$tag: $msg")
+ }
+ }
+}
\ No newline at end of file
diff --git a/logger/src/commonTest/kotlin/dev/scarlet/logger/filter/LazyLoggerTest.kt b/logger/src/commonTest/kotlin/dev/scarlet/logger/filter/LazyLoggerTest.kt
new file mode 100644
index 0000000..acd0644
--- /dev/null
+++ b/logger/src/commonTest/kotlin/dev/scarlet/logger/filter/LazyLoggerTest.kt
@@ -0,0 +1,97 @@
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.Content.Companion.with
+import dev.scarlet.logger.Logger
+import kotlin.random.Random
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+
+
+class LazyLoggerTest {
+
+ companion object {
+ private const val TAG = "LazyLoggerTest"
+ }
+
+ private lateinit var defaultFilter: Filter
+
+ @BeforeTest
+ fun setUp() {
+ defaultFilter = Filter.default
+ Filter._default = Filter.NONE
+ }
+
+ @AfterTest
+ fun tearDown() {
+ Filter._default = defaultFilter
+ }
+
+ @Test
+ fun logs_with_lazy_message() {
+ val list = mutableListOf()
+ repeat(Random.nextInt(1024)) {
+ list.add(Random.nextInt())
+ }
+ try {
+ check(list.all { it >= 0 })
+ Logger.i(TAG) { "All of element is non-negative in $list." }
+ } catch (e: Exception) {
+ Logger.w(TAG) { "At least one element is negative in $list." with e }
+ }
+ }
+
+ @Test
+ fun lazy_info_message_lambda_is_not_invoked_when_filtered_out() {
+ var executed = false
+ Logger.i(TAG) {
+ executed = true
+ "This should not be evaluated"
+ }
+ assertFalse(executed)
+ }
+
+ @Test
+ fun lazy_warn_message_lambda_is_not_invoked_when_filtered_out() {
+ var executed = false
+ Logger.w(TAG) {
+ executed = true
+ "Warning message"
+ }
+ assertFalse(executed)
+ }
+
+ @Test
+ fun lazy_error_message_lambda_is_not_invoked_when_filtered_out() {
+ var executed = false
+ Logger.e(TAG) {
+ executed = true
+ "Error message"
+ }
+ assertFalse(executed)
+ }
+
+ @Test
+ fun lazy_debug_message_with_exception_is_not_evaluated() {
+ var executed = false
+ val exception = RuntimeException("test")
+ Logger.d(TAG) {
+ executed = true
+ "Debug with exception" with exception
+ }
+ assertFalse(executed)
+ }
+
+ @Test
+ fun expensive_operation_in_lambda_is_avoided() {
+ var callCount = 0
+ fun expensiveOperation(): String {
+ callCount++
+ return "Expensive result: ${Random.nextInt()}"
+ }
+ Logger.i(TAG) { expensiveOperation() }
+ assertEquals(0, callCount)
+ }
+}
\ No newline at end of file
diff --git a/logger/src/iosMain/kotlin/dev/scarlet/logger/filter/Filter.ios.kt b/logger/src/iosMain/kotlin/dev/scarlet/logger/filter/Filter.ios.kt
new file mode 100644
index 0000000..40ea9fa
--- /dev/null
+++ b/logger/src/iosMain/kotlin/dev/scarlet/logger/filter/Filter.ios.kt
@@ -0,0 +1,5 @@
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.Logger.Level.INFO
+
+internal actual val platformFilter: Filter = Filter.atLeast(INFO)
\ No newline at end of file
diff --git a/logger/src/jsMain/kotlin/dev/scarlet/logger/filter/Filter.js.kt b/logger/src/jsMain/kotlin/dev/scarlet/logger/filter/Filter.js.kt
new file mode 100644
index 0000000..3dc8072
--- /dev/null
+++ b/logger/src/jsMain/kotlin/dev/scarlet/logger/filter/Filter.js.kt
@@ -0,0 +1,5 @@
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.Logger.Level.WARN
+
+internal actual val platformFilter: Filter = Filter.atLeast(WARN)
\ No newline at end of file
diff --git a/logger/src/jvmMain/kotlin/dev/scarlet/logger/filter/Filter.jvm.kt b/logger/src/jvmMain/kotlin/dev/scarlet/logger/filter/Filter.jvm.kt
new file mode 100644
index 0000000..40ea9fa
--- /dev/null
+++ b/logger/src/jvmMain/kotlin/dev/scarlet/logger/filter/Filter.jvm.kt
@@ -0,0 +1,5 @@
+package dev.scarlet.logger.filter
+
+import dev.scarlet.logger.Logger.Level.INFO
+
+internal actual val platformFilter: Filter = Filter.atLeast(INFO)
\ No newline at end of file