From a080fc437f2775e7191d98d73b05fe80a384db69 Mon Sep 17 00:00:00 2001 From: Wilberforce Uwadiegwu Date: Mon, 14 Sep 2026 15:55:12 +0800 Subject: [PATCH 1/3] reimplementation of kmp support and bare-bone jvm support --- .gitignore | 2 + gradle/libs.versions.toml | 1 + timber/build.gradle | 3 + .../kotlin/timber/log/DebugTree.android.kt | 81 +++ .../kotlin/timber/log/Platform.android.kt | 28 + .../timber/log/ThreadLocalRef.android.kt | 14 + .../androidMain/kotlin/timber/log/Timber.kt | 486 ------------------ .../commonMain/kotlin/timber/log/DebugTree.kt | 6 + .../commonMain/kotlin/timber/log/Platform.kt | 10 + .../commonMain/kotlin/timber/log/Priority.kt | 11 + .../kotlin/timber/log/ThreadLocalRef.kt | 7 + .../commonMain/kotlin/timber/log/Timber.kt | 329 ++++++++++-- 12 files changed, 458 insertions(+), 520 deletions(-) create mode 100644 timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt create mode 100644 timber/src/androidMain/kotlin/timber/log/Platform.android.kt create mode 100644 timber/src/androidMain/kotlin/timber/log/ThreadLocalRef.android.kt delete mode 100644 timber/src/androidMain/kotlin/timber/log/Timber.kt create mode 100644 timber/src/commonMain/kotlin/timber/log/DebugTree.kt create mode 100644 timber/src/commonMain/kotlin/timber/log/Platform.kt create mode 100644 timber/src/commonMain/kotlin/timber/log/Priority.kt create mode 100644 timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt diff --git a/.gitignore b/.gitignore index 92007a211..9e569f3b4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,9 @@ obj # Gradle .gradle +.kotlin jniLibs build local.properties reports +/gradle/gradle-daemon-jvm.properties diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 47bcbba2f..27436cc35 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ gradlePlugin-dokka = "org.jetbrains.dokka:dokka-gradle-plugin:2.2.0" gradlePlugin-japicmp = "me.champeau.gradle:japicmp-gradle-plugin:0.4.6" gradlePlugin-mavenPublish = "com.vanniktech:gradle-maven-publish-plugin:0.37.0" compatPlugin = "com.gradleup.tapmoc:tapmoc-gradle-plugin:0.4.2" +atomicfu = "org.jetbrains.kotlinx:atomicfu:0.33.0" annotations = "org.jetbrains:annotations:26.1.0" auto-service = { module = "com.google.auto.service:auto-service", version.ref = "autoService" } diff --git a/timber/build.gradle b/timber/build.gradle index 3b78d41e2..182e550b5 100644 --- a/timber/build.gradle +++ b/timber/build.gradle @@ -22,10 +22,13 @@ kotlin { } } + jvm() + sourceSets { commonMain { dependencies { implementation libs.annotations + implementation libs.atomicfu } } commonTest { diff --git a/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt b/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt new file mode 100644 index 000000000..2628bce0b --- /dev/null +++ b/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt @@ -0,0 +1,81 @@ +package timber.log + +import android.os.Build +import android.util.Log +import java.util.regex.Pattern +import kotlin.math.min +import timber.log.Timber.Forest +import timber.log.Timber.Tree + +actual open class DebugTree actual constructor() : Tree() { + private val fqcnIgnore = + listOf( + Timber::class.java.name, + Forest::class.java.name, + Tree::class.java.name, + DebugTree::class.java.name, + ) + + override val tag: String? + get() = + super.tag + ?: Throwable() + .stackTrace + .first { it.className !in fqcnIgnore } + .let(::createStackElementTag) + + /** + * Extract the tag which should be used for the message from the `element`. By default this will + * use the class name without any anonymous class suffixes (e.g., `Foo$1` becomes `Foo`). + * + * Note: This will not be called if a [manual tag][.tag] was specified. + */ + protected open fun createStackElementTag(element: StackTraceElement): String? { + var tag = element.className.substringAfterLast('.') + val m = ANONYMOUS_CLASS.matcher(tag) + if (m.find()) { + tag = m.replaceAll("") + } + // Tag length limit was removed in API 26. + return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) { + tag + } else { + tag.substring(0, MAX_TAG_LENGTH) + } + } + + /** + * Break up `message` into maximum-length chunks (if needed) and send to either + * [Log.println()][Log.println] or [Log.wtf()][Log.wtf] for logging. + * + * {@inheritDoc} + */ + override fun log(priority: Priority, tag: String?, message: String, t: Throwable?) { + if (message.length < MAX_LOG_LENGTH) { + writeLog(priority, tag, message) + return + } + + // Split by line, then ensure each line can fit into Log's maximum length. + var i = 0 + val length = message.length + while (i < length) { + var newline = message.indexOf('\n', i) + newline = if (newline != -1) newline else length + do { + val end = min(newline, i + MAX_LOG_LENGTH) + val part = message.substring(i, end) + writeLog(priority, tag, part) + i = end + } while (i < newline) + i++ + } + } + + companion object { + private const val MAX_LOG_LENGTH = 4000 + private const val MAX_TAG_LENGTH = 23 + private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$") + } + +} diff --git a/timber/src/androidMain/kotlin/timber/log/Platform.android.kt b/timber/src/androidMain/kotlin/timber/log/Platform.android.kt new file mode 100644 index 000000000..f3ecc7df5 --- /dev/null +++ b/timber/src/androidMain/kotlin/timber/log/Platform.android.kt @@ -0,0 +1,28 @@ +package timber.log + +import android.util.Log +import java.io.PrintWriter +import java.io.StringWriter + +internal actual fun writeLog(priority: Priority, tag: String?, message: String) { + when (priority) { + Priority.ASSERT -> Log.wtf(tag, message) + else -> Log.println(priority.toInt(), tag, message) + } +} + +internal actual fun getStackTraceString(t: Throwable): String { + // Don't replace this with Log.getStackTraceString() - it hides + // UnknownHostException, which is not what we want. + val sw = StringWriter(256) + val pw = PrintWriter(sw, false) + t.printStackTrace(pw) + pw.flush() + return sw.toString() +} + +internal actual fun String.format(args: Array) = this.format(*args) + + +// TODO: Write test for this +private fun Priority.toInt(): Int = ordinal + 2 // Logs starts from 2 diff --git a/timber/src/androidMain/kotlin/timber/log/ThreadLocalRef.android.kt b/timber/src/androidMain/kotlin/timber/log/ThreadLocalRef.android.kt new file mode 100644 index 000000000..b75867c22 --- /dev/null +++ b/timber/src/androidMain/kotlin/timber/log/ThreadLocalRef.android.kt @@ -0,0 +1,14 @@ +package timber.log + +internal actual class ThreadLocalRef { + private val delegate = ThreadLocal() + actual fun get(): T? = delegate.get() + + actual fun set(value: T?) { + delegate.set(value) + } + + actual fun remove() { + delegate.remove() + } +} diff --git a/timber/src/androidMain/kotlin/timber/log/Timber.kt b/timber/src/androidMain/kotlin/timber/log/Timber.kt deleted file mode 100644 index 022d91f69..000000000 --- a/timber/src/androidMain/kotlin/timber/log/Timber.kt +++ /dev/null @@ -1,486 +0,0 @@ -package timber.log - -import android.os.Build -import android.util.Log -import java.io.PrintWriter -import java.io.StringWriter -import java.util.ArrayList -import java.util.Collections -import java.util.Collections.unmodifiableList -import java.util.regex.Pattern -import org.jetbrains.annotations.NonNls - -/** Logging for lazy people. */ -actual class Timber actual private constructor() { - init { - throw AssertionError() - } - - /** A facade for handling logging calls. Install instances via [`Timber.plant()`][.plant]. */ - actual abstract class Tree { - @get:JvmSynthetic // Hide from public API. - internal val explicitTag = ThreadLocal() - - @get:JvmSynthetic // Hide from public API. - internal open val tag: String? - get() { - val tag = explicitTag.get() - if (tag != null) { - explicitTag.remove() - } - return tag - } - - /** Log a verbose message with optional format args. */ - actual open fun v(message: String?, vararg args: Any?) { - prepareLog(Log.VERBOSE, null, message, *args) - } - - /** Log a verbose exception and a message with optional format args. */ - actual open fun v(t: Throwable?, message: String?, vararg args: Any?) { - prepareLog(Log.VERBOSE, t, message, *args) - } - - /** Log a verbose exception. */ - actual open fun v(t: Throwable?) { - prepareLog(Log.VERBOSE, t, null) - } - - /** Log a debug message with optional format args. */ - actual open fun d(message: String?, vararg args: Any?) { - prepareLog(Log.DEBUG, null, message, *args) - } - - /** Log a debug exception and a message with optional format args. */ - actual open fun d(t: Throwable?, message: String?, vararg args: Any?) { - prepareLog(Log.DEBUG, t, message, *args) - } - - /** Log a debug exception. */ - actual open fun d(t: Throwable?) { - prepareLog(Log.DEBUG, t, null) - } - - /** Log an info message with optional format args. */ - actual open fun i(message: String?, vararg args: Any?) { - prepareLog(Log.INFO, null, message, *args) - } - - /** Log an info exception and a message with optional format args. */ - actual open fun i(t: Throwable?, message: String?, vararg args: Any?) { - prepareLog(Log.INFO, t, message, *args) - } - - /** Log an info exception. */ - actual open fun i(t: Throwable?) { - prepareLog(Log.INFO, t, null) - } - - /** Log a warning message with optional format args. */ - actual open fun w(message: String?, vararg args: Any?) { - prepareLog(Log.WARN, null, message, *args) - } - - /** Log a warning exception and a message with optional format args. */ - actual open fun w(t: Throwable?, message: String?, vararg args: Any?) { - prepareLog(Log.WARN, t, message, *args) - } - - /** Log a warning exception. */ - actual open fun w(t: Throwable?) { - prepareLog(Log.WARN, t, null) - } - - /** Log an error message with optional format args. */ - actual open fun e(message: String?, vararg args: Any?) { - prepareLog(Log.ERROR, null, message, *args) - } - - /** Log an error exception and a message with optional format args. */ - actual open fun e(t: Throwable?, message: String?, vararg args: Any?) { - prepareLog(Log.ERROR, t, message, *args) - } - - /** Log an error exception. */ - actual open fun e(t: Throwable?) { - prepareLog(Log.ERROR, t, null) - } - - /** Log an assert message with optional format args. */ - actual open fun wtf(message: String?, vararg args: Any?) { - prepareLog(Log.ASSERT, null, message, *args) - } - - /** Log an assert exception and a message with optional format args. */ - actual open fun wtf(t: Throwable?, message: String?, vararg args: Any?) { - prepareLog(Log.ASSERT, t, message, *args) - } - - /** Log an assert exception. */ - actual open fun wtf(t: Throwable?) { - prepareLog(Log.ASSERT, t, null) - } - - /** Log at `priority` a message with optional format args. */ - actual open fun log(priority: Int, message: String?, vararg args: Any?) { - prepareLog(priority, null, message, *args) - } - - /** Log at `priority` an exception and a message with optional format args. */ - actual open fun log(priority: Int, t: Throwable?, message: String?, vararg args: Any?) { - prepareLog(priority, t, message, *args) - } - - /** Log at `priority` an exception. */ - actual open fun log(priority: Int, t: Throwable?) { - prepareLog(priority, t, null) - } - - /** Return whether a message at `priority` should be logged. */ - @Deprecated("Use isLoggable(String, int)", ReplaceWith("this.isLoggable(null, priority)")) - protected open fun isLoggable(priority: Int): Boolean = true - - /** Return whether a message at `priority` or `tag` should be logged. */ - actual protected open fun isLoggable(tag: String?, priority: Int): Boolean = - isLoggable(priority) - - private fun prepareLog(priority: Int, t: Throwable?, message: String?, vararg args: Any?) { - // Consume tag even when message is not loggable so that next message is correctly tagged. - val tag = tag - if (!isLoggable(tag, priority)) { - return - } - - var message = message - if (message.isNullOrEmpty()) { - if (t == null) { - return // Swallow message if it's null and there's no throwable. - } - message = getStackTraceString(t) - } else { - if (args.isNotEmpty()) { - message = formatMessage(message, args) - } - if (t != null) { - message += "\n" + getStackTraceString(t) - } - } - - log(priority, tag, message, t) - } - - /** Formats a log message with optional arguments. */ - actual protected open fun formatMessage(message: String, args: Array): String = - message.format(*args) - - private fun getStackTraceString(t: Throwable): String { - // Don't replace this with Log.getStackTraceString() - it hides - // UnknownHostException, which is not what we want. - val sw = StringWriter(256) - val pw = PrintWriter(sw, false) - t.printStackTrace(pw) - pw.flush() - return sw.toString() - } - - /** - * Write a log message to its destination. Called for all level-specific methods by default. - * - * @param priority Log level. See [Log] for constants. - * @param tag Explicit or inferred tag. May be `null`. - * @param message Formatted log message. - * @param t Accompanying exceptions. May be `null`. - */ - actual protected abstract fun log(priority: Int, tag: String?, message: String, t: Throwable?) - } - - /** A [Tree] for debug builds. Automatically infers the tag from the calling class. */ - open class DebugTree : Tree() { - private val fqcnIgnore = - listOf( - Timber::class.java.name, - Forest::class.java.name, - Tree::class.java.name, - DebugTree::class.java.name, - ) - - override val tag: String? - get() = - super.tag - ?: Throwable() - .stackTrace - .first { it.className !in fqcnIgnore } - .let(::createStackElementTag) - - /** - * Extract the tag which should be used for the message from the `element`. By default this will - * use the class name without any anonymous class suffixes (e.g., `Foo$1` becomes `Foo`). - * - * Note: This will not be called if a [manual tag][.tag] was specified. - */ - protected open fun createStackElementTag(element: StackTraceElement): String? { - var tag = element.className.substringAfterLast('.') - val m = ANONYMOUS_CLASS.matcher(tag) - if (m.find()) { - tag = m.replaceAll("") - } - // Tag length limit was removed in API 26. - return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) { - tag - } else { - tag.substring(0, MAX_TAG_LENGTH) - } - } - - /** - * Break up `message` into maximum-length chunks (if needed) and send to either - * [Log.println()][Log.println] or [Log.wtf()][Log.wtf] for logging. - * - * {@inheritDoc} - */ - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - if (message.length < MAX_LOG_LENGTH) { - if (priority == Log.ASSERT) { - Log.wtf(tag, message) - } else { - Log.println(priority, tag, message) - } - return - } - - // Split by line, then ensure each line can fit into Log's maximum length. - var i = 0 - val length = message.length - while (i < length) { - var newline = message.indexOf('\n', i) - newline = if (newline != -1) newline else length - do { - val end = Math.min(newline, i + MAX_LOG_LENGTH) - val part = message.substring(i, end) - if (priority == Log.ASSERT) { - Log.wtf(tag, part) - } else { - Log.println(priority, tag, part) - } - i = end - } while (i < newline) - i++ - } - } - - companion object { - private const val MAX_LOG_LENGTH = 4000 - private const val MAX_TAG_LENGTH = 23 - private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$") - } - } - - actual companion object Forest : Tree() { - /** Log a verbose message with optional format args. */ - @JvmStatic - override fun v(@NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.v(message, *args) } - } - - /** Log a verbose exception and a message with optional format args. */ - @JvmStatic - override fun v(t: Throwable?, @NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.v(t, message, *args) } - } - - /** Log a verbose exception. */ - @JvmStatic - override fun v(t: Throwable?) { - treeArray.forEach { it.v(t) } - } - - /** Log a debug message with optional format args. */ - @JvmStatic - override fun d(@NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.d(message, *args) } - } - - /** Log a debug exception and a message with optional format args. */ - @JvmStatic - override fun d(t: Throwable?, @NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.d(t, message, *args) } - } - - /** Log a debug exception. */ - @JvmStatic - override fun d(t: Throwable?) { - treeArray.forEach { it.d(t) } - } - - /** Log an info message with optional format args. */ - @JvmStatic - override fun i(@NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.i(message, *args) } - } - - /** Log an info exception and a message with optional format args. */ - @JvmStatic - override fun i(t: Throwable?, @NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.i(t, message, *args) } - } - - /** Log an info exception. */ - @JvmStatic - override fun i(t: Throwable?) { - treeArray.forEach { it.i(t) } - } - - /** Log a warning message with optional format args. */ - @JvmStatic - override fun w(@NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.w(message, *args) } - } - - /** Log a warning exception and a message with optional format args. */ - @JvmStatic - override fun w(t: Throwable?, @NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.w(t, message, *args) } - } - - /** Log a warning exception. */ - @JvmStatic - override fun w(t: Throwable?) { - treeArray.forEach { it.w(t) } - } - - /** Log an error message with optional format args. */ - @JvmStatic - override fun e(@NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.e(message, *args) } - } - - /** Log an error exception and a message with optional format args. */ - @JvmStatic - override fun e(t: Throwable?, @NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.e(t, message, *args) } - } - - /** Log an error exception. */ - @JvmStatic - override fun e(t: Throwable?) { - treeArray.forEach { it.e(t) } - } - - /** Log an assert message with optional format args. */ - @JvmStatic - override fun wtf(@NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.wtf(message, *args) } - } - - /** Log an assert exception and a message with optional format args. */ - @JvmStatic - override fun wtf(t: Throwable?, @NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.wtf(t, message, *args) } - } - - /** Log an assert exception. */ - @JvmStatic - override fun wtf(t: Throwable?) { - treeArray.forEach { it.wtf(t) } - } - - /** Log at `priority` a message with optional format args. */ - @JvmStatic - override fun log(priority: Int, @NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.log(priority, message, *args) } - } - - /** Log at `priority` an exception and a message with optional format args. */ - @JvmStatic - override fun log(priority: Int, t: Throwable?, @NonNls message: String?, vararg args: Any?) { - treeArray.forEach { it.log(priority, t, message, *args) } - } - - /** Log at `priority` an exception. */ - @JvmStatic - override fun log(priority: Int, t: Throwable?) { - treeArray.forEach { it.log(priority, t) } - } - - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - throw AssertionError() // Missing override for log method. - } - - /** - * A view into Timber's planted trees as a tree itself. This can be used for injecting a logger - * instance rather than using static methods or to facilitate testing. - */ - @Suppress( - "NOTHING_TO_INLINE", // Kotlin users should reference `Tree.Forest` directly. - "NON_FINAL_MEMBER_IN_OBJECT", // For japicmp check. - ) - @JvmStatic - open inline fun asTree(): Tree = this - - /** Set a one-time tag for use on the next logging call. */ - @JvmStatic - actual fun tag(tag: String): Tree { - for (tree in treeArray) { - tree.explicitTag.set(tag) - } - return this - } - - /** Add a new logging tree. */ - @JvmStatic - actual fun plant(tree: Tree) { - require(tree !== this) { "Cannot plant Timber into itself." } - synchronized(trees) { - trees.add(tree) - treeArray = trees.toTypedArray() - } - } - - /** Adds new logging trees. */ - @JvmStatic - actual fun plant(vararg trees: Tree) { - for (tree in trees) { - requireNotNull(tree) { "trees contained null" } - require(tree !== this) { "Cannot plant Timber into itself." } - } - synchronized(this.trees) { - Collections.addAll(this.trees, *trees) - treeArray = this.trees.toTypedArray() - } - } - - /** Remove a planted tree. */ - @JvmStatic - actual fun uproot(tree: Tree) { - synchronized(trees) { - require(trees.remove(tree)) { "Cannot uproot tree which is not planted: $tree" } - treeArray = trees.toTypedArray() - } - } - - /** Remove all planted trees. */ - @JvmStatic - actual fun uprootAll() { - synchronized(trees) { - trees.clear() - treeArray = emptyArray() - } - } - - /** Return a copy of all planted [trees][Tree]. */ - @JvmStatic - actual fun forest(): List { - synchronized(trees) { - return unmodifiableList(trees.toList()) - } - } - - @get:[JvmStatic JvmName("treeCount")] - actual val treeCount - get() = treeArray.size - - // Both fields guarded by 'trees'. - private val trees = ArrayList() - @Volatile private var treeArray = emptyArray() - } -} diff --git a/timber/src/commonMain/kotlin/timber/log/DebugTree.kt b/timber/src/commonMain/kotlin/timber/log/DebugTree.kt new file mode 100644 index 000000000..27f6ad974 --- /dev/null +++ b/timber/src/commonMain/kotlin/timber/log/DebugTree.kt @@ -0,0 +1,6 @@ +package timber.log + +import timber.log.Timber.Tree + +/** A [Tree] for debug builds. Automatically infers the tag from the calling class. */ // TODO: Update docs +expect open class DebugTree(): Tree diff --git a/timber/src/commonMain/kotlin/timber/log/Platform.kt b/timber/src/commonMain/kotlin/timber/log/Platform.kt new file mode 100644 index 000000000..19f2f4ab4 --- /dev/null +++ b/timber/src/commonMain/kotlin/timber/log/Platform.kt @@ -0,0 +1,10 @@ +@file:JvmName("Platform") +package timber.log + +import kotlin.jvm.JvmName + +internal expect fun writeLog(priority: Priority, tag: String?, message: String) + +internal expect fun getStackTraceString(t: Throwable): String + +internal expect fun String.format(args: Array): String diff --git a/timber/src/commonMain/kotlin/timber/log/Priority.kt b/timber/src/commonMain/kotlin/timber/log/Priority.kt new file mode 100644 index 000000000..f33b6e045 --- /dev/null +++ b/timber/src/commonMain/kotlin/timber/log/Priority.kt @@ -0,0 +1,11 @@ +package timber.log + +// TODO: Doc +enum class Priority { + VERBOSE, + DEBUG, + INFO, + WARN, + ERROR, + ASSERT +} diff --git a/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt b/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt new file mode 100644 index 000000000..f4ac445d2 --- /dev/null +++ b/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt @@ -0,0 +1,7 @@ +package timber.log +// TODO: Doc +internal expect class ThreadLocalRef() { + fun get(): T? + fun set(value: T?) + fun remove() +} diff --git a/timber/src/commonMain/kotlin/timber/log/Timber.kt b/timber/src/commonMain/kotlin/timber/log/Timber.kt index 176188f02..b06bf311d 100644 --- a/timber/src/commonMain/kotlin/timber/log/Timber.kt +++ b/timber/src/commonMain/kotlin/timber/log/Timber.kt @@ -1,111 +1,372 @@ package timber.log + +import kotlin.concurrent.Volatile +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic +import kotlin.jvm.JvmSynthetic +import kotlinx.atomicfu.locks.SynchronizedObject +import kotlinx.atomicfu.locks.synchronized +import org.jetbrains.annotations.NonNls + /** Logging for lazy people. */ -expect class Timber private constructor() { +class Timber private constructor() { + + init { + throw AssertionError() + } /** A facade for handling logging calls. Install instances via [`Timber.plant()`][.plant]. */ abstract class Tree { + @get:JvmSynthetic // Hide from public API. + internal val explicitTag = ThreadLocalRef() + + @get:JvmSynthetic // Hide from public API. + internal open val tag: String? + get() { + val tag = explicitTag.get() + if (tag != null) { + explicitTag.remove() + } + return tag + } + /** Log a verbose message with optional format args. */ - open fun v(message: String?, vararg args: Any?) + open fun v(message: String?, vararg args: Any?) { + prepareLog(Priority.VERBOSE, null, message, *args) + } /** Log a verbose exception and a message with optional format args. */ - open fun v(t: Throwable?, message: String?, vararg args: Any?) + open fun v(t: Throwable?, message: String?, vararg args: Any?) { + prepareLog(Priority.VERBOSE, t, message, *args) + } /** Log a verbose exception. */ - open fun v(t: Throwable?) + open fun v(t: Throwable?) { + prepareLog(Priority.VERBOSE, t, null) + } /** Log a debug message with optional format args. */ - open fun d(message: String?, vararg args: Any?) + open fun d(message: String?, vararg args: Any?) { + prepareLog(Priority.DEBUG, null, message, *args) + } /** Log a debug exception and a message with optional format args. */ - open fun d(t: Throwable?, message: String?, vararg args: Any?) + open fun d(t: Throwable?, message: String?, vararg args: Any?) { + prepareLog(Priority.DEBUG, t, message, *args) + } /** Log a debug exception. */ - open fun d(t: Throwable?) + open fun d(t: Throwable?) { + prepareLog(Priority.DEBUG, t, null) + } /** Log an info message with optional format args. */ - open fun i(message: String?, vararg args: Any?) + open fun i(message: String?, vararg args: Any?) { + prepareLog(Priority.INFO, null, message, *args) + } /** Log an info exception and a message with optional format args. */ - open fun i(t: Throwable?, message: String?, vararg args: Any?) + open fun i(t: Throwable?, message: String?, vararg args: Any?) { + prepareLog(Priority.INFO, t, message, *args) + } /** Log an info exception. */ - open fun i(t: Throwable?) + open fun i(t: Throwable?) { + prepareLog(Priority.INFO, t, null) + } /** Log a warning message with optional format args. */ - open fun w(message: String?, vararg args: Any?) + open fun w(message: String?, vararg args: Any?) { + prepareLog(Priority.WARN, null, message, *args) + } /** Log a warning exception and a message with optional format args. */ - open fun w(t: Throwable?, message: String?, vararg args: Any?) + open fun w(t: Throwable?, message: String?, vararg args: Any?) { + prepareLog(Priority.WARN, t, message, *args) + } /** Log a warning exception. */ - open fun w(t: Throwable?) + open fun w(t: Throwable?) { + prepareLog(Priority.WARN, t, null) + } /** Log an error message with optional format args. */ - open fun e(message: String?, vararg args: Any?) + open fun e(message: String?, vararg args: Any?) { + prepareLog(Priority.ERROR, null, message, *args) + } /** Log an error exception and a message with optional format args. */ - open fun e(t: Throwable?, message: String?, vararg args: Any?) + open fun e(t: Throwable?, message: String?, vararg args: Any?) { + prepareLog(Priority.ERROR, t, message, *args) + } /** Log an error exception. */ - open fun e(t: Throwable?) + open fun e(t: Throwable?) { + prepareLog(Priority.ERROR, t, null) + } /** Log an assert message with optional format args. */ - open fun wtf(message: String?, vararg args: Any?) + open fun wtf(message: String?, vararg args: Any?) { + prepareLog(Priority.ASSERT, null, message, *args) + } /** Log an assert exception and a message with optional format args. */ - open fun wtf(t: Throwable?, message: String?, vararg args: Any?) + open fun wtf(t: Throwable?, message: String?, vararg args: Any?) { + prepareLog(Priority.ASSERT, t, message, *args) + } /** Log an assert exception. */ - open fun wtf(t: Throwable?) + open fun wtf(t: Throwable?) { + prepareLog(Priority.ASSERT, t, null) + } /** Log at `priority` a message with optional format args. */ - open fun log(priority: Int, message: String?, vararg args: Any?) + open fun log(priority: Priority, message: String?, vararg args: Any?) { + prepareLog(priority, null, message, *args) + } /** Log at `priority` an exception and a message with optional format args. */ - open fun log(priority: Int, t: Throwable?, message: String?, vararg args: Any?) + open fun log(priority: Priority, t: Throwable?, message: String?, vararg args: Any?) { + prepareLog(priority, t, message, *args) + } /** Log at `priority` an exception. */ - open fun log(priority: Int, t: Throwable?) + open fun log(priority: Priority, t: Throwable?) { + prepareLog(priority, t, null) + } + + /** Return whether a message at `priority` should be logged. */ + @Deprecated("Use isLoggable(String, int)", ReplaceWith("this.isLoggable(null, priority)")) + protected open fun isLoggable(priority: Priority) = true /** Return whether a message at `priority` or `tag` should be logged. */ - protected open fun isLoggable(tag: String?, priority: Int): Boolean + protected open fun isLoggable(tag: String?, priority: Priority) = isLoggable(priority) + + private fun prepareLog(priority: Priority, t: Throwable?, message: String?, vararg args: Any?) { + // Consume tag even when message is not loggable so that next message is correctly tagged. + val tag = tag + if (!isLoggable(tag, priority)) { + return + } + + var message = message + if (message.isNullOrEmpty()) { + if (t == null) { + return // Swallow message if it's null and there's no throwable. + } + message = getStackTraceString(t) + } else { + if (args.isNotEmpty()) { + message = formatMessage(message, args) + } + if (t != null) { + message += "\n" + getStackTraceString(t) + } + } + + log(priority, tag, message, t) + } /** Formats a log message with optional arguments. */ - protected open fun formatMessage(message: String, args: Array): String + protected open fun formatMessage(message: String, args: Array) = message.format(args) + /** * Write a log message to its destination. Called for all level-specific methods by default. * - * @param priority Log level. See [Log] for constants. + * @param priority Log level. See [Priority] for constants. * @param tag Explicit or inferred tag. May be `null`. * @param message Formatted log message. * @param t Accompanying exceptions. May be `null`. */ - protected abstract fun log(priority: Int, tag: String?, message: String, t: Throwable?) + protected abstract fun log(priority: Priority, tag: String?, message: String, t: Throwable?) } - companion object Forest : Tree { + companion object Forest : Tree() { + /** Log a verbose message with optional format args. */ + @JvmStatic override fun v(@NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.v(message, *args) } + } + + /** Log a verbose exception and a message with optional format args. */ + @JvmStatic override fun v(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.v(t, message, *args) } + } + + /** Log a verbose exception. */ + @JvmStatic override fun v(t: Throwable?) { + treeArray.forEach { it.v(t) } + } + + /** Log a debug message with optional format args. */ + @JvmStatic override fun d(@NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.d(message, *args) } + } + + /** Log a debug exception and a message with optional format args. */ + @JvmStatic override fun d(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.d(t, message, *args) } + } + + /** Log a debug exception. */ + @JvmStatic override fun d(t: Throwable?) { + treeArray.forEach { it.d(t) } + } + + /** Log an info message with optional format args. */ + @JvmStatic override fun i(@NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.i(message, *args) } + } + + /** Log an info exception and a message with optional format args. */ + @JvmStatic override fun i(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.i(t, message, *args) } + } + + /** Log an info exception. */ + @JvmStatic override fun i(t: Throwable?) { + treeArray.forEach { it.i(t) } + } + + /** Log a warning message with optional format args. */ + @JvmStatic override fun w(@NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.w(message, *args) } + } + + /** Log a warning exception and a message with optional format args. */ + @JvmStatic override fun w(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.w(t, message, *args) } + } + + /** Log a warning exception. */ + @JvmStatic override fun w(t: Throwable?) { + treeArray.forEach { it.w(t) } + } + + /** Log an error message with optional format args. */ + @JvmStatic override fun e(@NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.e(message, *args) } + } + + /** Log an error exception and a message with optional format args. */ + @JvmStatic override fun e(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.e(t, message, *args) } + } + + /** Log an error exception. */ + @JvmStatic override fun e(t: Throwable?) { + treeArray.forEach { it.e(t) } + } + + /** Log an assert message with optional format args. */ + @JvmStatic override fun wtf(@NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.wtf(message, *args) } + } + + /** Log an assert exception and a message with optional format args. */ + @JvmStatic override fun wtf(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.wtf(t, message, *args) } + } + + /** Log an assert exception. */ + @JvmStatic override fun wtf(t: Throwable?) { + treeArray.forEach { it.wtf(t) } + } + + /** Log at `priority` a message with optional format args. */ + @JvmStatic override fun log(priority: Priority, @NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.log(priority, message, *args) } + } + + /** Log at `priority` an exception and a message with optional format args. */ + @JvmStatic + override fun log(priority: Priority, t: Throwable?, @NonNls message: String?, vararg args: Any?) { + treeArray.forEach { it.log(priority, t, message, *args) } + } + + /** Log at `priority` an exception. */ + @JvmStatic override fun log(priority: Priority, t: Throwable?) { + treeArray.forEach { it.log(priority, t) } + } + + override fun log(priority: Priority, tag: String?, message: String, t: Throwable?) { + throw AssertionError() // Missing override for log method. + } + + /** + * A view into Timber's planted trees as a tree itself. This can be used for injecting a logger + * instance rather than using static methods or to facilitate testing. + */ + @Suppress( + "NOTHING_TO_INLINE", // Kotlin users should reference `Tree.Forest` directly. + "NON_FINAL_MEMBER_IN_OBJECT" // For japicmp check. + ) + @JvmStatic + open inline fun asTree(): Tree = this /** Set a one-time tag for use on the next logging call. */ - fun tag(tag: String): Tree + @JvmStatic fun tag(tag: String): Tree { + for (tree in treeArray) { + tree.explicitTag.set(tag) + } + return this + } /** Add a new logging tree. */ - fun plant(tree: Tree) + @JvmStatic fun plant(tree: Tree) { + require(tree !== this) { "Cannot plant Timber into itself." } + synchronized(lock) { + trees.add(tree) + treeArray = trees.toTypedArray() + } + } /** Adds new logging trees. */ - fun plant(vararg trees: Tree) + @JvmStatic fun plant(vararg trees: Tree) { + for (tree in trees) { + requireNotNull(tree) { "trees contained null" } + require(tree !== this) { "Cannot plant Timber into itself." } + } + synchronized(lock) { + this.trees.addAll(trees) + treeArray = this.trees.toTypedArray() + } + } /** Remove a planted tree. */ - fun uproot(tree: Tree) + @JvmStatic fun uproot(tree: Tree) { + synchronized(lock) { + require(trees.remove(tree)) { "Cannot uproot tree which is not planted: $tree" } + treeArray = trees.toTypedArray() + } + } /** Remove all planted trees. */ - fun uprootAll() + @JvmStatic + fun uprootAll() { + synchronized(lock) { + trees.clear() + treeArray = emptyArray() + } + } /** Return a copy of all planted [trees][Tree]. */ - fun forest(): List + @JvmStatic fun forest(): List { + synchronized(lock) { + return trees.toList() + } + } + + @get:[JvmStatic JvmName("treeCount")] + val treeCount get() = treeArray.size + + private val lock = SynchronizedObject() - val treeCount: Int + // Both fields guarded by 'lock'. + private val trees = ArrayList() + @Volatile + private var treeArray = emptyArray() } } From a6850d3916902606173009470150d53e430543ab Mon Sep 17 00:00:00 2001 From: Wilberforce Uwadiegwu Date: Mon, 14 Sep 2026 17:17:22 +0800 Subject: [PATCH 2/3] more complete jvm support --- gradle/libs.versions.toml | 1 - timber/build.gradle | 8 +- .../kotlin/timber/log/DebugTree.android.kt | 71 ++--------------- .../kotlin/timber/log/Platform.android.kt | 22 +----- .../commonMain/kotlin/timber/log/DebugTree.kt | 4 +- .../commonMain/kotlin/timber/log/Platform.kt | 2 +- .../commonMain/kotlin/timber/log/Priority.kt | 34 +++++++-- .../kotlin/timber/log/ThreadLocalRef.kt | 5 +- .../commonMain/kotlin/timber/log/Timber.kt | 52 ++++++------- .../kotlin/timber/log/DebugTree.kt | 76 +++++++++++++++++++ .../kotlin/timber/log/Platform.jvmAndroid.kt | 16 ++++ .../timber/log/ThreadLocalRef.jvmAndroid.kt} | 2 +- .../kotlin/timber/log/DebugTree.jvm.kt | 3 + .../jvmMain/kotlin/timber/log/Platform.jvm.kt | 14 ++++ 14 files changed, 182 insertions(+), 128 deletions(-) create mode 100644 timber/src/jvmAndroidMain/kotlin/timber/log/DebugTree.kt create mode 100644 timber/src/jvmAndroidMain/kotlin/timber/log/Platform.jvmAndroid.kt rename timber/src/{androidMain/kotlin/timber/log/ThreadLocalRef.android.kt => jvmAndroidMain/kotlin/timber/log/ThreadLocalRef.jvmAndroid.kt} (77%) create mode 100644 timber/src/jvmMain/kotlin/timber/log/DebugTree.jvm.kt create mode 100644 timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 27436cc35..47bcbba2f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,6 @@ gradlePlugin-dokka = "org.jetbrains.dokka:dokka-gradle-plugin:2.2.0" gradlePlugin-japicmp = "me.champeau.gradle:japicmp-gradle-plugin:0.4.6" gradlePlugin-mavenPublish = "com.vanniktech:gradle-maven-publish-plugin:0.37.0" compatPlugin = "com.gradleup.tapmoc:tapmoc-gradle-plugin:0.4.2" -atomicfu = "org.jetbrains.kotlinx:atomicfu:0.33.0" annotations = "org.jetbrains:annotations:26.1.0" auto-service = { module = "com.google.auto.service:auto-service", version.ref = "autoService" } diff --git a/timber/build.gradle b/timber/build.gradle index 182e550b5..44013fb4a 100644 --- a/timber/build.gradle +++ b/timber/build.gradle @@ -28,9 +28,11 @@ kotlin { commonMain { dependencies { implementation libs.annotations - implementation libs.atomicfu } } + jvmAndroidMain { dependsOn(commonMain) } + androidMain { dependsOn(jvmAndroidMain) } + jvmMain { dependsOn(jvmAndroidMain) } commonTest { dependencies { implementation libs.annotations @@ -39,6 +41,10 @@ kotlin { implementation libs.robolectric } } + + jvmAndroidTest { dependsOn(commonTest) } + androidUnitTest { dependsOn(jvmAndroidTest) } + jvmTest { dependsOn(jvmAndroidTest) } } abiValidation { diff --git a/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt b/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt index 2628bce0b..24ec1781f 100644 --- a/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt +++ b/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt @@ -1,81 +1,22 @@ package timber.log import android.os.Build -import android.util.Log -import java.util.regex.Pattern -import kotlin.math.min -import timber.log.Timber.Forest -import timber.log.Timber.Tree -actual open class DebugTree actual constructor() : Tree() { - private val fqcnIgnore = - listOf( - Timber::class.java.name, - Forest::class.java.name, - Tree::class.java.name, - DebugTree::class.java.name, - ) +actual open class DebugTree actual constructor() : JvmAndroidDebugTree() { - override val tag: String? - get() = - super.tag - ?: Throwable() - .stackTrace - .first { it.className !in fqcnIgnore } - .let(::createStackElementTag) + override fun maxLogLength(): Int = MAX_LOG_LENGTH - /** - * Extract the tag which should be used for the message from the `element`. By default this will - * use the class name without any anonymous class suffixes (e.g., `Foo$1` becomes `Foo`). - * - * Note: This will not be called if a [manual tag][.tag] was specified. - */ - protected open fun createStackElementTag(element: StackTraceElement): String? { - var tag = element.className.substringAfterLast('.') - val m = ANONYMOUS_CLASS.matcher(tag) - if (m.find()) { - tag = m.replaceAll("") - } + override fun maxTagLength(): Int { // Tag length limit was removed in API 26. - return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) { - tag + return if (Build.VERSION.SDK_INT >= 26) { + Int.MAX_VALUE } else { - tag.substring(0, MAX_TAG_LENGTH) - } - } - - /** - * Break up `message` into maximum-length chunks (if needed) and send to either - * [Log.println()][Log.println] or [Log.wtf()][Log.wtf] for logging. - * - * {@inheritDoc} - */ - override fun log(priority: Priority, tag: String?, message: String, t: Throwable?) { - if (message.length < MAX_LOG_LENGTH) { - writeLog(priority, tag, message) - return - } - - // Split by line, then ensure each line can fit into Log's maximum length. - var i = 0 - val length = message.length - while (i < length) { - var newline = message.indexOf('\n', i) - newline = if (newline != -1) newline else length - do { - val end = min(newline, i + MAX_LOG_LENGTH) - val part = message.substring(i, end) - writeLog(priority, tag, part) - i = end - } while (i < newline) - i++ + MAX_TAG_LENGTH } } companion object { private const val MAX_LOG_LENGTH = 4000 private const val MAX_TAG_LENGTH = 23 - private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$") } - } diff --git a/timber/src/androidMain/kotlin/timber/log/Platform.android.kt b/timber/src/androidMain/kotlin/timber/log/Platform.android.kt index f3ecc7df5..1967b6077 100644 --- a/timber/src/androidMain/kotlin/timber/log/Platform.android.kt +++ b/timber/src/androidMain/kotlin/timber/log/Platform.android.kt @@ -1,28 +1,10 @@ package timber.log import android.util.Log -import java.io.PrintWriter -import java.io.StringWriter -internal actual fun writeLog(priority: Priority, tag: String?, message: String) { +internal actual fun writeLog(priority: Int, tag: String?, message: String) { when (priority) { Priority.ASSERT -> Log.wtf(tag, message) - else -> Log.println(priority.toInt(), tag, message) + else -> Log.println(priority, tag, message) } } - -internal actual fun getStackTraceString(t: Throwable): String { - // Don't replace this with Log.getStackTraceString() - it hides - // UnknownHostException, which is not what we want. - val sw = StringWriter(256) - val pw = PrintWriter(sw, false) - t.printStackTrace(pw) - pw.flush() - return sw.toString() -} - -internal actual fun String.format(args: Array) = this.format(*args) - - -// TODO: Write test for this -private fun Priority.toInt(): Int = ordinal + 2 // Logs starts from 2 diff --git a/timber/src/commonMain/kotlin/timber/log/DebugTree.kt b/timber/src/commonMain/kotlin/timber/log/DebugTree.kt index 27f6ad974..b3a1c9df4 100644 --- a/timber/src/commonMain/kotlin/timber/log/DebugTree.kt +++ b/timber/src/commonMain/kotlin/timber/log/DebugTree.kt @@ -2,5 +2,5 @@ package timber.log import timber.log.Timber.Tree -/** A [Tree] for debug builds. Automatically infers the tag from the calling class. */ // TODO: Update docs -expect open class DebugTree(): Tree +/** A [Tree] for debug builds. Automatically infers the tag from the calling class. */ +expect class DebugTree(): Tree diff --git a/timber/src/commonMain/kotlin/timber/log/Platform.kt b/timber/src/commonMain/kotlin/timber/log/Platform.kt index 19f2f4ab4..fa5e6d697 100644 --- a/timber/src/commonMain/kotlin/timber/log/Platform.kt +++ b/timber/src/commonMain/kotlin/timber/log/Platform.kt @@ -3,7 +3,7 @@ package timber.log import kotlin.jvm.JvmName -internal expect fun writeLog(priority: Priority, tag: String?, message: String) +internal expect fun writeLog(priority: Int, tag: String?, message: String) internal expect fun getStackTraceString(t: Throwable): String diff --git a/timber/src/commonMain/kotlin/timber/log/Priority.kt b/timber/src/commonMain/kotlin/timber/log/Priority.kt index f33b6e045..674d4f050 100644 --- a/timber/src/commonMain/kotlin/timber/log/Priority.kt +++ b/timber/src/commonMain/kotlin/timber/log/Priority.kt @@ -1,11 +1,29 @@ package timber.log -// TODO: Doc -enum class Priority { - VERBOSE, - DEBUG, - INFO, - WARN, - ERROR, - ASSERT +/** + * Log levels. + * Note: this is a direct mapping to android.util.Log values + */ +object Priority { + const val VERBOSE = 2 + const val DEBUG = 3 + const val INFO = 4 + const val WARN = 5 + const val ERROR = 6 + const val ASSERT = 7 + + /** + * @return the string representation of this [priority] + */ + fun name(priority: Int): String { + return when (priority) { + VERBOSE -> "Verbose" + DEBUG -> "Debug" + INFO -> "Info" + WARN -> "Warn" + ERROR -> "Error" + ASSERT -> "Assert" + else -> throw IllegalArgumentException("unsupported priority: $priority") + } + } } diff --git a/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt b/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt index f4ac445d2..75100f810 100644 --- a/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt +++ b/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt @@ -1,5 +1,8 @@ package timber.log -// TODO: Doc + +/** + * A per thread value holder. The equivalent of `java.lang.ThreadLocal` on JVM compatible platforms. + */ internal expect class ThreadLocalRef() { fun get(): T? fun set(value: T?) diff --git a/timber/src/commonMain/kotlin/timber/log/Timber.kt b/timber/src/commonMain/kotlin/timber/log/Timber.kt index b06bf311d..a6e28c4e0 100644 --- a/timber/src/commonMain/kotlin/timber/log/Timber.kt +++ b/timber/src/commonMain/kotlin/timber/log/Timber.kt @@ -1,12 +1,12 @@ package timber.log +import java.util.Collections import kotlin.concurrent.Volatile +import java.util.Collections.unmodifiableList import kotlin.jvm.JvmName import kotlin.jvm.JvmStatic import kotlin.jvm.JvmSynthetic -import kotlinx.atomicfu.locks.SynchronizedObject -import kotlinx.atomicfu.locks.synchronized import org.jetbrains.annotations.NonNls /** Logging for lazy people. */ @@ -20,7 +20,7 @@ class Timber private constructor() { abstract class Tree { @get:JvmSynthetic // Hide from public API. - internal val explicitTag = ThreadLocalRef() + internal val explicitTag = ThreadLocalRef() // TODO: Have a test cover this thread safety @get:JvmSynthetic // Hide from public API. internal open val tag: String? @@ -123,28 +123,28 @@ class Timber private constructor() { } /** Log at `priority` a message with optional format args. */ - open fun log(priority: Priority, message: String?, vararg args: Any?) { + open fun log(priority: Int, message: String?, vararg args: Any?) { prepareLog(priority, null, message, *args) } /** Log at `priority` an exception and a message with optional format args. */ - open fun log(priority: Priority, t: Throwable?, message: String?, vararg args: Any?) { + open fun log(priority: Int, t: Throwable?, message: String?, vararg args: Any?) { prepareLog(priority, t, message, *args) } /** Log at `priority` an exception. */ - open fun log(priority: Priority, t: Throwable?) { + open fun log(priority: Int, t: Throwable?) { prepareLog(priority, t, null) } /** Return whether a message at `priority` should be logged. */ @Deprecated("Use isLoggable(String, int)", ReplaceWith("this.isLoggable(null, priority)")) - protected open fun isLoggable(priority: Priority) = true + protected open fun isLoggable(priority: Int) = true /** Return whether a message at `priority` or `tag` should be logged. */ - protected open fun isLoggable(tag: String?, priority: Priority) = isLoggable(priority) + protected open fun isLoggable(tag: String?, priority: Int) = isLoggable(priority) - private fun prepareLog(priority: Priority, t: Throwable?, message: String?, vararg args: Any?) { + private fun prepareLog(priority: Int, t: Throwable?, message: String?, vararg args: Any?) { // Consume tag even when message is not loggable so that next message is correctly tagged. val tag = tag if (!isLoggable(tag, priority)) { @@ -181,7 +181,7 @@ class Timber private constructor() { * @param message Formatted log message. * @param t Accompanying exceptions. May be `null`. */ - protected abstract fun log(priority: Priority, tag: String?, message: String, t: Throwable?) + protected abstract fun log(priority: Int, tag: String?, message: String, t: Throwable?) } companion object Forest : Tree() { @@ -276,22 +276,22 @@ class Timber private constructor() { } /** Log at `priority` a message with optional format args. */ - @JvmStatic override fun log(priority: Priority, @NonNls message: String?, vararg args: Any?) { + @JvmStatic override fun log(priority: Int, @NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.log(priority, message, *args) } } /** Log at `priority` an exception and a message with optional format args. */ @JvmStatic - override fun log(priority: Priority, t: Throwable?, @NonNls message: String?, vararg args: Any?) { + override fun log(priority: Int, t: Throwable?, @NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.log(priority, t, message, *args) } } /** Log at `priority` an exception. */ - @JvmStatic override fun log(priority: Priority, t: Throwable?) { + @JvmStatic override fun log(priority: Int, t: Throwable?) { treeArray.forEach { it.log(priority, t) } } - override fun log(priority: Priority, tag: String?, message: String, t: Throwable?) { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { throw AssertionError() // Missing override for log method. } @@ -317,7 +317,7 @@ class Timber private constructor() { /** Add a new logging tree. */ @JvmStatic fun plant(tree: Tree) { require(tree !== this) { "Cannot plant Timber into itself." } - synchronized(lock) { + synchronized(trees) { trees.add(tree) treeArray = trees.toTypedArray() } @@ -329,24 +329,23 @@ class Timber private constructor() { requireNotNull(tree) { "trees contained null" } require(tree !== this) { "Cannot plant Timber into itself." } } - synchronized(lock) { - this.trees.addAll(trees) + synchronized(this.trees) { + Collections.addAll(this.trees, *trees) treeArray = this.trees.toTypedArray() } } /** Remove a planted tree. */ @JvmStatic fun uproot(tree: Tree) { - synchronized(lock) { + synchronized(trees) { require(trees.remove(tree)) { "Cannot uproot tree which is not planted: $tree" } treeArray = trees.toTypedArray() } } /** Remove all planted trees. */ - @JvmStatic - fun uprootAll() { - synchronized(lock) { + @JvmStatic fun uprootAll() { + synchronized(trees) { trees.clear() treeArray = emptyArray() } @@ -354,19 +353,16 @@ class Timber private constructor() { /** Return a copy of all planted [trees][Tree]. */ @JvmStatic fun forest(): List { - synchronized(lock) { - return trees.toList() + synchronized(trees) { + return unmodifiableList(trees.toList()) } } @get:[JvmStatic JvmName("treeCount")] val treeCount get() = treeArray.size - private val lock = SynchronizedObject() - - // Both fields guarded by 'lock'. + // Both fields guarded by 'trees'. private val trees = ArrayList() - @Volatile - private var treeArray = emptyArray() + @Volatile private var treeArray = emptyArray() } } diff --git a/timber/src/jvmAndroidMain/kotlin/timber/log/DebugTree.kt b/timber/src/jvmAndroidMain/kotlin/timber/log/DebugTree.kt new file mode 100644 index 000000000..74f722a64 --- /dev/null +++ b/timber/src/jvmAndroidMain/kotlin/timber/log/DebugTree.kt @@ -0,0 +1,76 @@ +package timber.log + +import java.util.regex.Pattern +import kotlin.math.min +import timber.log.Timber.Forest +import timber.log.Timber.Tree + +open class JvmAndroidDebugTree : Tree() { + private val fqcnIgnore = listOf( + Timber::class.java.name, + Forest::class.java.name, + Tree::class.java.name, + DebugTree::class.java.name, + ) + + override val tag: String? + get() = + super.tag + ?: Throwable() + .stackTrace + .first { it.className !in fqcnIgnore } + .let(::createStackElementTag) + + /** + * Extract the tag which should be used for the message from the `element`. By default this will + * use the class name without any anonymous class suffixes (e.g., `Foo$1` becomes `Foo`). + * + * Note: This will not be called if a [manual tag][.tag] was specified. + */ + protected open fun createStackElementTag(element: StackTraceElement): String? { + var tag = element.className.substringAfterLast('.') + val m = ANONYMOUS_CLASS.matcher(tag) + if (m.find()) { + tag = m.replaceAll("") + } + return when { + maxTagLength() < Int.MAX_VALUE && tag.length > maxTagLength() -> tag.substring(0, maxTagLength()) + else -> tag + } + } + + /** + * Break up `message` into maximum-length chunks (if needed) and send to either for logging. + * + * {@inheritDoc} + */ + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + if (message.length < maxLogLength()) { + writeLog(priority, tag, message) + return + } + + // Split by line, then ensure each line can fit into Log's maximum length. + var i = 0 + val length = message.length + while (i < length) { + var newline = message.indexOf('\n', i) + newline = if (newline != -1) newline else length + do { + val end = min(newline, i + maxLogLength()) + val part = message.substring(i, end) + writeLog(priority, tag, part) + i = end + } while (i < newline) + i++ + } + } + + open fun maxLogLength() = Int.MAX_VALUE + + open fun maxTagLength() = Int.MAX_VALUE + + companion object { + private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$") + } +} diff --git a/timber/src/jvmAndroidMain/kotlin/timber/log/Platform.jvmAndroid.kt b/timber/src/jvmAndroidMain/kotlin/timber/log/Platform.jvmAndroid.kt new file mode 100644 index 000000000..774db494b --- /dev/null +++ b/timber/src/jvmAndroidMain/kotlin/timber/log/Platform.jvmAndroid.kt @@ -0,0 +1,16 @@ +package timber.log + +import java.io.PrintWriter +import java.io.StringWriter + +internal actual fun getStackTraceString(t: Throwable): String { + // Don't replace this with Log.getStackTraceString() - it hides + // UnknownHostException, which is not what we want. + val sw = StringWriter(256) + val pw = PrintWriter(sw, false) + t.printStackTrace(pw) + pw.flush() + return sw.toString() +} + +internal actual fun String.format(args: Array) = this.format(*args) diff --git a/timber/src/androidMain/kotlin/timber/log/ThreadLocalRef.android.kt b/timber/src/jvmAndroidMain/kotlin/timber/log/ThreadLocalRef.jvmAndroid.kt similarity index 77% rename from timber/src/androidMain/kotlin/timber/log/ThreadLocalRef.android.kt rename to timber/src/jvmAndroidMain/kotlin/timber/log/ThreadLocalRef.jvmAndroid.kt index b75867c22..63ce11ead 100644 --- a/timber/src/androidMain/kotlin/timber/log/ThreadLocalRef.android.kt +++ b/timber/src/jvmAndroidMain/kotlin/timber/log/ThreadLocalRef.jvmAndroid.kt @@ -1,6 +1,6 @@ package timber.log -internal actual class ThreadLocalRef { +internal actual class ThreadLocalRef actual constructor() { private val delegate = ThreadLocal() actual fun get(): T? = delegate.get() diff --git a/timber/src/jvmMain/kotlin/timber/log/DebugTree.jvm.kt b/timber/src/jvmMain/kotlin/timber/log/DebugTree.jvm.kt new file mode 100644 index 000000000..23cbf8574 --- /dev/null +++ b/timber/src/jvmMain/kotlin/timber/log/DebugTree.jvm.kt @@ -0,0 +1,3 @@ +package timber.log + +actual open class DebugTree actual constructor() : JvmAndroidDebugTree() diff --git a/timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt b/timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt new file mode 100644 index 000000000..b30a6b9e8 --- /dev/null +++ b/timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt @@ -0,0 +1,14 @@ +package timber.log + +internal actual fun writeLog(priority: Int, tag: String?, message: String) { + val sb = StringBuilder().apply { + append(Priority.name(priority)) + if (!tag.isNullOrBlank()) { + append("/") + append(tag) + } + append(": ") + append(message) + } + println(sb.toString()) +} From 555200e2298196bdc3d9ff99f5d3ba006ea44dec Mon Sep 17 00:00:00 2001 From: Wilberforce Uwadiegwu Date: Mon, 14 Sep 2026 23:47:56 +0800 Subject: [PATCH 3/3] cleanup, readded ignored tests, fixed backward compatibility --- timber/build.gradle | 22 +-- .../java/timber/log/TimberJavaTest.java | 0 .../kotlin}/timber/log/TimberTest.kt | 2 +- .../kotlin/timber/log/DebugTree.android.kt | 22 --- .../kotlin/timber/log/Platform.android.kt | 15 ++ .../commonMain/kotlin/timber/log/DebugTree.kt | 6 - .../commonMain/kotlin/timber/log/Platform.kt | 9 +- .../commonMain/kotlin/timber/log/Priority.kt | 9 +- .../kotlin/timber/log/StackTraceElement.kt | 5 + .../kotlin/timber/log/ThreadLocalRef.kt | 2 + .../commonMain/kotlin/timber/log/Timber.kt | 140 +++++++++++++----- .../kotlin/timber/log/DebugTree.kt | 76 ---------- .../kotlin/timber/log/Platform.jvmAndroid.kt | 16 ++ .../log/StackTraceElement.jvmAndroid.kt | 5 + .../timber/log/ThreadLocalRef.jvmAndroid.kt | 1 + .../kotlin/timber/log/DebugTree.jvm.kt | 3 - .../jvmMain/kotlin/timber/log/Platform.jvm.kt | 19 ++- 17 files changed, 184 insertions(+), 168 deletions(-) rename timber/src/{test => androidHostTest}/java/timber/log/TimberJavaTest.java (100%) rename timber/src/{test/java => androidHostTest/kotlin}/timber/log/TimberTest.kt (99%) delete mode 100644 timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt delete mode 100644 timber/src/commonMain/kotlin/timber/log/DebugTree.kt create mode 100644 timber/src/commonMain/kotlin/timber/log/StackTraceElement.kt delete mode 100644 timber/src/jvmAndroidMain/kotlin/timber/log/DebugTree.kt create mode 100644 timber/src/jvmAndroidMain/kotlin/timber/log/StackTraceElement.jvmAndroid.kt delete mode 100644 timber/src/jvmMain/kotlin/timber/log/DebugTree.jvm.kt diff --git a/timber/build.gradle b/timber/build.gradle index 44013fb4a..0692eb450 100644 --- a/timber/build.gradle +++ b/timber/build.gradle @@ -7,11 +7,15 @@ apply plugin: 'org.jetbrains.dokka' // Must be applied here for publish plugin. apply plugin: 'com.gradleup.tapmoc' kotlin { + applyDefaultHierarchyTemplate() androidLibrary { namespace = 'timber.log' compileSdk = libs.versions.compileSdk.get().toInteger() minSdk = libs.versions.minSdk.get().toInteger() + withHostTest {} + withJava() + optimization { // "it" --> https://issuetracker.google.com/issues/445115242 it.consumerKeepRules.file('consumer-keep-rules.pro') @@ -31,20 +35,20 @@ kotlin { } } jvmAndroidMain { dependsOn(commonMain) } - androidMain { dependsOn(jvmAndroidMain) } - jvmMain { dependsOn(jvmAndroidMain) } + androidMain { dependsOn(jvmAndroidMain) } + jvmMain { dependsOn(jvmAndroidMain) } commonTest { dependencies { implementation libs.annotations - implementation libs.junit implementation libs.assertk + } + } + androidHostTest { + dependencies { + implementation libs.junit implementation libs.robolectric } } - - jvmAndroidTest { dependsOn(commonTest) } - androidUnitTest { dependsOn(jvmAndroidTest) } - jvmTest { dependsOn(jvmAndroidTest) } } abiValidation { @@ -60,8 +64,8 @@ tapmoc { tasks.named('check') { check -> check.dependsOn( - // TODO: https://youtrack.jetbrains.com/issue/KT-78525 - tasks.named('checkLegacyAbi'), + // TODO: https://youtrack.jetbrains.com/issue/KT-78525 + tasks.named('checkLegacyAbi'), ) } diff --git a/timber/src/test/java/timber/log/TimberJavaTest.java b/timber/src/androidHostTest/java/timber/log/TimberJavaTest.java similarity index 100% rename from timber/src/test/java/timber/log/TimberJavaTest.java rename to timber/src/androidHostTest/java/timber/log/TimberJavaTest.java diff --git a/timber/src/test/java/timber/log/TimberTest.kt b/timber/src/androidHostTest/kotlin/timber/log/TimberTest.kt similarity index 99% rename from timber/src/test/java/timber/log/TimberTest.kt rename to timber/src/androidHostTest/kotlin/timber/log/TimberTest.kt index fa671993c..4d9822528 100644 --- a/timber/src/test/java/timber/log/TimberTest.kt +++ b/timber/src/androidHostTest/kotlin/timber/log/TimberTest.kt @@ -51,7 +51,7 @@ class TimberTest { Timber.d("Test") - assertLog().hasDebugMessage("TimberTest:48", "Test").hasNoMoreMessages() + assertLog().hasDebugMessage("TimberTest:52", "Test").hasNoMoreMessages() } @Test diff --git a/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt b/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt deleted file mode 100644 index 24ec1781f..000000000 --- a/timber/src/androidMain/kotlin/timber/log/DebugTree.android.kt +++ /dev/null @@ -1,22 +0,0 @@ -package timber.log - -import android.os.Build - -actual open class DebugTree actual constructor() : JvmAndroidDebugTree() { - - override fun maxLogLength(): Int = MAX_LOG_LENGTH - - override fun maxTagLength(): Int { - // Tag length limit was removed in API 26. - return if (Build.VERSION.SDK_INT >= 26) { - Int.MAX_VALUE - } else { - MAX_TAG_LENGTH - } - } - - companion object { - private const val MAX_LOG_LENGTH = 4000 - private const val MAX_TAG_LENGTH = 23 - } -} diff --git a/timber/src/androidMain/kotlin/timber/log/Platform.android.kt b/timber/src/androidMain/kotlin/timber/log/Platform.android.kt index 1967b6077..526c90797 100644 --- a/timber/src/androidMain/kotlin/timber/log/Platform.android.kt +++ b/timber/src/androidMain/kotlin/timber/log/Platform.android.kt @@ -1,5 +1,6 @@ package timber.log +import android.os.Build import android.util.Log internal actual fun writeLog(priority: Int, tag: String?, message: String) { @@ -8,3 +9,17 @@ internal actual fun writeLog(priority: Int, tag: String?, message: String) { else -> Log.println(priority, tag, message) } } + +internal actual fun maxLogLength(): Int = MAX_LOG_LENGTH + +internal actual fun maxTagLength(): Int { + // Tag length limit was removed in API 26. + return if (Build.VERSION.SDK_INT >= 26) { + Int.MAX_VALUE + } else { + MAX_TAG_LENGTH + } +} + +private const val MAX_LOG_LENGTH = 4000 +private const val MAX_TAG_LENGTH = 23 diff --git a/timber/src/commonMain/kotlin/timber/log/DebugTree.kt b/timber/src/commonMain/kotlin/timber/log/DebugTree.kt deleted file mode 100644 index b3a1c9df4..000000000 --- a/timber/src/commonMain/kotlin/timber/log/DebugTree.kt +++ /dev/null @@ -1,6 +0,0 @@ -package timber.log - -import timber.log.Timber.Tree - -/** A [Tree] for debug builds. Automatically infers the tag from the calling class. */ -expect class DebugTree(): Tree diff --git a/timber/src/commonMain/kotlin/timber/log/Platform.kt b/timber/src/commonMain/kotlin/timber/log/Platform.kt index fa5e6d697..4c80c4f00 100644 --- a/timber/src/commonMain/kotlin/timber/log/Platform.kt +++ b/timber/src/commonMain/kotlin/timber/log/Platform.kt @@ -1,10 +1,13 @@ -@file:JvmName("Platform") package timber.log -import kotlin.jvm.JvmName - internal expect fun writeLog(priority: Int, tag: String?, message: String) internal expect fun getStackTraceString(t: Throwable): String internal expect fun String.format(args: Array): String + +internal expect fun maxLogLength(): Int + +internal expect fun maxTagLength(): Int + +internal expect fun callerStackElement(): StackTraceElement? diff --git a/timber/src/commonMain/kotlin/timber/log/Priority.kt b/timber/src/commonMain/kotlin/timber/log/Priority.kt index 674d4f050..62d75a48b 100644 --- a/timber/src/commonMain/kotlin/timber/log/Priority.kt +++ b/timber/src/commonMain/kotlin/timber/log/Priority.kt @@ -1,9 +1,6 @@ package timber.log -/** - * Log levels. - * Note: this is a direct mapping to android.util.Log values - */ +/** Log levels. Note: this is a direct mapping to android.util.Log values */ object Priority { const val VERBOSE = 2 const val DEBUG = 3 @@ -12,9 +9,7 @@ object Priority { const val ERROR = 6 const val ASSERT = 7 - /** - * @return the string representation of this [priority] - */ + /** @return the string representation of this [priority] */ fun name(priority: Int): String { return when (priority) { VERBOSE -> "Verbose" diff --git a/timber/src/commonMain/kotlin/timber/log/StackTraceElement.kt b/timber/src/commonMain/kotlin/timber/log/StackTraceElement.kt new file mode 100644 index 000000000..0a16a46b8 --- /dev/null +++ b/timber/src/commonMain/kotlin/timber/log/StackTraceElement.kt @@ -0,0 +1,5 @@ +package timber.log + +expect class StackTraceElement + +internal expect fun StackTraceElement.className(): String diff --git a/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt b/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt index 75100f810..49223beb3 100644 --- a/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt +++ b/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt @@ -5,6 +5,8 @@ package timber.log */ internal expect class ThreadLocalRef() { fun get(): T? + fun set(value: T?) + fun remove() } diff --git a/timber/src/commonMain/kotlin/timber/log/Timber.kt b/timber/src/commonMain/kotlin/timber/log/Timber.kt index a6e28c4e0..a4da9afb3 100644 --- a/timber/src/commonMain/kotlin/timber/log/Timber.kt +++ b/timber/src/commonMain/kotlin/timber/log/Timber.kt @@ -1,12 +1,12 @@ package timber.log - import java.util.Collections -import kotlin.concurrent.Volatile import java.util.Collections.unmodifiableList +import kotlin.concurrent.Volatile import kotlin.jvm.JvmName import kotlin.jvm.JvmStatic import kotlin.jvm.JvmSynthetic +import kotlin.math.min import org.jetbrains.annotations.NonNls /** Logging for lazy people. */ @@ -20,7 +20,7 @@ class Timber private constructor() { abstract class Tree { @get:JvmSynthetic // Hide from public API. - internal val explicitTag = ThreadLocalRef() // TODO: Have a test cover this thread safety + internal val explicitTag = ThreadLocalRef() @get:JvmSynthetic // Hide from public API. internal open val tag: String? @@ -154,7 +154,7 @@ class Timber private constructor() { var message = message if (message.isNullOrEmpty()) { if (t == null) { - return // Swallow message if it's null and there's no throwable. + return // Swallow message if it's null and there's no throwable. } message = getStackTraceString(t) } else { @@ -172,7 +172,6 @@ class Timber private constructor() { /** Formats a log message with optional arguments. */ protected open fun formatMessage(message: String, args: Array) = message.format(args) - /** * Write a log message to its destination. Called for all level-specific methods by default. * @@ -184,99 +183,166 @@ class Timber private constructor() { protected abstract fun log(priority: Int, tag: String?, message: String, t: Throwable?) } + /** A [Tree] for debug builds. Automatically infers the tag from the calling class. */ + open class DebugTree : Tree() { + + override val tag: String? + get() = super.tag ?: callerStackElement()?.let(::createStackElementTag) + + /** + * Extract the tag which should be used for the message from the `element`. By default this will + * use the class name without any anonymous class suffixes (e.g., `Foo$1` becomes `Foo`). + * + * Note: This will not be called if a [manual tag][tag] was specified. + */ + protected open fun createStackElementTag(element: StackTraceElement): String? { + val tag = + element.className().substringAfterLast(".").let { + ANONYMOUS_CLASS.replace(it, "") + } + return if (tag.length <= maxTagLength()) tag else tag.substring(0, maxTagLength()) + } + + /** Break up `message` into maximum-length chunks (if needed) and send for logging. */ + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + if (message.length < maxLogLength()) { + writeLog(priority, tag, message) + return + } + + // Split by line, then ensure each line can fit into the platform's maximum length. + var i = 0 + val length = message.length + while (i < length) { + var newline = message.indexOf('\n', i) + newline = if (newline != -1) newline else length + do { + val end = min(newline, i + maxLogLength()) + val part = message.substring(i, end) + writeLog(priority, tag, part) + i = end + } while (i < newline) + i++ + } + } + + companion object { + private val ANONYMOUS_CLASS = Regex("(\\\$\\d+)+$") + } + } + companion object Forest : Tree() { /** Log a verbose message with optional format args. */ - @JvmStatic override fun v(@NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun v(@NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.v(message, *args) } } /** Log a verbose exception and a message with optional format args. */ - @JvmStatic override fun v(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun v(t: Throwable?, @NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.v(t, message, *args) } } /** Log a verbose exception. */ - @JvmStatic override fun v(t: Throwable?) { + @JvmStatic + override fun v(t: Throwable?) { treeArray.forEach { it.v(t) } } /** Log a debug message with optional format args. */ - @JvmStatic override fun d(@NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun d(@NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.d(message, *args) } } /** Log a debug exception and a message with optional format args. */ - @JvmStatic override fun d(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun d(t: Throwable?, @NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.d(t, message, *args) } } /** Log a debug exception. */ - @JvmStatic override fun d(t: Throwable?) { + @JvmStatic + override fun d(t: Throwable?) { treeArray.forEach { it.d(t) } } /** Log an info message with optional format args. */ - @JvmStatic override fun i(@NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun i(@NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.i(message, *args) } } /** Log an info exception and a message with optional format args. */ - @JvmStatic override fun i(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun i(t: Throwable?, @NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.i(t, message, *args) } } /** Log an info exception. */ - @JvmStatic override fun i(t: Throwable?) { + @JvmStatic + override fun i(t: Throwable?) { treeArray.forEach { it.i(t) } } /** Log a warning message with optional format args. */ - @JvmStatic override fun w(@NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun w(@NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.w(message, *args) } } /** Log a warning exception and a message with optional format args. */ - @JvmStatic override fun w(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun w(t: Throwable?, @NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.w(t, message, *args) } } /** Log a warning exception. */ - @JvmStatic override fun w(t: Throwable?) { + @JvmStatic + override fun w(t: Throwable?) { treeArray.forEach { it.w(t) } } /** Log an error message with optional format args. */ - @JvmStatic override fun e(@NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun e(@NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.e(message, *args) } } /** Log an error exception and a message with optional format args. */ - @JvmStatic override fun e(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun e(t: Throwable?, @NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.e(t, message, *args) } } /** Log an error exception. */ - @JvmStatic override fun e(t: Throwable?) { + @JvmStatic + override fun e(t: Throwable?) { treeArray.forEach { it.e(t) } } /** Log an assert message with optional format args. */ - @JvmStatic override fun wtf(@NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun wtf(@NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.wtf(message, *args) } } /** Log an assert exception and a message with optional format args. */ - @JvmStatic override fun wtf(t: Throwable?, @NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun wtf(t: Throwable?, @NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.wtf(t, message, *args) } } /** Log an assert exception. */ - @JvmStatic override fun wtf(t: Throwable?) { + @JvmStatic + override fun wtf(t: Throwable?) { treeArray.forEach { it.wtf(t) } } /** Log at `priority` a message with optional format args. */ - @JvmStatic override fun log(priority: Int, @NonNls message: String?, vararg args: Any?) { + @JvmStatic + override fun log(priority: Int, @NonNls message: String?, vararg args: Any?) { treeArray.forEach { it.log(priority, message, *args) } } @@ -287,7 +353,8 @@ class Timber private constructor() { } /** Log at `priority` an exception. */ - @JvmStatic override fun log(priority: Int, t: Throwable?) { + @JvmStatic + override fun log(priority: Int, t: Throwable?) { treeArray.forEach { it.log(priority, t) } } @@ -301,13 +368,14 @@ class Timber private constructor() { */ @Suppress( "NOTHING_TO_INLINE", // Kotlin users should reference `Tree.Forest` directly. - "NON_FINAL_MEMBER_IN_OBJECT" // For japicmp check. + "NON_FINAL_MEMBER_IN_OBJECT", // For japicmp check. ) @JvmStatic open inline fun asTree(): Tree = this /** Set a one-time tag for use on the next logging call. */ - @JvmStatic fun tag(tag: String): Tree { + @JvmStatic + fun tag(tag: String): Tree { for (tree in treeArray) { tree.explicitTag.set(tag) } @@ -315,7 +383,8 @@ class Timber private constructor() { } /** Add a new logging tree. */ - @JvmStatic fun plant(tree: Tree) { + @JvmStatic + fun plant(tree: Tree) { require(tree !== this) { "Cannot plant Timber into itself." } synchronized(trees) { trees.add(tree) @@ -324,7 +393,8 @@ class Timber private constructor() { } /** Adds new logging trees. */ - @JvmStatic fun plant(vararg trees: Tree) { + @JvmStatic + fun plant(vararg trees: Tree) { for (tree in trees) { requireNotNull(tree) { "trees contained null" } require(tree !== this) { "Cannot plant Timber into itself." } @@ -336,7 +406,8 @@ class Timber private constructor() { } /** Remove a planted tree. */ - @JvmStatic fun uproot(tree: Tree) { + @JvmStatic + fun uproot(tree: Tree) { synchronized(trees) { require(trees.remove(tree)) { "Cannot uproot tree which is not planted: $tree" } treeArray = trees.toTypedArray() @@ -344,7 +415,8 @@ class Timber private constructor() { } /** Remove all planted trees. */ - @JvmStatic fun uprootAll() { + @JvmStatic + fun uprootAll() { synchronized(trees) { trees.clear() treeArray = emptyArray() @@ -352,14 +424,16 @@ class Timber private constructor() { } /** Return a copy of all planted [trees][Tree]. */ - @JvmStatic fun forest(): List { + @JvmStatic + fun forest(): List { synchronized(trees) { return unmodifiableList(trees.toList()) } } @get:[JvmStatic JvmName("treeCount")] - val treeCount get() = treeArray.size + val treeCount + get() = treeArray.size // Both fields guarded by 'trees'. private val trees = ArrayList() diff --git a/timber/src/jvmAndroidMain/kotlin/timber/log/DebugTree.kt b/timber/src/jvmAndroidMain/kotlin/timber/log/DebugTree.kt deleted file mode 100644 index 74f722a64..000000000 --- a/timber/src/jvmAndroidMain/kotlin/timber/log/DebugTree.kt +++ /dev/null @@ -1,76 +0,0 @@ -package timber.log - -import java.util.regex.Pattern -import kotlin.math.min -import timber.log.Timber.Forest -import timber.log.Timber.Tree - -open class JvmAndroidDebugTree : Tree() { - private val fqcnIgnore = listOf( - Timber::class.java.name, - Forest::class.java.name, - Tree::class.java.name, - DebugTree::class.java.name, - ) - - override val tag: String? - get() = - super.tag - ?: Throwable() - .stackTrace - .first { it.className !in fqcnIgnore } - .let(::createStackElementTag) - - /** - * Extract the tag which should be used for the message from the `element`. By default this will - * use the class name without any anonymous class suffixes (e.g., `Foo$1` becomes `Foo`). - * - * Note: This will not be called if a [manual tag][.tag] was specified. - */ - protected open fun createStackElementTag(element: StackTraceElement): String? { - var tag = element.className.substringAfterLast('.') - val m = ANONYMOUS_CLASS.matcher(tag) - if (m.find()) { - tag = m.replaceAll("") - } - return when { - maxTagLength() < Int.MAX_VALUE && tag.length > maxTagLength() -> tag.substring(0, maxTagLength()) - else -> tag - } - } - - /** - * Break up `message` into maximum-length chunks (if needed) and send to either for logging. - * - * {@inheritDoc} - */ - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - if (message.length < maxLogLength()) { - writeLog(priority, tag, message) - return - } - - // Split by line, then ensure each line can fit into Log's maximum length. - var i = 0 - val length = message.length - while (i < length) { - var newline = message.indexOf('\n', i) - newline = if (newline != -1) newline else length - do { - val end = min(newline, i + maxLogLength()) - val part = message.substring(i, end) - writeLog(priority, tag, part) - i = end - } while (i < newline) - i++ - } - } - - open fun maxLogLength() = Int.MAX_VALUE - - open fun maxTagLength() = Int.MAX_VALUE - - companion object { - private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$") - } -} diff --git a/timber/src/jvmAndroidMain/kotlin/timber/log/Platform.jvmAndroid.kt b/timber/src/jvmAndroidMain/kotlin/timber/log/Platform.jvmAndroid.kt index 774db494b..c0bed348f 100644 --- a/timber/src/jvmAndroidMain/kotlin/timber/log/Platform.jvmAndroid.kt +++ b/timber/src/jvmAndroidMain/kotlin/timber/log/Platform.jvmAndroid.kt @@ -2,6 +2,8 @@ package timber.log import java.io.PrintWriter import java.io.StringWriter +import timber.log.Timber.Forest +import timber.log.Timber.Tree internal actual fun getStackTraceString(t: Throwable): String { // Don't replace this with Log.getStackTraceString() - it hides @@ -14,3 +16,17 @@ internal actual fun getStackTraceString(t: Throwable): String { } internal actual fun String.format(args: Array) = this.format(*args) + +internal actual fun callerStackElement(): StackTraceElement? { + val st = Throwable().stackTrace + val thisClass = st.firstOrNull()?.className() + return st.firstOrNull { it.className() != thisClass && it.className !in fqcnIgnore } +} + +private val fqcnIgnore = + listOf( + Timber::class.java.name, + Forest::class.java.name, + Tree::class.java.name, + Timber.DebugTree::class.java.name, + ) diff --git a/timber/src/jvmAndroidMain/kotlin/timber/log/StackTraceElement.jvmAndroid.kt b/timber/src/jvmAndroidMain/kotlin/timber/log/StackTraceElement.jvmAndroid.kt new file mode 100644 index 000000000..22ce229ce --- /dev/null +++ b/timber/src/jvmAndroidMain/kotlin/timber/log/StackTraceElement.jvmAndroid.kt @@ -0,0 +1,5 @@ +package timber.log + +actual typealias StackTraceElement = java.lang.StackTraceElement + +internal actual fun StackTraceElement.className(): String = this.className diff --git a/timber/src/jvmAndroidMain/kotlin/timber/log/ThreadLocalRef.jvmAndroid.kt b/timber/src/jvmAndroidMain/kotlin/timber/log/ThreadLocalRef.jvmAndroid.kt index 63ce11ead..68eccd8dc 100644 --- a/timber/src/jvmAndroidMain/kotlin/timber/log/ThreadLocalRef.jvmAndroid.kt +++ b/timber/src/jvmAndroidMain/kotlin/timber/log/ThreadLocalRef.jvmAndroid.kt @@ -2,6 +2,7 @@ package timber.log internal actual class ThreadLocalRef actual constructor() { private val delegate = ThreadLocal() + actual fun get(): T? = delegate.get() actual fun set(value: T?) { diff --git a/timber/src/jvmMain/kotlin/timber/log/DebugTree.jvm.kt b/timber/src/jvmMain/kotlin/timber/log/DebugTree.jvm.kt deleted file mode 100644 index 23cbf8574..000000000 --- a/timber/src/jvmMain/kotlin/timber/log/DebugTree.jvm.kt +++ /dev/null @@ -1,3 +0,0 @@ -package timber.log - -actual open class DebugTree actual constructor() : JvmAndroidDebugTree() diff --git a/timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt b/timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt index b30a6b9e8..1dfc1e809 100644 --- a/timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt +++ b/timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt @@ -1,14 +1,17 @@ package timber.log internal actual fun writeLog(priority: Int, tag: String?, message: String) { - val sb = StringBuilder().apply { + val line = buildString { append(Priority.name(priority)) - if (!tag.isNullOrBlank()) { - append("/") - append(tag) - } - append(": ") - append(message) + if (!tag.isNullOrBlank()) append("/").append(tag) + append(": ").append(message) + } + when { + priority >= Priority.WARN -> System.err.println(line) + else -> println(line) } - println(sb.toString()) } + +internal actual fun maxLogLength() = Int.MAX_VALUE + +internal actual fun maxTagLength() = Int.MAX_VALUE