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