mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #3453 from vitorpamplona/claude/quartz-logging-review-o1l51r
Make Log.sink pluggable for custom logging backends
This commit is contained in:
@@ -42,7 +42,6 @@ class Lud06 {
|
||||
null
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
Log.w("Lud06ToLud16", "Fail to convert LUD06 to LUD16", t)
|
||||
null
|
||||
}
|
||||
@@ -52,7 +51,6 @@ class Lud06 {
|
||||
try {
|
||||
Bech32.decodeBytes(str, false).second.decodeToString()
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
Log.w("Lud06ToLud16", "Fail to convert LUD06 to LUD16", t)
|
||||
null
|
||||
}
|
||||
|
||||
+2
-2
@@ -277,7 +277,7 @@ class OpenTimestamps(
|
||||
"Lite-client verification, assuming block $blockHash is valid",
|
||||
)
|
||||
} catch (e2: Exception) {
|
||||
e2.printStackTrace()
|
||||
// The stack trace propagates with the rethrown exception; the caller reports it.
|
||||
throw e2
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ class OpenTimestamps(
|
||||
subStamp.merge(upgradedStamp)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
Log.w("OpenTimestamps", "Failed to merge upgraded timestamp", e)
|
||||
}
|
||||
|
||||
upgraded = true
|
||||
|
||||
@@ -21,21 +21,31 @@
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
object Log {
|
||||
/** Lines below this severity are dropped before ever reaching the [sink]. */
|
||||
var minLevel: LogLevel = LogLevel.DEBUG
|
||||
|
||||
/**
|
||||
* Where every line that passes [minLevel] is delivered. Replace this to hand
|
||||
* logging over to the consuming application (Timber, SLF4J, a file, a test
|
||||
* buffer, /dev/null). Defaults to [PlatformLogSink] — the historical
|
||||
* per-platform native logger — so behavior is unchanged until a consumer
|
||||
* opts in. See [LogSink].
|
||||
*/
|
||||
var sink: LogSink = PlatformLogSink
|
||||
|
||||
fun d(
|
||||
tag: String,
|
||||
message: String,
|
||||
throwable: Throwable? = null,
|
||||
) {
|
||||
if (minLevel <= LogLevel.DEBUG) PlatformLog.d(tag, message, throwable)
|
||||
if (minLevel <= LogLevel.DEBUG) sink.log(LogLevel.DEBUG, tag, message, throwable)
|
||||
}
|
||||
|
||||
inline fun d(
|
||||
tag: String,
|
||||
message: () -> String,
|
||||
) {
|
||||
if (minLevel <= LogLevel.DEBUG) PlatformLog.d(tag, message())
|
||||
if (minLevel <= LogLevel.DEBUG) sink.log(LogLevel.DEBUG, tag, message(), null)
|
||||
}
|
||||
|
||||
fun i(
|
||||
@@ -43,14 +53,14 @@ object Log {
|
||||
message: String,
|
||||
throwable: Throwable? = null,
|
||||
) {
|
||||
if (minLevel <= LogLevel.INFO) PlatformLog.i(tag, message, throwable)
|
||||
if (minLevel <= LogLevel.INFO) sink.log(LogLevel.INFO, tag, message, throwable)
|
||||
}
|
||||
|
||||
inline fun i(
|
||||
tag: String,
|
||||
message: () -> String,
|
||||
) {
|
||||
if (minLevel <= LogLevel.INFO) PlatformLog.i(tag, message())
|
||||
if (minLevel <= LogLevel.INFO) sink.log(LogLevel.INFO, tag, message(), null)
|
||||
}
|
||||
|
||||
fun w(
|
||||
@@ -58,14 +68,14 @@ object Log {
|
||||
message: String,
|
||||
throwable: Throwable? = null,
|
||||
) {
|
||||
if (minLevel <= LogLevel.WARN) PlatformLog.w(tag, message, throwable)
|
||||
if (minLevel <= LogLevel.WARN) sink.log(LogLevel.WARN, tag, message, throwable)
|
||||
}
|
||||
|
||||
inline fun w(
|
||||
tag: String,
|
||||
message: () -> String,
|
||||
) {
|
||||
if (minLevel <= LogLevel.WARN) PlatformLog.w(tag, message())
|
||||
if (minLevel <= LogLevel.WARN) sink.log(LogLevel.WARN, tag, message(), null)
|
||||
}
|
||||
|
||||
fun e(
|
||||
@@ -73,13 +83,13 @@ object Log {
|
||||
message: String,
|
||||
throwable: Throwable? = null,
|
||||
) {
|
||||
if (minLevel <= LogLevel.ERROR) PlatformLog.e(tag, message, throwable)
|
||||
if (minLevel <= LogLevel.ERROR) sink.log(LogLevel.ERROR, tag, message, throwable)
|
||||
}
|
||||
|
||||
inline fun e(
|
||||
tag: String,
|
||||
message: () -> String,
|
||||
) {
|
||||
if (minLevel <= LogLevel.ERROR) PlatformLog.e(tag, message())
|
||||
if (minLevel <= LogLevel.ERROR) sink.log(LogLevel.ERROR, tag, message(), null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
/**
|
||||
* Destination for every diagnostic line Quartz emits past the [Log.minLevel] gate.
|
||||
*
|
||||
* Quartz owns *what* to log; the consumer of the library owns *where* it goes.
|
||||
* Replace [Log.sink] to route Quartz's diagnostics into your own logging stack
|
||||
* (Timber, SLF4J/Logback, Crashlytics, a file, a test buffer, or nowhere at all):
|
||||
*
|
||||
* ```kotlin
|
||||
* Log.sink = LogSink { level, tag, message, throwable ->
|
||||
* Timber.tag(tag).log(level.toAndroidPriority(), throwable, message)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The default is [PlatformLogSink], which reproduces the historical per-platform
|
||||
* behavior (android.util.Log on Android, System.err on JVM, NSLog on Apple,
|
||||
* println on Linux), so nothing changes unless a consumer opts in.
|
||||
*
|
||||
* The [message] has already passed the [Log.minLevel] filter and any lazy
|
||||
* `() -> String` builder has already been evaluated, so a sink never needs to
|
||||
* re-check the level for allocation reasons.
|
||||
*/
|
||||
fun interface LogSink {
|
||||
fun log(
|
||||
level: LogLevel,
|
||||
tag: String,
|
||||
message: String,
|
||||
throwable: Throwable?,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Default [LogSink] that forwards to the platform's native logger through
|
||||
* [PlatformLog]. Used unless a consumer installs their own [Log.sink].
|
||||
*/
|
||||
object PlatformLogSink : LogSink {
|
||||
override fun log(
|
||||
level: LogLevel,
|
||||
tag: String,
|
||||
message: String,
|
||||
throwable: Throwable?,
|
||||
) {
|
||||
when (level) {
|
||||
LogLevel.DEBUG -> PlatformLog.d(tag, message, throwable)
|
||||
LogLevel.INFO -> PlatformLog.i(tag, message, throwable)
|
||||
LogLevel.WARN -> PlatformLog.w(tag, message, throwable)
|
||||
LogLevel.ERROR -> PlatformLog.e(tag, message, throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertSame
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class LogSinkTest {
|
||||
private data class Line(
|
||||
val level: LogLevel,
|
||||
val tag: String,
|
||||
val message: String,
|
||||
val throwable: Throwable?,
|
||||
)
|
||||
|
||||
private val originalSink = Log.sink
|
||||
private val originalMinLevel = Log.minLevel
|
||||
|
||||
@AfterTest
|
||||
fun restore() {
|
||||
Log.sink = originalSink
|
||||
Log.minLevel = originalMinLevel
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultSinkIsThePlatformLogger() {
|
||||
assertSame(PlatformLogSink, Log.sink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerSinkReceivesEveryLevelWithTagMessageAndThrowable() {
|
||||
val captured = mutableListOf<Line>()
|
||||
Log.minLevel = LogLevel.DEBUG
|
||||
Log.sink = LogSink { level, tag, message, throwable -> captured.add(Line(level, tag, message, throwable)) }
|
||||
|
||||
val boom = RuntimeException("boom")
|
||||
Log.d("A", "d")
|
||||
Log.i("B", "i")
|
||||
Log.w("C", "w")
|
||||
Log.e("D", "e", boom)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
Line(LogLevel.DEBUG, "A", "d", null),
|
||||
Line(LogLevel.INFO, "B", "i", null),
|
||||
Line(LogLevel.WARN, "C", "w", null),
|
||||
Line(LogLevel.ERROR, "D", "e", boom),
|
||||
),
|
||||
captured,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun minLevelGatesBeforeReachingTheSink() {
|
||||
val captured = mutableListOf<Line>()
|
||||
Log.minLevel = LogLevel.WARN
|
||||
Log.sink = LogSink { level, tag, message, throwable -> captured.add(Line(level, tag, message, throwable)) }
|
||||
|
||||
Log.d("A", "dropped")
|
||||
Log.i("B", "dropped")
|
||||
Log.w("C", "kept")
|
||||
Log.e("D", "kept")
|
||||
|
||||
assertEquals(listOf(LogLevel.WARN, LogLevel.ERROR), captured.map { it.level })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun lazyMessageIsNotBuiltWhenGatedOut() {
|
||||
var built = false
|
||||
Log.minLevel = LogLevel.ERROR
|
||||
Log.sink = LogSink { _, _, _, _ -> }
|
||||
|
||||
Log.d("A") {
|
||||
built = true
|
||||
"expensive"
|
||||
}
|
||||
|
||||
assertTrue(!built, "lambda must not run below minLevel")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun lazyMessagePassesNullThrowable() {
|
||||
var captured: Line? = null
|
||||
Log.minLevel = LogLevel.DEBUG
|
||||
Log.sink = LogSink { level, tag, message, throwable -> captured = Line(level, tag, message, throwable) }
|
||||
|
||||
Log.w("T") { "built" }
|
||||
|
||||
assertEquals(LogLevel.WARN, captured?.level)
|
||||
assertEquals("built", captured?.message)
|
||||
assertNull(captured?.throwable)
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -116,7 +117,7 @@ class ElectrumXClient(
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
// Log but don't crash — callers handle null gracefully.
|
||||
e.printStackTrace()
|
||||
Log.w("ElectrumXClient", "Failed to look up $identifier on ${server.host}:${server.port}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user