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/timber/build.gradle b/timber/build.gradle index 3b78d41e2..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') @@ -22,17 +26,26 @@ kotlin { } } + jvm() + sourceSets { commonMain { dependencies { implementation libs.annotations } } + jvmAndroidMain { dependsOn(commonMain) } + 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 } } @@ -51,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/Platform.android.kt b/timber/src/androidMain/kotlin/timber/log/Platform.android.kt new file mode 100644 index 000000000..526c90797 --- /dev/null +++ b/timber/src/androidMain/kotlin/timber/log/Platform.android.kt @@ -0,0 +1,25 @@ +package timber.log + +import android.os.Build +import android.util.Log + +internal actual fun writeLog(priority: Int, tag: String?, message: String) { + when (priority) { + Priority.ASSERT -> Log.wtf(tag, message) + 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/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/Platform.kt b/timber/src/commonMain/kotlin/timber/log/Platform.kt new file mode 100644 index 000000000..4c80c4f00 --- /dev/null +++ b/timber/src/commonMain/kotlin/timber/log/Platform.kt @@ -0,0 +1,13 @@ +package timber.log + +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 new file mode 100644 index 000000000..62d75a48b --- /dev/null +++ b/timber/src/commonMain/kotlin/timber/log/Priority.kt @@ -0,0 +1,24 @@ +package timber.log + +/** 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/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 new file mode 100644 index 000000000..49223beb3 --- /dev/null +++ b/timber/src/commonMain/kotlin/timber/log/ThreadLocalRef.kt @@ -0,0 +1,12 @@ +package timber.log + +/** + * 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?) + + fun remove() +} diff --git a/timber/src/commonMain/kotlin/timber/log/Timber.kt b/timber/src/commonMain/kotlin/timber/log/Timber.kt index 176188f02..a4da9afb3 100644 --- a/timber/src/commonMain/kotlin/timber/log/Timber.kt +++ b/timber/src/commonMain/kotlin/timber/log/Timber.kt @@ -1,84 +1,181 @@ package timber.log +import java.util.Collections +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. */ -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: 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: Int, 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: Int, 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: Int) = 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: Int) = 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. */ - 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`. @@ -86,26 +183,260 @@ expect class Timber private constructor() { protected abstract fun log(priority: Int, tag: String?, message: String, t: Throwable?) } - companion object Forest : Tree { + /** 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?) { + 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. */ - 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(trees) { + 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(this.trees) { + Collections.addAll(this.trees, *trees) + treeArray = this.trees.toTypedArray() + } + } /** Remove a planted tree. */ - 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() + } + } /** Remove all planted trees. */ - fun uprootAll() + @JvmStatic + fun uprootAll() { + synchronized(trees) { + trees.clear() + treeArray = emptyArray() + } + } /** Return a copy of all planted [trees][Tree]. */ - fun forest(): List - - val treeCount: Int + @JvmStatic + fun forest(): List { + synchronized(trees) { + return unmodifiableList(trees.toList()) + } + } + + @get:[JvmStatic JvmName("treeCount")] + val treeCount + get() = treeArray.size + + // Both fields guarded by 'trees'. + private val trees = ArrayList() + @Volatile private var treeArray = emptyArray() } } 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..c0bed348f --- /dev/null +++ b/timber/src/jvmAndroidMain/kotlin/timber/log/Platform.jvmAndroid.kt @@ -0,0 +1,32 @@ +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 + // 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) + +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 new file mode 100644 index 000000000..68eccd8dc --- /dev/null +++ b/timber/src/jvmAndroidMain/kotlin/timber/log/ThreadLocalRef.jvmAndroid.kt @@ -0,0 +1,15 @@ +package timber.log + +internal actual class ThreadLocalRef actual constructor() { + 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/jvmMain/kotlin/timber/log/Platform.jvm.kt b/timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt new file mode 100644 index 000000000..1dfc1e809 --- /dev/null +++ b/timber/src/jvmMain/kotlin/timber/log/Platform.jvm.kt @@ -0,0 +1,17 @@ +package timber.log + +internal actual fun writeLog(priority: Int, tag: String?, message: String) { + val line = buildString { + append(Priority.name(priority)) + if (!tag.isNullOrBlank()) append("/").append(tag) + append(": ").append(message) + } + when { + priority >= Priority.WARN -> System.err.println(line) + else -> println(line) + } +} + +internal actual fun maxLogLength() = Int.MAX_VALUE + +internal actual fun maxTagLength() = Int.MAX_VALUE