feat(logging): Replace Timber with Kermit for multiplatform logging (#4083)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-12-28 08:30:15 -06:00 committed by GitHub
parent a927481e4d
commit 0776e029f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 727 additions and 957 deletions

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -26,10 +27,8 @@ import kotlinx.serialization.json.Json
import org.meshtastic.core.data.model.CustomTileProviderConfig
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.map.MapTileProviderPrefs
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.plus
interface CustomTileProviderRepository {
fun getCustomTileProviders(): Flow<List<CustomTileProviderConfig>>
@ -88,7 +87,7 @@ constructor(
try {
customTileProvidersStateFlow.value = json.decodeFromString<List<CustomTileProviderConfig>>(jsonString)
} catch (e: SerializationException) {
Timber.e(e, "Error deserializing tile providers")
Logger.e(e) { "Error deserializing tile providers" }
customTileProvidersStateFlow.value = emptyList()
}
} else {
@ -102,7 +101,7 @@ constructor(
val jsonString = json.encodeToString(providers)
mapTileProviderPrefs.customTileProviders = jsonString
} catch (e: SerializationException) {
Timber.e(e, "Error serializing tile providers")
Logger.e(e) { "Error serializing tile providers" }
}
}
}

View file

@ -18,12 +18,12 @@
package org.meshtastic.core.data.datasource
import android.app.Application
import co.touchlab.kermit.Logger
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.meshtastic.core.model.BootloaderOtaQuirk
import timber.log.Timber
import javax.inject.Inject
class BootloaderOtaQuirksJsonDataSource @Inject constructor(private val application: Application) {
@ -32,7 +32,7 @@ class BootloaderOtaQuirksJsonDataSource @Inject constructor(private val applicat
val inputStream = application.assets.open("device_bootloader_ota_quirks.json")
inputStream.use { Json.decodeFromStream<ListWrapper>(it).devices }
}
.onFailure { e -> Timber.w(e, "Failed to load device_bootloader_ota_quirks.json") }
.onFailure { e -> Logger.w(e) { "Failed to load device_bootloader_ota_quirks.json" } }
.getOrDefault(emptyList())
@Serializable private data class ListWrapper(val devices: List<BootloaderOtaQuirk> = emptyList())

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource
@ -27,7 +28,6 @@ import org.meshtastic.core.database.entity.asExternalModel
import org.meshtastic.core.model.BootloaderOtaQuirk
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@ -59,45 +59,40 @@ constructor(
@Suppress("LongMethod")
suspend fun getDeviceHardwareByModel(hwModel: Int, forceRefresh: Boolean = false): Result<DeviceHardware?> =
withContext(Dispatchers.IO) {
Timber.d(
"DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=%d, forceRefresh=%b)",
hwModel,
forceRefresh,
)
Logger.d {
"DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=$hwModel, forceRefresh=$forceRefresh)"
}
val quirks = loadQuirks()
if (forceRefresh) {
Timber.d("DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache")
Logger.d { "DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache" }
localDataSource.deleteAllDeviceHardware()
} else {
// 1. Attempt to retrieve from cache first
val cachedEntity = localDataSource.getByHwModel(hwModel)
if (cachedEntity != null && !cachedEntity.isStale()) {
Timber.d("DeviceHardwareRepository: using fresh cached device hardware for hwModel=%d", hwModel)
Logger.d { "DeviceHardwareRepository: using fresh cached device hardware for hwModel=$hwModel" }
return@withContext Result.success(
applyBootloaderQuirk(hwModel, cachedEntity.asExternalModel(), quirks),
)
}
Timber.d("DeviceHardwareRepository: no fresh cache for hwModel=%d, attempting remote fetch", hwModel)
Logger.d { "DeviceHardwareRepository: no fresh cache for hwModel=$hwModel, attempting remote fetch" }
}
// 2. Fetch from remote API
runCatching {
Timber.d("DeviceHardwareRepository: fetching device hardware from remote API")
Logger.d { "DeviceHardwareRepository: fetching device hardware from remote API" }
val remoteHardware = remoteDataSource.getAllDeviceHardware()
Timber.d(
"DeviceHardwareRepository: remote API returned %d device hardware entries",
remoteHardware.size,
)
Logger.d {
"DeviceHardwareRepository: remote API returned ${remoteHardware.size} device hardware entries"
}
localDataSource.insertAllDeviceHardware(remoteHardware)
val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
Timber.d(
"DeviceHardwareRepository: lookup after remote fetch for hwModel=%d %s",
hwModel,
if (fromDb != null) "succeeded" else "returned null",
)
Logger.d {
"DeviceHardwareRepository: lookup after remote fetch for hwModel=$hwModel ${if (fromDb != null) "succeeded" else "returned null"}"
}
fromDb
}
.onSuccess {
@ -105,57 +100,48 @@ constructor(
return@withContext Result.success(applyBootloaderQuirk(hwModel, it, quirks))
}
.onFailure { e ->
Timber.w(
e,
"DeviceHardwareRepository: failed to fetch device hardware from server for hwModel=%d",
hwModel,
)
Logger.w(e) {
"DeviceHardwareRepository: failed to fetch device hardware from server for hwModel=$hwModel"
}
// 3. Attempt to use stale cache as a fallback, but only if it looks complete.
val staleEntity = localDataSource.getByHwModel(hwModel)
if (staleEntity != null && !staleEntity.isIncomplete()) {
Timber.d("DeviceHardwareRepository: using stale cached device hardware for hwModel=%d", hwModel)
Logger.d { "DeviceHardwareRepository: using stale cached device hardware for hwModel=$hwModel" }
return@withContext Result.success(
applyBootloaderQuirk(hwModel, staleEntity.asExternalModel(), quirks),
)
}
// 4. Fallback to bundled JSON if cache is empty or incomplete
Timber.d(
"DeviceHardwareRepository: cache %s for hwModel=%d, falling back to bundled JSON asset",
if (staleEntity == null) "empty" else "incomplete",
hwModel,
)
Logger.d {
"DeviceHardwareRepository: cache ${if (staleEntity == null) "empty" else "incomplete"} for hwModel=$hwModel, falling back to bundled JSON asset"
}
return@withContext loadFromBundledJson(hwModel, quirks)
}
}
private suspend fun loadFromBundledJson(hwModel: Int, quirks: List<BootloaderOtaQuirk>): Result<DeviceHardware?> =
runCatching {
Timber.d("DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=%d", hwModel)
Logger.d { "DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=$hwModel" }
val jsonHardware = jsonDataSource.loadDeviceHardwareFromJsonAsset()
Timber.d(
"DeviceHardwareRepository: bundled JSON returned %d device hardware entries",
jsonHardware.size,
)
Logger.d {
"DeviceHardwareRepository: bundled JSON returned ${jsonHardware.size} device hardware entries"
}
localDataSource.insertAllDeviceHardware(jsonHardware)
val base = localDataSource.getByHwModel(hwModel)?.asExternalModel()
Timber.d(
"DeviceHardwareRepository: lookup after JSON load for hwModel=%d %s",
hwModel,
if (base != null) "succeeded" else "returned null",
)
Logger.d {
"DeviceHardwareRepository: lookup after JSON load for hwModel=$hwModel ${if (base != null) "succeeded" else "returned null"}"
}
applyBootloaderQuirk(hwModel, base, quirks)
}
.also { result ->
result.exceptionOrNull()?.let { e ->
Timber.e(
e,
"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=%d",
hwModel,
)
Logger.e(e) {
"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=$hwModel"
}
}
}
@ -174,7 +160,7 @@ constructor(
private fun loadQuirks(): List<BootloaderOtaQuirk> {
val quirks = bootloaderOtaQuirksJsonDataSource.loadBootloaderOtaQuirksFromJsonAsset()
Timber.d("DeviceHardwareRepository: loaded %d bootloader quirks", quirks.size)
Logger.d { "DeviceHardwareRepository: loaded ${quirks.size} bootloader quirks" }
return quirks
}
@ -186,17 +172,11 @@ constructor(
if (base == null) return null
val quirk = quirks.firstOrNull { it.hwModel == hwModel }
Timber.d(
"DeviceHardwareRepository: applyBootloaderQuirk for hwModel=%d, quirk found=%b",
hwModel,
quirk != null,
)
Logger.d { "DeviceHardwareRepository: applyBootloaderQuirk for hwModel=$hwModel, quirk found=${quirk != null}" }
return if (quirk != null) {
Timber.d(
"DeviceHardwareRepository: applying quirk: requiresBootloaderUpgradeForOta=%b, infoUrl=%s",
quirk.requiresBootloaderUpgradeForOta,
quirk.infoUrl,
)
Logger.d {
"DeviceHardwareRepository: applying quirk: requiresBootloaderUpgradeForOta=${quirk.requiresBootloaderUpgradeForOta}, infoUrl=${quirk.infoUrl}"
}
base.copy(
requiresBootloaderUpgradeForOta = quirk.requiresBootloaderUpgradeForOta,
bootloaderInfoUrl = quirk.infoUrl,

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource
@ -26,7 +27,6 @@ import org.meshtastic.core.database.entity.FirmwareReleaseEntity
import org.meshtastic.core.database.entity.FirmwareReleaseType
import org.meshtastic.core.database.entity.asExternalModel
import org.meshtastic.core.network.FirmwareReleaseRemoteDataSource
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@ -68,7 +68,7 @@ constructor(
// This gives the UI something to show immediately.
val cachedRelease = localDataSource.getLatestRelease(releaseType)
cachedRelease?.let {
Timber.d("Emitting cached firmware for $releaseType (isStale=${it.isStale()})")
Logger.d { "Emitting cached firmware for $releaseType (isStale=${it.isStale()})" }
emit(it.asExternalModel())
}
@ -84,7 +84,7 @@ constructor(
// The `distinctUntilChanged()` operator on the collector side will prevent
// re-emitting the same data if the cache wasn't actually updated.
val finalRelease = localDataSource.getLatestRelease(releaseType)
Timber.d("Emitting final firmware for $releaseType from cache.")
Logger.d { "Emitting final firmware for $releaseType from cache." }
emit(finalRelease?.asExternalModel())
}
@ -98,7 +98,7 @@ constructor(
private suspend fun updateCacheFromSources() {
val remoteFetchSuccess =
runCatching {
Timber.d("Fetching fresh firmware releases from remote API.")
Logger.d { "Fetching fresh firmware releases from remote API." }
val networkReleases = remoteDataSource.getFirmwareReleases()
// The API fetches all release types, so we cache them all at once.
@ -109,13 +109,13 @@ constructor(
// If remote fetch failed, try the JSON fallback as a last resort.
if (!remoteFetchSuccess) {
Timber.w("Remote fetch failed, attempting to cache from bundled JSON.")
Logger.w { "Remote fetch failed, attempting to cache from bundled JSON." }
runCatching {
val jsonReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
localDataSource.insertFirmwareReleases(jsonReleases.releases.stable, FirmwareReleaseType.STABLE)
localDataSource.insertFirmwareReleases(jsonReleases.releases.alpha, FirmwareReleaseType.ALPHA)
}
.onFailure { Timber.w("Failed to cache from JSON: ${it.message}") }
.onFailure { Logger.w { "Failed to cache from JSON: ${it.message}" } }
}
}

View file

@ -27,6 +27,7 @@ import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.location.LocationRequestCompat
import androidx.core.location.altitude.AltitudeConverterCompat
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
@ -34,7 +35,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -68,7 +68,7 @@ constructor(
try {
AltitudeConverterCompat.addMslAltitudeToLocation(context, location)
} catch (e: Exception) {
Timber.e(e, "addMslAltitudeToLocation() failed")
Logger.e(e) { "addMslAltitudeToLocation() failed" }
}
}
// info("New location: $location")
@ -85,9 +85,9 @@ constructor(
}
}
Timber.i(
"Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m",
)
Logger.i {
"Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m"
}
_receivingLocationUpdates.value = true
analytics.track("location_start") // Figure out how many users needed to use the phone GPS
@ -106,7 +106,7 @@ constructor(
}
awaitClose {
Timber.i("Stopping location requests")
Logger.i { "Stopping location requests" }
_receivingLocationUpdates.value = false
analytics.track("location_stop")