diff --git a/dependencies.md b/dependencies.md index 5a4ea5e77..c49adee64 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-logging-context-tests:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-context-tests:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. @@ -413,14 +413,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:00 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-fixtures:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-fixtures:2.0.0-SNAPSHOT.390` ## Runtime ## Compile, tests, and tooling @@ -1148,14 +1148,14 @@ This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:01 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-grpc-context:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-grpc-context:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1982,14 +1982,14 @@ This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:00 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jul-backend:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-jul-backend:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2800,14 +2800,14 @@ This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:00 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-default-platform:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-jvm-default-platform:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -3662,14 +3662,14 @@ This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:00 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-jul-backend-grpc-context:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-jvm-jul-backend-grpc-context:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -4520,14 +4520,14 @@ This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:01 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-jul-backend-std-context:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-jvm-jul-backend-std-context:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5370,14 +5370,14 @@ This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:01 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-log4j2-backend-std-context:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-jvm-log4j2-backend-std-context:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -6220,14 +6220,14 @@ This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:01 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-slf4j-jdk14-backend-std-context:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-jvm-slf4j-jdk14-backend-std-context:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -7078,14 +7078,14 @@ This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:01 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-jvm-slf4j-reload4j-backend-std-context:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-jvm-slf4j-reload4j-backend-std-context:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -7940,14 +7940,14 @@ This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:01 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-log4j2-backend:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-log4j2-backend:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8782,14 +8782,14 @@ This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:00 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging:2.0.0-SNAPSHOT.390` ## Runtime ## Compile, tests, and tooling @@ -9521,14 +9521,14 @@ This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:01 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-logging-testlib:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine.tools:spine-logging-testlib:2.0.0-SNAPSHOT.390` ## Runtime ## Compile, tests, and tooling @@ -10256,14 +10256,14 @@ This report was generated on **Sat Sep 13 14:12:17 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:00 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-probe-backend:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-probe-backend:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.auto.service. **Name** : auto-service-annotations. **Version** : 1.1.1. @@ -11122,14 +11122,14 @@ This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:00 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-smoke-test:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-smoke-test:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.18.3. @@ -12008,14 +12008,14 @@ This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:00 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-logging-std-context:2.0.0-SNAPSHOT.380` +# Dependencies of `io.spine:spine-logging-std-context:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -12826,6 +12826,6 @@ This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Sep 13 14:12:16 WEST 2025** using +This report was generated on **Mon Sep 15 12:08:00 WEST 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt index fb58a501d..08c60ddea 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/AbstractLogger.kt @@ -184,7 +184,9 @@ public abstract class AbstractLogger> protected constructo if (depth.getValue() <= MAX_ALLOWED_RECURSION_DEPTH) { backend.log(data) } else { - reportError("unbounded recursion in log statement", data) + reportError( + "Unbounded recursion in log statement (depth: ${depth.getValue()}).", data + ) } } } catch (logError: RuntimeException) { diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/CountingRateLimiter.kt b/logging/src/commonMain/kotlin/io/spine/logging/CountingRateLimiter.kt similarity index 100% rename from logging/src/jvmMain/kotlin/io/spine/logging/CountingRateLimiter.kt rename to logging/src/commonMain/kotlin/io/spine/logging/CountingRateLimiter.kt diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/DurationRateLimiter.kt b/logging/src/commonMain/kotlin/io/spine/logging/DurationRateLimiter.kt similarity index 100% rename from logging/src/jvmMain/kotlin/io/spine/logging/DurationRateLimiter.kt rename to logging/src/commonMain/kotlin/io/spine/logging/DurationRateLimiter.kt diff --git a/logging/src/commonMain/kotlin/io/spine/logging/KeyPart.kt b/logging/src/commonMain/kotlin/io/spine/logging/KeyPart.kt index 55b9cae52..1862c9644 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/KeyPart.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/KeyPart.kt @@ -35,30 +35,95 @@ package io.spine.logging * @see * Original Flogger code for historic reference. */ -public expect class KeyPart { +public class KeyPart private constructor(scope: LoggingScope) { + + private val ref = WeakRef(scope) + + /** + * The concurrency lock for accessing [onCloseHooks]. + */ + private val hookLock = Any() + + /** + * The list of functions to execute when the instance is [closed][close]. + */ + private val onCloseHooks = mutableListOf<() -> Unit>() /** - * Adds a hook that will be executed when this key part is closed. - * - * @param hook the function to execute on close + * Adds the function to be executed when the instance is [closed][close]. */ - @Suppress("unused") // the parameter is used by `actual` impl. - public fun addOnCloseHook(hook: () -> Unit) + public fun addOnCloseHook(hook: () -> Unit) { + synchronized(hookLock) { + onCloseHooks += hook + } + } /** - * Closes this key part and executes all registered close hooks. - * - * After closing, the key part becomes invalid and should not be used. + * Executes clean-up functions previously added by the [addOnCloseHook] functions. */ - public fun close() + public fun close() { + // If this were ever too "bursty" due to removal of many keys for the same scope, + // we could modify this code to process only a maximum number of removals each time + // and keep a single "in progress" `KeyPart` around until the next time. + + // This executes once for each map entry created in the enclosing scope. + // It is very dependent on logging usage in the scope and theoretically unbounded. + val hooksToRun: List<() -> Unit> = synchronized(hookLock) { + // Snapshot and clear under lock to avoid concurrent modification. + val copy = onCloseHooks.toList() + onCloseHooks.clear() + copy + } + // Invoke hooks outside the lock to avoid holding the lock during the user code. + for (hook in hooksToRun) { + hook.invoke() + } + } public companion object { /** - * Removes keys that are no longer in use from the internal storage. - * - * This helps prevent memory leaks by cleaning up abandoned key references. + * The lock for accessing [registry]. */ - public fun removeUnusedKeys() + private val registryLock = Any() + + /** + * Contains [KeyPart] instances produced by the [create] function. + */ + private val registry = mutableSetOf() + + public fun create(scope: LoggingScope): KeyPart = + KeyPart(scope).also { key -> + synchronized(registryLock) { + registry += key + } + } + + /** + * Removes the keys that already do not have referenced scopes. + */ + public fun removeUnusedKeys() { + // There are always more specialized keys than entries in the reference queue, + // so the queue should be empty most of the time we get here. + + // Snapshot the registry to avoid concurrent modification during iteration. + val snapshot: List = synchronized(registryLock) { + registry.toList() + } + + val dead = ArrayList() + for (k in snapshot) { + if (k.ref.get() == null) { + dead += k + } + } + for (k in dead) { + // Close outside the registry lock. + k.close() + synchronized(registryLock) { + registry.remove(k) + } + } + } } } diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/LogContext.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt similarity index 99% rename from logging/src/jvmMain/kotlin/io/spine/logging/LogContext.kt rename to logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt index ee76638a9..a7beaa429 100644 --- a/logging/src/jvmMain/kotlin/io/spine/logging/LogContext.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogContext.kt @@ -33,7 +33,6 @@ import io.spine.logging.backend.Metadata import io.spine.logging.backend.Platform import io.spine.logging.context.Tags import io.spine.logging.util.Checks.checkNotNull -import io.spine.reflect.CallerFinder.stackForCallerOf import kotlin.time.DurationUnit /** @@ -315,10 +314,10 @@ protected constructor( // // By skipping the initial code inside this method, we don't trigger any stack // capture until after the "log" method. - val context = LogSiteStackTrace( + val context = LogSiteStackTrace.create( _metadata!!.findValue(Key.LOG_CAUSE), stackSize, - stackForCallerOf(LogContext::class.java, stackSize.maxDepth, 1) + stackForCallerOf(LogContext::class, stackSize.maxDepth, 1) ) // The "cause" is a unique metadata key, we must replace any existing value. addMetadata(Key.LOG_CAUSE, context) diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt index 1c819e205..e298be5ef 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,238 @@ package io.spine.logging +import io.spine.annotation.TestOnly + /** - * Platform-neutral marker for a bucketing strategy used by - * [LoggingApi.per(key, strategy)][LoggingApi.per]. + * Provides a strategy for "bucketing" a potentially unbounded set of log + * aggregation keys used by the [LoggingApi.per] method. + * + * When implementing new strategies not provided by this class, it is important + * to ensure that the [apply] method returns values from a bounded set of + * instances wherever possible. + * + * This is important because the returned values are held persistently for + * potentially many different log sites. + * + * If a different instance is returned each time [apply] is called, a + * different instance will be held in each log site. + * + * This multiplies the amount of memory that is retained indefinitely by + * any use of [LoggingApi.per]. + * + * One way to handle arbitrary key types would be to create a strategy which + * "interns" instances in some way, to produce singleton identifiers. + * + * Unfortunately, interning can itself be a cause of unbounded memory leaks, + * so a bucketing strategy wishing to perform interning should probably + * support a user defined maximum capacity to limit the overall risk. + * + * If too many instances are seen, the strategy should begin to return `null` + * (and log an appropriate warning). + * + * The additional complexity created by this approach really tells us that + * types which require interning in order to be used as aggregation keys + * should be considered unsuitable, and callers should seek alternatives. + * + * @param name The name of this strategy, used for debugging purposes. + * + * @see + * Original Java code for historical context. */ -public expect abstract class LogPerBucketingStrategy +public abstract class LogPerBucketingStrategy protected constructor( + private val name: String +) { + /** + * Maps a log aggregation key from a potentially unbounded set of + * key values to a bounded set of instances. + * + * Implementations of this method should be efficient and avoid + * allocating memory wherever possible. + * + * The returned value must be an immutable identifier with minimal additional + * allocation requirements and ideally have singleton semantics + * (e.g., an [Enum] or [Integer] value). + * + * **Warning**: If keys are not known to have natural singleton semantics + * (e.g. [String]) then returning the given key instance is generally a bad idea. + * + * Even if the set of key values is small, the set of distinct allocated instances + * passed to [LoggingApi.per] can be unbounded, and that's what matters. + * + * As such, it is always better to map keys to some singleton identifier or + * intern the keys in some way. + * + * @param key A non-null key from a potentially unbounded set of log aggregation keys. + * @return an immutable value from some known bounded set, which will be held persistently by + * internal Flogger data structures as part of the log aggregation feature. If + * `null` is returned, the corresponding call to `per(key, STRATEGY)` has no effect. + */ + protected abstract fun apply(key: T): Any? + + internal fun doApply(key: T): Any? = apply(key) + + /** + * Access to the [apply] method for testing purposes. + * + * This method is not part of the public API and should not be used by client code. + */ + @TestOnly + internal fun applyForTesting(key: T): Any? = apply(key) + + override fun toString(): String = + "${LogPerBucketingStrategy::class.simpleName}[$name]" + + public companion object { + + /** + * A strategy to use only if the set of log aggregation keys is known to be + * a strictly bounded set of instances with singleton semantics. + */ + private val KNOWN_BOUNDED = object : LogPerBucketingStrategy("KnownBounded") { + override fun apply(key: Any): Any = key + } + + /** + * This is a "safe" strategy as far as memory use is concerned since class objects + * are effectively singletons. + */ + private val BY_CLASS = object : LogPerBucketingStrategy("ByClass") { + override fun apply(key: Any): Any = key::class + } + + /** + * This is a "safe" strategy as far as memory use is concerned, because a class object + * returns the same string instance every time its called, and class objects + * are effectively singletons. + */ + private val BY_CLASS_NAME = object : LogPerBucketingStrategy("ByClassName") { + override fun apply(key: Any): Any = key::class.qualifiedName!! + /* This is a naturally interned value, so no need to call `intern()`. */ + } + + /** + * A strategy to use only if the set of log aggregation keys is known to be + * a strictly bounded set of instances with singleton semantics. + * + * **WARNING**: When using this strategy, keys passed to [LoggingApi.per] + * are used as-is by the log aggregation code, and held indefinitely by internal + * static data structures. + * + * As such it is vital that key instances used with this strategy have singleton semantics + * (i.e., if `k1.equals(k2)` then `k1 == k2`). + * + * Failure to adhere to this requirement is likely to result in hard to detect memory leaks. + * + * If keys do not have singleton semantics then you should use a different strategy, + * such as [byHashCode] or [byClass]. + */ + @JvmStatic + public fun knownBounded(): LogPerBucketingStrategy = KNOWN_BOUNDED + + /** + * A strategy which uses the [Class] of the given key for log aggregation. + * + * This is useful when you need to aggregate over specific exceptions or similar + * type-distinguished instances. + * + * Note that using this strategy will result in a reference to the [Class] object of + * the key being retained indefinitely. + * + * This will prevent class unloading from occurring for affected classes, and + * it is up to the caller to decide if this is acceptable or not. + */ + @JvmStatic + public fun byClass(): LogPerBucketingStrategy = BY_CLASS + + /** + * A strategy which uses the [Class] name of the given key for log aggregation. + * + * This is useful when you need to aggregate over specific exceptions or similar + * type-distinguished instances. + * + * This is an alternative strategy to [byClass] which avoids holding onto the class + * instance and avoids any issues with class unloading. + * + * However, it may conflate classes if applications use complex arrangements of custom + * class-loaders, but this should be extremely rare. + */ + @JvmStatic + public fun byClassName(): LogPerBucketingStrategy = BY_CLASS_NAME + + /** + * A strategy defined for some given set of known keys. + * + * Unlike [knownBounded], this strategy maps keys to a bounded set of identifiers, and + * permits the use of non-singleton keys in [LoggingApi.per]. + * + * If keys outside this set are used this strategy returns `null`, and + * log aggregation will not occur. + * + * Duplicates in [knownKeys] are ignored. + */ + @JvmStatic + public fun forKnownKeys(knownKeys: Iterable): LogPerBucketingStrategy { + val keyMap = HashMap() + val name = buildString { + append("ForKnownKeys(") + var index = 0 + for (key in knownKeys) { + if (!keyMap.containsKey(key)) { + if (index > 0) { + append(", ") + } + append(key) + keyMap[key] = index + index++ + } + } + append(')') + } + // We check here to avoid querying `knownKeys` size just for the precondition check. + require(!keyMap.isEmpty()) { "`knownKeys` must not be empty." } + return object : LogPerBucketingStrategy(name) { + override fun apply(key: Any): Any? = keyMap[key] + } + } + + /** + * A strategy which uses the [hashCode] of a given key, modulo [maxBuckets], for + * log aggregation. + * + * This is a fallback strategy for cases where the set of possible values is + * not known in advance or could be arbitrarily large in unusual circumstances. + * + * When using this method, it is obviously important that the [hashCode] method of + * the expected keys is well distributed, since duplicate hash codes, or hash codes + * congruent to [maxBuckets] will cause keys to be conflated. + * + * The caller is responsible for deciding the number of unique log aggregation keys this + * strategy can return. This choice is a trade-off between memory usage and the risk of + * conflating keys when performing log aggregation. + * + * Each log site using this strategy will hold up to [maxBuckets] distinct versions of log + * site information to allow rate limiting and other stateful operations to be applied + * separately per bucket. + * + * The overall allocation cost depends on the type of rate limiting used alongside this + * method, but it scales linearly with [maxBuckets]. + * + * It is recommended to keep the value of [maxBuckets] below 250, since this + * guarantees no additional allocations will occur when using this strategy. + * However, the value chosen should be as small as practically possible for + * the typical expected number of unique keys. + * + * To avoid unwanted allocation at log sites, users are strongly encouraged to assign the + * returned value to a static field and pass that to any log statements which need it. + */ + @JvmStatic + public fun byHashCode(maxBuckets: Int): LogPerBucketingStrategy { + require(maxBuckets > 0) { "`maxBuckets` must be positive: $maxBuckets." } + return object : LogPerBucketingStrategy("ByHashCode($maxBuckets)") { + @Suppress("MagicNumber") + override fun apply(key: Any): Any = + Math.floorMod(key.hashCode(), maxBuckets) - 128 + } + } + } +} diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/LogSiteGroupingKey.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogSiteGroupingKey.kt similarity index 100% rename from logging/src/jvmMain/kotlin/io/spine/logging/LogSiteGroupingKey.kt rename to logging/src/commonMain/kotlin/io/spine/logging/LogSiteGroupingKey.kt diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LogSiteLookup.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogSiteLookup.kt index a17e34e95..b18610ac7 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LogSiteLookup.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogSiteLookup.kt @@ -26,32 +26,31 @@ package io.spine.logging +import io.spine.logging.backend.Platform import kotlin.reflect.KClass /** - * Determines log sites for the current line of code. - * - * Note that determining of a log site at runtime can be a slow - * operation because it usually involves some form of stack trace analysis. - * - * Methods of this class can be used with the [LoggingApi.withInjectedLogSite] - * method to implement logging helper methods. + * Determines log sites for the current line of code using + * [LogCallerFinder][io.spine.logging.backend.LogCallerFinder] + * obtained from a [io.spine.logging.backend.Platform]. */ -public expect object LogSiteLookup { +public object LogSiteLookup { /** * Returns a [LogSite] for the caller of the specified class. * - * In some platforms, log site determination may be unsupported, and in - * those cases this method should return the [LogSite.Invalid] instance. + * If log site determination is unsupported, this method returns + * the [LogSite.Invalid] instance. */ - public fun callerOf(loggingApi: KClass<*>): LogSite + public fun callerOf(loggingApi: KClass<*>): LogSite = + Platform.getCallerFinder().findLogSite(loggingApi, 0) /** * Returns a [LogSite] for the current line of code. * - * In some platforms, log site determination may be unsupported, and in - * those cases this method should return the [LogSite.Invalid] instance. + * If log site determination is unsupported, this method returns + * the [LogSite.Invalid] instance. */ - public fun logSite(): LogSite + public fun logSite(): LogSite = + Platform.getCallerFinder().findLogSite(LogSiteLookup::class, 0) } diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/LogSiteMap.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogSiteMap.kt similarity index 100% rename from logging/src/jvmMain/kotlin/io/spine/logging/LogSiteMap.kt rename to logging/src/commonMain/kotlin/io/spine/logging/LogSiteMap.kt diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LogSiteStackTrace.kt b/logging/src/commonMain/kotlin/io/spine/logging/LogSiteStackTrace.kt index 803998e6d..df14805ef 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LogSiteStackTrace.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LogSiteStackTrace.kt @@ -30,4 +30,18 @@ package io.spine.logging * A synthetic exception which can be attached to log statements when additional stack trace * information is required in log files or via tools such as ECatcher. */ -public expect class LogSiteStackTrace +@Suppress("UtilityClassWithPublicConstructor") +public expect class LogSiteStackTrace : Throwable { + + public companion object { + + /** + * Creates a new log stack trace instance with the given properties. + */ + public fun create( + cause: Throwable?, + stackSize: StackSize, + syntheticStackTrace: Array + ): LogSiteStackTrace + } +} diff --git a/logging/src/commonMain/kotlin/io/spine/logging/LoggingScope.kt b/logging/src/commonMain/kotlin/io/spine/logging/LoggingScope.kt index f89e2bcce..f1789d60c 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/LoggingScope.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/LoggingScope.kt @@ -26,6 +26,8 @@ package io.spine.logging +import io.spine.annotation.VisibleForTesting + /** * An opaque scope marker which can be attached to log sites to provide "per scope" behaviour * for stateful logging operations (e.g., rate limiting). @@ -48,19 +50,7 @@ package io.spine.logging * @see * Original Java code for historical context. */ -public expect abstract class LoggingScope { - - public companion object { - - /** - * Creates a scope which automatically removes any associated keys - * from [LogSiteMap]s when it is garbage collected. - * - * The given label is used only for debugging purposes and may appear in log - * statements, it should not contain any user data or other runtime information. - */ - public fun create(label: String): LoggingScope - } +public abstract class LoggingScope protected constructor(private val label: String) { /** * Returns a specialization of the given key which accounts for this scope instance. @@ -94,4 +84,65 @@ public expect abstract class LoggingScope { * all the currently active scopes which apply to it. */ protected abstract fun onClose(removalHook: () -> Unit) + + /** + * Opens [specialize] for the package. + */ + internal fun doSpecialize(key: LogSiteKey): LogSiteKey = specialize(key) + + /** + * Opens access to [onClose] for the package. + */ + internal fun doOnClose(removalHook: () -> Unit) = onClose(removalHook) + + /** + * Returns the [label] of this scope. + */ + override fun toString(): String = label + + public companion object { + + /** + * Creates a scope which automatically removes any associated keys + * from [LogSiteMap]s when it is garbage collected. + * + * The given label is used only for debugging purposes and may appear in log + * statements, it should not contain any user data or other runtime information. + */ + @JvmStatic + public fun create(label: String): LoggingScope = WeakScope(label) + } + + @VisibleForTesting + internal class WeakScope(label: String) : LoggingScope(label) { + + /** + * Do NOT reference the Scope directly from a specialized key, use the "key part" + * to avoid the key part weak reference is enqueued which triggers tidy up at the next + * call to `specializeForScopesIn()` where scopes are used. + * + * This must be unique per scope since it acts as a qualifier within specialized + * log site keys. Using a different weak reference per specialized key would not work + * (which is part of the reason we also need the "on close" queue as well as + * the reference queue). + */ + private val keyPart: KeyPart = KeyPart.create(this) + + override fun specialize(key: LogSiteKey): LogSiteKey = + SpecializedLogSiteKey.of(key, keyPart) + + override fun onClose(removalHook: () -> Unit) { + // Clear the reference queue about as often as we would add a new key to a map. + // This should still mean that the queue is almost always empty when we check + // it (since we expect more than one specialized log site key per scope) and it + // avoids spamming the queue clearance loop for every log statement and avoids + // class loading the reference queue until we know scopes have been used. + KeyPart.removeUnusedKeys() + keyPart.addOnCloseHook(removalHook) + } + + internal fun closeForTesting() { + keyPart.close() + } + } } diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/MutableMetadata.kt b/logging/src/commonMain/kotlin/io/spine/logging/MutableMetadata.kt similarity index 100% rename from logging/src/jvmMain/kotlin/io/spine/logging/MutableMetadata.kt rename to logging/src/commonMain/kotlin/io/spine/logging/MutableMetadata.kt diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/RateLimitPeriod.kt b/logging/src/commonMain/kotlin/io/spine/logging/RateLimitPeriod.kt similarity index 95% rename from logging/src/jvmMain/kotlin/io/spine/logging/RateLimitPeriod.kt rename to logging/src/commonMain/kotlin/io/spine/logging/RateLimitPeriod.kt index ad34cdf6d..8ad06d29d 100644 --- a/logging/src/jvmMain/kotlin/io/spine/logging/RateLimitPeriod.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/RateLimitPeriod.kt @@ -35,8 +35,8 @@ import kotlin.time.toTimeUnit * Immutable metadata for rate limiting based on a fixed count. * * This corresponds to the - * [LOG_AT_MOST_EVERY][io.spine.logging.jvm.LogContext.Key.LOG_AT_MOST_EVERY] - * metadata key in [io.spine.logging.backend.LogData]. + * [LOG_AT_MOST_EVERY][LogContext.Key.LOG_AT_MOST_EVERY] + * metadata key in [LogData][io.spine.logging.backend.LogData]. * * Unlike the metadata for `every(N)`, we need to use a wrapper class here to preserve * the time unit information for accurate rate limit calculations. diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/RateLimitStatus.kt b/logging/src/commonMain/kotlin/io/spine/logging/RateLimitStatus.kt similarity index 100% rename from logging/src/jvmMain/kotlin/io/spine/logging/RateLimitStatus.kt rename to logging/src/commonMain/kotlin/io/spine/logging/RateLimitStatus.kt diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/SamplingRateLimiter.kt b/logging/src/commonMain/kotlin/io/spine/logging/SamplingRateLimiter.kt similarity index 100% rename from logging/src/jvmMain/kotlin/io/spine/logging/SamplingRateLimiter.kt rename to logging/src/commonMain/kotlin/io/spine/logging/SamplingRateLimiter.kt diff --git a/logging/src/commonMain/kotlin/io/spine/logging/StackTraceElement.kt b/logging/src/commonMain/kotlin/io/spine/logging/StackTraceElement.kt index 82de9e1eb..5386c2b87 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/StackTraceElement.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/StackTraceElement.kt @@ -26,7 +26,22 @@ package io.spine.logging +import kotlin.reflect.KClass + /** * Platform-neutral representation of a stack trace element. */ public expect class StackTraceElement + +/** + * Creates a stack trace array with [io.spine.logging.StackTraceElement] for the given arguments. + * + * @param target The class to get the stack from. + * @param maxDepth The maximum size of the returned stack (pass -1 for the complete stack). + * @param skip The minimum number of stack frames to skip before looking for callers. + */ +public expect fun stackForCallerOf( + target: KClass<*>, + maxDepth: Int, + skip: Int +): Array diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/WithLogging.kt b/logging/src/commonMain/kotlin/io/spine/logging/WeakRef.kt similarity index 63% rename from logging/src/jvmMain/kotlin/io/spine/logging/WithLogging.kt rename to logging/src/commonMain/kotlin/io/spine/logging/WeakRef.kt index 01d646312..f91d1ad3f 100644 --- a/logging/src/jvmMain/kotlin/io/spine/logging/WithLogging.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/WeakRef.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023, TeamDev. All rights reserved. + * Copyright 2025, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,34 +26,20 @@ package io.spine.logging -import io.spine.logging.LoggingFactory.loggerFor - /** - * Provides [Logger] instance as a property. - * - * Implement this interface when logging is needed. + * A weak reference holder that manages references which can be garbage collected. * - * Usage example: + * This class provides platform-specific implementation of weak references, + * which allow objects to be garbage collected when they are no longer strongly + * referenced elsewhere in the application. * - * ```kotlin - * class MyClass : WithLogging { - * fun doAction() { - * logger.atInfo().log { "Action is in progress." } - * } - * } - * ``` + * @param T The type of the referenced object. + * @property referent The object to be weakly referenced. */ -public actual interface WithLogging { - - /** - * Returns the logger created for this class. - */ - public actual val logger: Logger<*> - get() = loggerFor(this::class) +internal expect class WeakRef(referent: T) { /** - * Convenience method for obtaining the logger created for this class - * when calling from Java code, avoiding the `get` prefix. + * Returns the referent object, or `null` if it has been garbage collected. */ - public fun logger(): Logger<*> = logger + fun get(): T? } diff --git a/logging/src/commonMain/kotlin/io/spine/logging/WithLogging.kt b/logging/src/commonMain/kotlin/io/spine/logging/WithLogging.kt index 87950902f..2587eb86b 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/WithLogging.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/WithLogging.kt @@ -26,6 +26,8 @@ package io.spine.logging +import io.spine.logging.LoggingFactory.loggerFor + /** * Provides [Logger] instance as a property. * @@ -64,11 +66,17 @@ package io.spine.logging * As for now, providing a default implementation for a property makes it * impossible to customize accessing of a logger in target implementations. */ -public expect interface WithLogging { +public interface WithLogging { /** * Returns the logger created for this class. */ - @Suppress("RedundantModalityModifier") // `open` is required for the JVM impl. to override. - public open val logger: Logger<*> + public val logger: Logger<*> + get() = loggerFor(this::class) + + /** + * Convenience method for obtaining the logger created for this class + * when calling from Java code, avoiding the `get` prefix. + */ + public fun logger(): Logger<*> = logger } diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/backend/SimpleMessageFormatter.kt b/logging/src/commonMain/kotlin/io/spine/logging/backend/SimpleMessageFormatter.kt similarity index 99% rename from logging/src/jvmMain/kotlin/io/spine/logging/backend/SimpleMessageFormatter.kt rename to logging/src/commonMain/kotlin/io/spine/logging/backend/SimpleMessageFormatter.kt index 9af555d89..7d854298a 100644 --- a/logging/src/jvmMain/kotlin/io/spine/logging/backend/SimpleMessageFormatter.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/backend/SimpleMessageFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2023, The Flogger Authors; 2025, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logging/src/commonMain/kotlin/io/spine/logging/util/RecursionDepth.kt b/logging/src/commonMain/kotlin/io/spine/logging/util/RecursionDepth.kt index f8b0ca695..070b11065 100644 --- a/logging/src/commonMain/kotlin/io/spine/logging/util/RecursionDepth.kt +++ b/logging/src/commonMain/kotlin/io/spine/logging/util/RecursionDepth.kt @@ -27,39 +27,105 @@ package io.spine.logging.util import io.spine.annotation.Internal +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * A platform-neutral API for tracking recursion depth of logging operations. * - * Use `Platform.getCurrentRecursionDepth()` to query the current depth from outside - * of the core logging internals. + * The current recursion depth is stored as a [CoroutineContext.Element] which makes it + * coroutine-friendly while remaining platform-neutral. * - * ### API Note + * Use `Platform.getCurrentRecursionDepth()` to query the current depth from the outside + * of the core logging internals. * - * This class is an internal detail and must not be used outside the core of the Logging library. + * API Note: This class is an internal detail and must not be used outside the core of + * the Logging library. Backends which need to know the recursion depth should call + * `io.spine.logging.backend.Platform.getCurrentRecursionDepth()`. */ -public expect class RecursionDepth() : AutoCloseable { +public class RecursionDepth private constructor() : CoroutineContext.Element, AutoCloseable { + + override val key: CoroutineContext.Key<*> get() = Key + + private var value: Int = 0 + + /** + * Do not call this method directly, use `Platform.getCurrentRecursionDepth()`. + */ + @Internal + public fun getValue(): Int = value + + override fun close() { + if (value > 0) { + value -= 1 + if (value == 0) { + // Remove the element from the current context when it reaches zero. + CurrentContext.set(CurrentContext.get().minusKey(Key)) + } + return + } + error("Mismatched calls to `RecursionDepth`.") + } - public companion object { + + + /** + * The [CoroutineContext.Key] for managing [RecursionDepth] in a [CoroutineContext]. + */ + public companion object Key : CoroutineContext.Key { + /** + * Holds the current coroutine context for logging operations. + * + * The recursion depth itself is stored inside the context element, not in this holder. + */ + private object CurrentContext { + private var holder: CoroutineContext = EmptyCoroutineContext + fun get(): CoroutineContext = holder + fun set(ctx: CoroutineContext) { + holder = ctx + } + } /** * Do not call this method directly, use `Platform.getCurrentRecursionDepth()`. */ @Internal - public fun getCurrentDepth(): Int + @JvmStatic + public fun getCurrentDepth(): Int = CurrentContext.get()[Key]?.value ?: 0 /** * Do not call this method directly, use `Platform.getCurrentRecursionDepth()`. */ @Internal - public fun enterLogStatement(): RecursionDepth + @JvmStatic + public fun enterLogStatement(): RecursionDepth { + val ctx = CurrentContext.get() + val depth = ctx[Key] ?: RecursionDepth() + depth.value += 1 + if (depth.value == 0) { + error("`RecursionDepth` with -1 value encountered.") + } + CurrentContext.set(ctx + depth) + return depth + } + } - /** - * Do not call this method directly, use `Platform.getCurrentRecursionDepth()`. - */ - @Internal - public fun getValue(): Int + override fun toString(): String { + return "RecursionDepth(value=$value)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (this::class != other::class) return false + + other as RecursionDepth - override fun close() + return value == other.value + } + + override fun hashCode(): Int { + return value + } } diff --git a/logging/src/commonTest/kotlin/io/spine/logging/LogSiteStackTraceSpec.kt b/logging/src/commonTest/kotlin/io/spine/logging/LogSiteStackTraceSpec.kt index 472a46829..733b0a457 100644 --- a/logging/src/commonTest/kotlin/io/spine/logging/LogSiteStackTraceSpec.kt +++ b/logging/src/commonTest/kotlin/io/spine/logging/LogSiteStackTraceSpec.kt @@ -53,26 +53,26 @@ internal class LogSiteStackTraceSpec { @Test fun `return message containing the requested stack size`() { - val trace = LogSiteStackTrace(null, StackSize.FULL, arrayOfNulls(0)) + val trace = LogSiteStackTrace.create(null, StackSize.FULL, arrayOfNulls(0)) trace shouldHaveMessage "FULL" } @Test fun `return the given cause`() { val cause = RuntimeException() - val trace = LogSiteStackTrace(cause, StackSize.SMALL, arrayOfNulls(0)) + val trace = LogSiteStackTrace.create(cause, StackSize.SMALL, arrayOfNulls(0)) trace.cause shouldBeSameInstanceAs cause } @Test fun `allow nullable cause`() { - val trace = LogSiteStackTrace(null, StackSize.NONE, arrayOfNulls(0)) + val trace = LogSiteStackTrace.create(null, StackSize.NONE, arrayOfNulls(0)) trace.cause.shouldBeNull() } @Test fun `return the given stack trace`() { - val trace = LogSiteStackTrace(null, StackSize.SMALL, FAKE_STACK) + val trace = LogSiteStackTrace.create(null, StackSize.SMALL, FAKE_STACK) trace.stackTrace shouldNotBeSameInstanceAs FAKE_STACK trace.stackTrace shouldBe FAKE_STACK } diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/KeyPart.kt b/logging/src/jvmMain/kotlin/io/spine/logging/KeyPart.kt deleted file mode 100644 index d34aa366e..000000000 --- a/logging/src/jvmMain/kotlin/io/spine/logging/KeyPart.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2020, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging - -import java.lang.ref.ReferenceQueue -import java.lang.ref.WeakReference -import java.util.concurrent.ConcurrentLinkedQueue - -/** - * This class is only loaded once we've seen scopes in action (Android doesn't like - * eager class loading, and many Android apps won't use scopes). This forms part of each - * log site key, so must have singleton semantics per scope. - * - * This file encapsulates Java-specific weak-reference and concurrent queue usage to keep - * the [LoggingScope] API Kotlin-only. - * - * @see - * Original Flogger code for historic reference. - */ -public actual class KeyPart internal constructor(scope: LoggingScope) : - WeakReference(scope, queue) { - - private val onCloseHooks = ConcurrentLinkedQueue<() -> Unit>() - - public actual fun addOnCloseHook(hook: () -> Unit) { - onCloseHooks.offer(hook) - } - - // If this were ever too "bursty" due to removal of many keys for the same scope, - // we could modify this code to process only a maximum number of removals each time - // and keep a single "in progress" KeyPart around until the next time. - public actual fun close() { - // This executes once for each map entry created in the enclosing scope. - // It is very dependent on logging usage in the scope and theoretically unbounded. - var r = onCloseHooks.poll() - while (r != null) { - r() - r = onCloseHooks.poll() - } - } - - public actual companion object { - - /** - * The singleton reference queue of the logging scopes. - * - * The queue is populated via [KeyPart] which descends from [WeakReference]. - * - * @see KeyPart - */ - private val queue = ReferenceQueue() - - public actual fun removeUnusedKeys() { - // There are always more specialized keys than entries in the reference queue, - // so the queue should be empty most of the time we get here. - var p = queue.poll() as KeyPart? - while (p != null) { - p.close() - p = queue.poll() as KeyPart? - } - } - } -} diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt b/logging/src/jvmMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt deleted file mode 100644 index f14ebdeb4..000000000 --- a/logging/src/jvmMain/kotlin/io/spine/logging/LogPerBucketingStrategy.kt +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging - -import io.spine.annotation.TestOnly - -/** - * Provides a strategy for "bucketing" a potentially unbounded set of log - * aggregation keys used by the [LoggingApi.per] method. - * - * When implementing new strategies not provided by this class, it is important - * to ensure that the [apply] method returns values from a bounded set of - * instances wherever possible. - * - * This is important because the returned values are held persistently for - * potentially many different log sites. - * - * If a different instance is returned each time [apply] is called, a - * different instance will be held in each log site. - * - * This multiplies the amount of memory that is retained indefinitely by - * any use of [LoggingApi.per]. - * - * One way to handle arbitrary key types would be to create a strategy which - * "interns" instances in some way, to produce singleton identifiers. - * - * Unfortunately, interning can itself be a cause of unbounded memory leaks, - * so a bucketing strategy wishing to perform interning should probably - * support a user defined maximum capacity to limit the overall risk. - * - * If too many instances are seen, the strategy should begin to return `null` - * (and log an appropriate warning). - * - * The additional complexity created by this approach really tells us that - * types which require interning in order to be used as aggregation keys - * should be considered unsuitable, and callers should seek alternatives. - * - * @param name The name of this strategy, used for debugging purposes. - * - * @see - * Original Java code for historical context. - */ -public actual abstract class LogPerBucketingStrategy protected constructor( - private val name: String -) { - /** - * Maps a log aggregation key from a potentially unbounded set of - * key values to a bounded set of instances. - * - * Implementations of this method should be efficient and avoid - * allocating memory wherever possible. - * - * The returned value must be an immutable identifier with minimal additional - * allocation requirements and ideally have singleton semantics - * (e.g., an [Enum] or [Integer] value). - * - * **Warning**: If keys are not known to have natural singleton semantics - * (e.g. [String]) then returning the given key instance is generally a bad idea. - * - * Even if the set of key values is small, the set of distinct allocated instances - * passed to [LoggingApi.per] can be unbounded, and that's what matters. - * - * As such, it is always better to map keys to some singleton identifier or - * intern the keys in some way. - * - * @param key A non-null key from a potentially unbounded set of log aggregation keys. - * @return an immutable value from some known bounded set, which will be held persistently by - * internal Flogger data structures as part of the log aggregation feature. If - * `null` is returned, the corresponding call to `per(key, STRATEGY)` has no effect. - */ - protected abstract fun apply(key: T): Any? - - internal fun doApply(key: T): Any? = apply(key) - - /** - * Access to the [apply] method for testing purposes. - * - * This method is not part of the public API and should not be used by client code. - */ - @TestOnly - internal fun applyForTesting(key: T): Any? = apply(key) - - override fun toString(): String = - "${LogPerBucketingStrategy::class.java.simpleName}[$name]" - - public companion object { - - /** - * A strategy to use only if the set of log aggregation keys is known to be - * a strictly bounded set of instances with singleton semantics. - */ - private val KNOWN_BOUNDED = object : LogPerBucketingStrategy("KnownBounded") { - override fun apply(key: Any): Any = key - } - - /** - * This is a "safe" strategy as far as memory use is concerned since class objects - * are effectively singletons. - */ - private val BY_CLASS = object : LogPerBucketingStrategy("ByClass") { - override fun apply(key: Any): Any = key.javaClass - } - - /** - * This is a "safe" strategy as far as memory use is concerned, because a class object - * returns the same string instance every time its called, and class objects - * are effectively singletons. - */ - private val BY_CLASS_NAME = object : LogPerBucketingStrategy("ByClassName") { - override fun apply(key: Any): Any = key.javaClass.name /* This is a naturally interned - value, so no need to call `intern()`. */ - } - - /** - * A strategy to use only if the set of log aggregation keys is known to be - * a strictly bounded set of instances with singleton semantics. - * - * **WARNING**: When using this strategy, keys passed to [LoggingApi.per] - * are used as-is by the log aggregation code, and held indefinitely by internal - * static data structures. - * - * As such it is vital that key instances used with this strategy have singleton semantics - * (i.e., if `k1.equals(k2)` then `k1 == k2`). - * - * Failure to adhere to this requirement is likely to result in hard to detect memory leaks. - * - * If keys do not have singleton semantics then you should use a different strategy, - * such as [byHashCode] or [byClass]. - */ - @JvmStatic - public fun knownBounded(): LogPerBucketingStrategy = KNOWN_BOUNDED - - /** - * A strategy which uses the [Class] of the given key for log aggregation. - * - * This is useful when you need to aggregate over specific exceptions or similar - * type-distinguished instances. - * - * Note that using this strategy will result in a reference to the [Class] object of - * the key being retained indefinitely. - * - * This will prevent class unloading from occurring for affected classes, and - * it is up to the caller to decide if this is acceptable or not. - */ - @JvmStatic - public fun byClass(): LogPerBucketingStrategy = BY_CLASS - - /** - * A strategy which uses the [Class] name of the given key for log aggregation. - * - * This is useful when you need to aggregate over specific exceptions or similar - * type-distinguished instances. - * - * This is an alternative strategy to [byClass] which avoids holding onto the class - * instance and avoids any issues with class unloading. - * - * However, it may conflate classes if applications use complex arrangements of custom - * class-loaders, but this should be extremely rare. - */ - @JvmStatic - public fun byClassName(): LogPerBucketingStrategy = BY_CLASS_NAME - - /** - * A strategy defined for some given set of known keys. - * - * Unlike [knownBounded], this strategy maps keys to a bounded set of identifiers, and - * permits the use of non-singleton keys in [LoggingApi.per]. - * - * If keys outside this set are used this strategy returns `null`, and - * log aggregation will not occur. - * - * Duplicates in [knownKeys] are ignored. - */ - @JvmStatic - public fun forKnownKeys(knownKeys: Iterable): LogPerBucketingStrategy { - val keyMap = HashMap() - val name = buildString { - append("ForKnownKeys(") - var index = 0 - for (key in knownKeys) { - if (!keyMap.containsKey(key)) { - if (index > 0) { - append(", ") - } - append(key) - keyMap[key] = index - index++ - } - } - append(')') - } - // We check here to avoid querying `knownKeys` size just for the precondition check. - require(!keyMap.isEmpty()) { "`knownKeys` must not be empty." } - return object : LogPerBucketingStrategy(name) { - override fun apply(key: Any): Any? = keyMap[key] - } - } - - /** - * A strategy which uses the [hashCode] of a given key, modulo [maxBuckets], for - * log aggregation. - * - * This is a fallback strategy for cases where the set of possible values is - * not known in advance or could be arbitrarily large in unusual circumstances. - * - * When using this method, it is obviously important that the [hashCode] method of - * the expected keys is well distributed, since duplicate hash codes, or hash codes - * congruent to [maxBuckets] will cause keys to be conflated. - * - * The caller is responsible for deciding the number of unique log aggregation keys this - * strategy can return. This choice is a trade-off between memory usage and the risk of - * conflating keys when performing log aggregation. - * - * Each log site using this strategy will hold up to [maxBuckets] distinct versions of log - * site information to allow rate limiting and other stateful operations to be applied - * separately per bucket. - * - * The overall allocation cost depends on the type of rate limiting used alongside this - * method, but it scales linearly with [maxBuckets]. - * - * It is recommended to keep the value of [maxBuckets] below 250, since this - * guarantees no additional allocations will occur when using this strategy. - * However, the value chosen should be as small as practically possible for - * the typical expected number of unique keys. - * - * To avoid unwanted allocation at log sites, users are strongly encouraged to assign the - * returned value to a static field and pass that to any log statements which need it. - */ - @JvmStatic - public fun byHashCode(maxBuckets: Int): LogPerBucketingStrategy { - require(maxBuckets > 0) { "`maxBuckets` must be positive: $maxBuckets." } - return object : LogPerBucketingStrategy("ByHashCode($maxBuckets)") { - @Suppress("MagicNumber") - override fun apply(key: Any): Any = - Math.floorMod(key.hashCode(), maxBuckets) - 128 - } - } - } -} diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/LogSiteStackTrace.kt b/logging/src/jvmMain/kotlin/io/spine/logging/LogSiteStackTrace.kt index 0a9b5b9fe..8212cff35 100644 --- a/logging/src/jvmMain/kotlin/io/spine/logging/LogSiteStackTrace.kt +++ b/logging/src/jvmMain/kotlin/io/spine/logging/LogSiteStackTrace.kt @@ -39,7 +39,7 @@ import java.io.Serial * Original Java code for historical context. */ @Suppress("ExceptionClassNameDoesntEndWithException") -public actual class LogSiteStackTrace( +public actual class LogSiteStackTrace private constructor( cause: Throwable?, stackSize: StackSize, syntheticStackTrace: Array @@ -62,8 +62,17 @@ public actual class LogSiteStackTrace( @Suppress("NonSynchronizedMethodOverridesSynchronizedMethod") override fun fillInStackTrace(): Throwable = this - public companion object { + public actual companion object { + @Serial private const val serialVersionUID: Long = 0L + + public actual fun create( + cause: Throwable?, + stackSize: StackSize, + syntheticStackTrace: Array + ): LogSiteStackTrace { + return LogSiteStackTrace(cause, stackSize, syntheticStackTrace) + } } } diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/LoggingFactory.kt b/logging/src/jvmMain/kotlin/io/spine/logging/LoggingFactory.kt index 5ef55aa60..17b8c14d7 100644 --- a/logging/src/jvmMain/kotlin/io/spine/logging/LoggingFactory.kt +++ b/logging/src/jvmMain/kotlin/io/spine/logging/LoggingFactory.kt @@ -72,10 +72,10 @@ public actual object LoggingFactory: ClassValue() { repeatedMetadataKey(label, type.kotlin) private fun createForClass(cls: Class<*>): JvmLogger { - val floggerBackend = Platform.getBackend(cls.name) - val flogger = Middleman(floggerBackend) + val loggerBackend = Platform.getBackend(cls.name) + val loggerImpl = Middleman(loggerBackend) // As for now, `JvmLogger` just delegates actual work to Flogger. - return JvmLogger(cls.kotlin, flogger) + return JvmLogger(cls.kotlin, loggerImpl) } @JvmStatic diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/LoggingScope.kt b/logging/src/jvmMain/kotlin/io/spine/logging/LoggingScope.kt deleted file mode 100644 index 492dafe52..000000000 --- a/logging/src/jvmMain/kotlin/io/spine/logging/LoggingScope.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2020, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging - -import io.spine.annotation.VisibleForTesting - -/** - * JVM implementation of the logging scope which provides garbage collection for the keys - * via JVM week references. - * - * @see - * Original Flogger code for historic reference. - */ -public actual abstract class LoggingScope protected constructor(private val label: String) { - - protected actual abstract fun specialize(key: LogSiteKey): LogSiteKey - protected actual abstract fun onClose(removalHook: () -> Unit) - - /** - * Opens [specialize] for the package. - */ - internal fun doSpecialize(key: LogSiteKey): LogSiteKey = specialize(key) - - /** - * Opens access to [onClose] for the package. - */ - internal fun doOnClose(removalHook: () -> Unit) = onClose(removalHook) - - /** - * Returns the [label] of this scope. - */ - override fun toString(): String = label - - public actual companion object { - - /** - * Creates a scope which automatically removes any associated keys - * from [LogSiteMap]s when it is garbage collected. - * - * The given label is used only for debugging purposes and may appear in log - * statements, it should not contain any user data or other runtime information. - */ - @JvmStatic - public actual fun create(label: String): LoggingScope = WeakScope(label) - } - - @VisibleForTesting - internal class WeakScope(label: String) : LoggingScope(label) { - - /** - * Do NOT reference the Scope directly from a specialized key, use the "key part" - * to avoid the key part weak reference is enqueued which triggers tidy up at the next - * call to `specializeForScopesIn()` where scopes are used. - * - * This must be unique per scope since it acts as a qualifier within specialized - * log site keys. Using a different weak reference per specialized key would not work - * (which is part of the reason we also need the "on close" queue as well as - * the reference queue). - */ - private val keyPart: KeyPart = KeyPart(this) - - override fun specialize(key: LogSiteKey): LogSiteKey = - SpecializedLogSiteKey.of(key, keyPart) - - override fun onClose(removalHook: () -> Unit) { - // Clear the reference queue about as often as we would add a new key to a map. - // This should still mean that the queue is almost always empty when we check - // it (since we expect more than one specialized log site key per scope) and it - // avoids spamming the queue clearance loop for every log statement and avoids - // class loading the reference queue until we know scopes have been used. - KeyPart.removeUnusedKeys() - keyPart.addOnCloseHook(removalHook) - } - - internal fun closeForTesting() { - keyPart.close() - } - } -} diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/StackTraceElement.jvm.kt b/logging/src/jvmMain/kotlin/io/spine/logging/StackTraceElementJvm.kt similarity index 84% rename from logging/src/jvmMain/kotlin/io/spine/logging/StackTraceElement.jvm.kt rename to logging/src/jvmMain/kotlin/io/spine/logging/StackTraceElementJvm.kt index f19f185ba..37d158082 100644 --- a/logging/src/jvmMain/kotlin/io/spine/logging/StackTraceElement.jvm.kt +++ b/logging/src/jvmMain/kotlin/io/spine/logging/StackTraceElementJvm.kt @@ -26,4 +26,13 @@ package io.spine.logging +import io.spine.reflect.CallerFinder +import kotlin.reflect.KClass + public actual typealias StackTraceElement = java.lang.StackTraceElement + +public actual fun stackForCallerOf( + target: KClass<*>, + maxDepth: Int, + skip: Int +): Array = CallerFinder.stackForCallerOf(target.java, maxDepth, skip) diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/LogSiteLookup.kt b/logging/src/jvmMain/kotlin/io/spine/logging/WeakRef.kt similarity index 57% rename from logging/src/jvmMain/kotlin/io/spine/logging/LogSiteLookup.kt rename to logging/src/jvmMain/kotlin/io/spine/logging/WeakRef.kt index a7d39ade3..1f0513119 100644 --- a/logging/src/jvmMain/kotlin/io/spine/logging/LogSiteLookup.kt +++ b/logging/src/jvmMain/kotlin/io/spine/logging/WeakRef.kt @@ -26,36 +26,19 @@ package io.spine.logging -import io.spine.logging.backend.Platform -import kotlin.reflect.KClass +import java.lang.ref.ReferenceQueue +import java.lang.ref.WeakReference /** - * Determines log sites for the current line of code using - * [LogCallerFinder][io.spine.logging.backend.LogCallerFinder] - * obtained from a [Platform]. + * JVM implementation of the `WeakRef` class. */ -public actual object LogSiteLookup { +internal actual class WeakRef actual constructor(referent: T) { - /** - * Returns a [LogSite] for the caller of the specified class. - * - * If log site determination is unsupported, this method returns - * the [LogSite.Invalid] instance. - */ - public actual fun callerOf(loggingApi: KClass<*>): LogSite { - return Platform.getCallerFinder().findLogSite(loggingApi, 0) - } + private val ref = WeakReference(referent, queue) + + actual fun get(): T? = ref.get() - /** - * Returns a [LogSite] for the current line of code. - * - * If log site determination is unsupported, this method returns - * the [LogSite.Invalid] instance. - */ - public actual fun logSite(): LogSite { - return Platform.getCallerFinder().findLogSite( - LogSiteLookup::class, - 0 - ) + companion object { + internal val queue = ReferenceQueue() } } diff --git a/logging/src/jvmMain/kotlin/io/spine/logging/util/RecursionDepth.kt b/logging/src/jvmMain/kotlin/io/spine/logging/util/RecursionDepth.kt deleted file mode 100644 index e2d3cb590..000000000 --- a/logging/src/jvmMain/kotlin/io/spine/logging/util/RecursionDepth.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2019, The Flogger Authors; 2025, TeamDev. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.logging.util - -import io.spine.annotation.Internal - -/** - * A thread local counter, incremented whenever a log statement is being processed by the backend. - * - * If this value is greater than 1, then reentrant logging has occurred, and some code may behave - * differently to avoid issues such as unbounded recursion. Logging may even be disabled completely - * if the depth gets too high. - * - * #### API Note - * This class is an internal detail and must not be used outside the core of the Logging library. - * Backends which need to know the recursion depth should call - * [io.spine.logging.backend.Platform.getCurrentRecursionDepth]. - * - * @see - * Original Java code for historical context. - */ -public actual class RecursionDepth actual constructor() : AutoCloseable { - - public actual companion object { - - private val holder = object : ThreadLocal() { - override fun initialValue(): RecursionDepth = RecursionDepth() - } - - /** - * Do not call this method directly, use `Platform.getCurrentRecursionDepth()`. - */ - @Internal - @JvmStatic - public actual fun getCurrentDepth(): Int = holder.get().value - - /** - * Do not call this method directly, use `Platform.getCurrentRecursionDepth()`. - */ - @Internal - @JvmStatic - public actual fun enterLogStatement(): RecursionDepth { - val depth = holder.get() - if (++depth.value == 0) { - throw AssertionError( - "Overflow of `RecursionDepth` (possible error in core library)." - ) - } - return depth - } - } - - private var value = 0 - - /** - * Do not call this method directly, use `Platform.getCurrentRecursionDepth()`. - */ - @Internal - public actual fun getValue(): Int = value - - public actual override fun close() { - if (value > 0) { - value -= 1 - return - } - throw AssertionError( - "Mismatched calls to `RecursionDepth` (possible error in core library)." - ) - } -} diff --git a/logging/src/jvmTest/kotlin/io/spine/logging/backend/AnyExtsJvmSpec.kt b/logging/src/jvmTest/kotlin/io/spine/logging/backend/AnyExtsJvmSpec.kt index 3bb2789d1..9f2da7349 100644 --- a/logging/src/jvmTest/kotlin/io/spine/logging/backend/AnyExtsJvmSpec.kt +++ b/logging/src/jvmTest/kotlin/io/spine/logging/backend/AnyExtsJvmSpec.kt @@ -32,7 +32,7 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -@DisplayName("Any? extensions under JVM should") +@DisplayName("`Any?` extensions under JVM should") internal class AnyExtsJvmSpec { @Nested inner class diff --git a/pom.xml b/pom.xml index f3d21719a..4f439c547 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine spine-logging -2.0.0-SNAPSHOT.380 +2.0.0-SNAPSHOT.390 2015 diff --git a/version.gradle.kts b/version.gradle.kts index 490dccc11..5a3f3015a 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.380") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.390")