Skip to content

Commit 3868ab5

Browse files
Claudeclaude
andcommitted
refactor: replace all println/printStackTrace logging with Kermit
Adopt Kermit (co.touchlab:kermit:2.0.8) as the KMP logging framework across the entire codebase, replacing ad-hoc println(), printStackTrace(), and System.err.println() calls with structured, level-filtered logging. - Add kermit dependency to gradle/libs.versions.toml and base/build.gradle.kts (as api() so all downstream modules inherit it) - Replace ~50 println() calls with Logger.withTag("Tag").x { } using appropriate levels (d/i/w/e) - Replace all 5 printStackTrace() calls with log.e(throwable) { } - Add logging to ~15 silent catch blocks (catch (_: Exception)) - Remove manual DEBUG flag in Usb4JavaPN533Transport.kt (Kermit handles log level filtering) - Add CLAUDE.md rule #10 documenting Kermit logging conventions Scope: all production Kotlin code (commonMain, androidMain, iosMain, wasmJsMain, jvmMain). Excluded: tools/mdst/ CLI utilities and test files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 887c958 commit 3868ab5

File tree

52 files changed

+326
-128
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+326
-128
lines changed

CLAUDE.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,39 @@ Do NOT claim work is complete without verification.
112112

113113
When continuing from a previous session, check for implementation plans and session transcripts in `~/.claude/` to recover context rather than starting from scratch.
114114

115+
### 10. Use Kermit for all logging
116+
117+
Use `co.touchlab.kermit.Logger` for all logging. Never use `println()`, `e.printStackTrace()`, `android.util.Log`, `NSLog`, or `console.log` in Kotlin code.
118+
119+
```kotlin
120+
import co.touchlab.kermit.Logger
121+
122+
// Create a tagged logger (use class/module name as tag)
123+
private val log = Logger.withTag("MyClass")
124+
125+
// Log levels (least to most severe): v, d, i, w, e, a
126+
log.d { "Debug message with $variable" } // Use lambda syntax for lazy eval
127+
log.w(exception) { "Warning with context" } // Attach throwable
128+
log.e(exception) { "Error description" } // Errors — replaces printStackTrace()
129+
130+
// In catch blocks — NEVER swallow exceptions silently:
131+
catch (e: Exception) {
132+
log.w(e) { "Descriptive message about what failed" }
133+
// ... handle gracefully
134+
}
135+
136+
// For expected/benign exceptions, still log at debug level:
137+
catch (e: SpecificException) {
138+
log.d { "Expected: description" }
139+
}
140+
```
141+
142+
Do NOT:
143+
- Use `println()` for logging (except in CLI tools under `tools/`)
144+
- Call `e.printStackTrace()` — use `log.e(e) { "msg" }` instead
145+
- Catch exceptions without logging them (no silent swallowing)
146+
- Use `catch (_: Exception)` without at least a `log.d` call
147+
115148
## Build Commands
116149

117150
```bash

app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/DesktopCardScanner.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
package com.codebutler.farebot.desktop
2424

25+
import co.touchlab.kermit.Logger
2526
import com.codebutler.farebot.card.RawCard
2627
import com.codebutler.farebot.card.nfc.pn533.PN533
2728
import com.codebutler.farebot.card.nfc.pn533.PN533Device
@@ -48,6 +49,8 @@ import kotlinx.coroutines.launch
4849
* the error is logged and the other backends continue scanning.
4950
* Results from any backend are emitted to the shared [scannedCards] flow.
5051
*/
52+
private val log = Logger.withTag("DesktopCardScanner")
53+
5154
class DesktopCardScanner : CardScanner {
5255
override val requiresActiveScan: Boolean = true
5356

@@ -80,7 +83,7 @@ class DesktopCardScanner : CardScanner {
8083
val backendJobs =
8184
backends.map { backend ->
8285
launch {
83-
println("[DesktopCardScanner] Starting ${backend.name} backend")
86+
log.i { "Starting ${backend.name} backend" }
8487
try {
8588
backend.scanLoop(
8689
onCardDetected = { tag ->
@@ -100,11 +103,11 @@ class DesktopCardScanner : CardScanner {
100103
)
101104
} catch (e: Exception) {
102105
if (isActive) {
103-
println("[DesktopCardScanner] ${backend.name} backend failed: ${e.message}")
106+
log.w(e) { "${backend.name} backend failed" }
104107
}
105108
} catch (e: Error) {
106109
// Catch LinkageError / UnsatisfiedLinkError from native libs
107-
println("[DesktopCardScanner] ${backend.name} backend unavailable: ${e.message}")
110+
log.w(e) { "${backend.name} backend unavailable" }
108111
}
109112
}
110113
}
@@ -132,7 +135,7 @@ class DesktopCardScanner : CardScanner {
132135
try {
133136
PN533Device.openAll()
134137
} catch (e: UnsatisfiedLinkError) {
135-
println("[DesktopCardScanner] libusb not available: ${e.message}")
138+
log.w(e) { "libusb not available" }
136139
emptyList()
137140
}
138141
if (transports.isEmpty()) {
@@ -144,7 +147,7 @@ class DesktopCardScanner : CardScanner {
144147
val probe = PN533(transport)
145148
val fw = probe.getFirmwareVersion()
146149
val label = "PN53x #${index + 1}"
147-
println("[DesktopCardScanner] $label firmware: $fw")
150+
log.i { "$label firmware: $fw" }
148151
if (fw.version >= 2) {
149152
backends.add(PN533ReaderBackend(transport))
150153
} else {

app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/Main.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import androidx.compose.ui.window.MenuBar
1313
import androidx.compose.ui.window.Window
1414
import androidx.compose.ui.window.application
1515
import androidx.compose.ui.window.rememberWindowState
16+
import co.touchlab.kermit.LogWriter
17+
import co.touchlab.kermit.Logger
18+
import co.touchlab.kermit.Severity
1619
import com.codebutler.farebot.card.CardType
1720
import com.codebutler.farebot.shared.FareBotApp
1821
import com.codebutler.farebot.shared.di.LocalAppGraph
@@ -26,6 +29,24 @@ import javax.imageio.ImageIO
2629
private const val ICON_PATH = "composeResources/farebot.app.generated.resources/drawable/ic_launcher.png"
2730

2831
fun main() {
32+
Logger.setLogWriters(
33+
object : LogWriter() {
34+
override fun log(
35+
severity: Severity,
36+
message: String,
37+
tag: String,
38+
throwable: Throwable?,
39+
) {
40+
val ts = java.time.LocalTime.now()
41+
val prefix = "$ts ${severity.name[0]}/$tag: "
42+
println("$prefix$message")
43+
throwable?.stackTraceToString()?.lineSequence()?.forEach { line ->
44+
println("$prefix$line")
45+
}
46+
}
47+
},
48+
)
49+
2950
System.setProperty("apple.awt.application.appearance", "system")
3051

3152
val desktop = Desktop.getDesktop()

app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PN533ReaderBackend.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class PN533ReaderBackend(
3535

3636
override suspend fun initDevice(pn533: PN533) {
3737
val fw = pn533.getFirmwareVersion()
38-
println("[$name] Firmware: $fw")
38+
log.i { "Firmware: $fw" }
3939
pn533.samConfiguration()
4040
pn533.setMaxRetries(passiveActivation = 0x02)
4141
}

app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PN53xReaderBackend.kt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
package com.codebutler.farebot.desktop
2424

25+
import co.touchlab.kermit.Logger
2526
import com.codebutler.farebot.card.CardType
2627
import com.codebutler.farebot.card.RawCard
2728
import com.codebutler.farebot.card.cepas.CEPASCardReader
@@ -51,6 +52,8 @@ import kotlinx.coroutines.delay
5152
abstract class PN53xReaderBackend(
5253
private val preOpenedTransport: Usb4JavaPN533Transport? = null,
5354
) : NfcReaderBackend {
55+
protected val log by lazy { Logger.withTag(name) }
56+
5457
protected abstract suspend fun initDevice(pn533: PN533)
5558

5659
protected open fun createTransceiver(
@@ -87,7 +90,7 @@ abstract class PN53xReaderBackend(
8790
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
8891
) {
8992
while (true) {
90-
println("[$name] Polling for cards...")
93+
log.i { "Polling for cards..." }
9194

9295
// Try ISO 14443-A (106 kbps) first — covers Classic, Ultralight, DESFire
9396
var target = pn533.inListPassiveTarget(baudRate = PN533.BAUD_RATE_106_ISO14443A)
@@ -122,20 +125,21 @@ abstract class PN53xReaderBackend(
122125
try {
123126
val rawCard = readTarget(pn533, target, onProgress)
124127
onCardRead(rawCard)
125-
println("[$name] Card read successfully")
128+
log.i { "Card read successfully" }
126129
} catch (e: Exception) {
127-
println("[$name] Read error: ${e.message}")
130+
log.e(e) { "Read error" }
128131
onError(e)
129132
}
130133

131134
// Release target
132135
try {
133136
pn533.inRelease(target.tg)
134-
} catch (_: PN533Exception) {
137+
} catch (e: PN533Exception) {
138+
log.d(e) { "inRelease failed (expected)" }
135139
}
136140

137141
// Wait for card removal by polling until no target detected
138-
println("[$name] Waiting for card removal...")
142+
log.i { "Waiting for card removal..." }
139143
waitForRemoval(pn533)
140144
}
141145
}
@@ -157,7 +161,7 @@ abstract class PN53xReaderBackend(
157161
): RawCard<*> {
158162
val info = PN533CardInfo.fromTypeA(target)
159163
val tagId = target.uid
160-
println("[$name] Type A card: type=${info.cardType}, SAK=0x%02X, UID=${tagId.hex()}".format(target.sak))
164+
log.i { "Type A card: type=${info.cardType}, SAK=0x%02X, UID=${tagId.hex()}".format(target.sak) }
161165

162166
return when (info.cardType) {
163167
CardType.MifareDesfire, CardType.ISO7816 -> {
@@ -193,7 +197,7 @@ abstract class PN53xReaderBackend(
193197
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
194198
): RawCard<*> {
195199
val tagId = target.idm
196-
println("[$name] FeliCa card: IDm=${tagId.hex()}")
200+
log.i { "FeliCa card: IDm=${tagId.hex()}" }
197201
val adapter = PN533FeliCaTagAdapter(pn533, target.idm)
198202
return FeliCaReader.readTag(tagId, adapter, onProgress = onProgress)
199203
}
@@ -208,7 +212,8 @@ abstract class PN53xReaderBackend(
208212
baudRate = PN533.BAUD_RATE_212_FELICA,
209213
initiatorData = SENSF_REQ,
210214
)
211-
} catch (_: PN533Exception) {
215+
} catch (e: PN533Exception) {
216+
log.d(e) { "Poll during removal check failed" }
212217
null
213218
}
214219
if (target == null) {
@@ -217,7 +222,8 @@ abstract class PN53xReaderBackend(
217222
// Card still present, release and keep waiting
218223
try {
219224
pn533.inRelease(target.tg)
220-
} catch (_: PN533Exception) {
225+
} catch (e: PN533Exception) {
226+
log.d(e) { "inRelease during removal wait failed" }
221227
}
222228
}
223229
}

app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PcscReaderBackend.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
package com.codebutler.farebot.desktop
2424

25+
import co.touchlab.kermit.Logger
2526
import com.codebutler.farebot.card.CardType
2627
import com.codebutler.farebot.card.RawCard
2728
import com.codebutler.farebot.card.cepas.CEPASCardReader
@@ -41,6 +42,8 @@ import javax.smartcardio.CardException
4142
import javax.smartcardio.CommandAPDU
4243
import javax.smartcardio.TerminalFactory
4344

45+
private val log = Logger.withTag("PcscReaderBackend")
46+
4447
/**
4548
* PC/SC reader backend using javax.smartcardio.
4649
*
@@ -69,18 +72,18 @@ class PcscReaderBackend : NfcReaderBackend {
6972
}
7073

7174
val terminal = terminals.first()
72-
println("[PC/SC] Using reader: ${terminal.name}")
75+
log.i { "Using reader: ${terminal.name}" }
7376

7477
while (true) {
75-
println("[PC/SC] Waiting for card...")
78+
log.i { "Waiting for card..." }
7679
terminal.waitForCardPresent(0)
7780

7881
try {
7982
val card = terminal.connect("*")
8083
try {
8184
val atr = card.atr.bytes
8285
val info = PCSCCardInfo.fromATR(atr)
83-
println("[PC/SC] Card detected: type=${info.cardType}, ATR=${atr.hex()}")
86+
log.i { "Card detected: type=${info.cardType}, ATR=${atr.hex()}" }
8487

8588
val channel = card.basicChannel
8689

@@ -93,24 +96,25 @@ class PcscReaderBackend : NfcReaderBackend {
9396
} else {
9497
byteArrayOf()
9598
}
96-
println("[PC/SC] Tag ID: ${tagId.hex()}")
99+
log.i { "Tag ID: ${tagId.hex()}" }
97100

98101
onCardDetected(ScannedTag(id = tagId, techList = listOf(info.cardType.name)))
99102
val rawCard = readCard(info, channel, tagId, onProgress)
100103
onCardRead(rawCard)
101-
println("[PC/SC] Card read successfully")
104+
log.i { "Card read successfully" }
102105
} finally {
103106
try {
104107
card.disconnect(false)
105-
} catch (_: Exception) {
108+
} catch (e: Exception) {
109+
log.d(e) { "Card disconnect failed" }
106110
}
107111
}
108112
} catch (e: Exception) {
109-
println("[PC/SC] Read error: ${e.message}")
113+
log.e(e) { "Read error" }
110114
onError(e)
111115
}
112116

113-
println("[PC/SC] Waiting for card removal...")
117+
log.i { "Waiting for card removal..." }
114118
terminal.waitForCardAbsent(0)
115119
}
116120
}

app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/RCS956ReaderBackend.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class RCS956ReaderBackend(
6262
pn533.resetMode()
6363

6464
val fw = pn533.getFirmwareVersion()
65-
println("[$name] Firmware: $fw (RC-S956)")
65+
log.i { "Firmware: $fw (RC-S956)" }
6666

6767
// mute() = resetMode + super().mute()
6868
pn533.resetMode()

app/src/commonMain/kotlin/com/codebutler/farebot/shared/nfc/ISO7816Dispatcher.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
package com.codebutler.farebot.shared.nfc
2424

25+
import co.touchlab.kermit.Logger
2526
import com.codebutler.farebot.card.RawCard
2627
import com.codebutler.farebot.card.china.ChinaRegistry
2728
import com.codebutler.farebot.card.desfire.DesfireCardReader
@@ -36,6 +37,8 @@ import com.codebutler.farebot.card.nfc.CardTransceiver
3637
* then falls back to the DESFire protocol if no known application is found.
3738
*/
3839
object ISO7816Dispatcher {
40+
private val log = Logger.withTag("ISO7816Dispatcher")
41+
3942
suspend fun readCard(
4043
tagId: ByteArray,
4144
transceiver: CardTransceiver,
@@ -59,7 +62,7 @@ object ISO7816Dispatcher {
5962
return try {
6063
ISO7816CardReader.readCard(tagId, transceiver, appConfigs, onProgress)
6164
} catch (e: Exception) {
62-
println("[ISO7816Dispatcher] ISO7816 read attempt failed: $e")
65+
log.w(e) { "ISO7816 read attempt failed" }
6366
null
6467
}
6568
}

app/src/commonMain/kotlin/com/codebutler/farebot/shared/serialize/MetrodroidJsonParser.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
package com.codebutler.farebot.shared.serialize
2424

25+
import co.touchlab.kermit.Logger
2526
import com.codebutler.farebot.base.util.ByteUtils
2627
import com.codebutler.farebot.card.RawCard
2728
import com.codebutler.farebot.card.classic.raw.RawClassicBlock
@@ -60,6 +61,8 @@ import kotlin.time.Instant
6061
* Metrodroid JSON tree, similar to how FlipperNfcParser handles Flipper NFC dumps.
6162
*/
6263
object MetrodroidJsonParser {
64+
private val log = Logger.withTag("MetrodroidJsonParser")
65+
6366
fun parse(obj: JsonObject): RawCard<*>? {
6467
val tagId = parseTagId(obj)
6568
val scannedAt = parseScannedAt(obj)
@@ -398,7 +401,7 @@ object MetrodroidJsonParser {
398401
return try {
399402
ByteUtils.hexStringToByteArray(hex)
400403
} catch (e: Exception) {
401-
println("[MetrodroidJsonParser] Failed to parse hex string: $e")
404+
log.w(e) { "Failed to parse hex string" }
402405
ByteArray(0)
403406
}
404407
}

app/src/commonMain/kotlin/com/codebutler/farebot/shared/viewmodel/CardViewModel.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.codebutler.farebot.shared.viewmodel
22

33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
5+
import co.touchlab.kermit.Logger
56
import com.codebutler.farebot.base.ui.HeaderListItem
67
import com.codebutler.farebot.base.util.DateFormatStyle
78
import com.codebutler.farebot.base.util.formatDate
@@ -39,6 +40,8 @@ class CardViewModel(
3940
private val cardSerializer: CardSerializer,
4041
private val cardPersister: CardPersister,
4142
) : ViewModel() {
43+
private val log = Logger.withTag("CardViewModel")
44+
4245
private val _uiState = MutableStateFlow(CardUiState())
4346
val uiState: StateFlow<CardUiState> = _uiState.asStateFlow()
4447

@@ -154,8 +157,7 @@ class CardViewModel(
154157
)
155158
}
156159
} catch (ex: Exception) {
157-
println("[FareBot] Card load error: ${ex::class.simpleName}: ${ex.message}")
158-
ex.printStackTrace()
160+
log.e(ex) { "Card load error: ${ex::class.simpleName}: ${ex.message}" }
159161
_uiState.value =
160162
CardUiState(
161163
isLoading = false,

0 commit comments

Comments
 (0)