mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-12 08:42:01 -04:00
chore: Integrate MQTT logging with Kermit and enhance PII sanitization (#5338)
This commit is contained in:
@@ -31,26 +31,21 @@ import org.meshtastic.core.repository.PlatformAnalytics
|
||||
class FdroidPlatformAnalytics : PlatformAnalytics {
|
||||
init {
|
||||
// For F-Droid builds we don't initialize external analytics services.
|
||||
// In debug builds we attach a DebugTree for convenient local logging, but
|
||||
// release builds rely on system logging only.
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.setMinSeverity(Severity.Debug)
|
||||
Logger.i { "F-Droid platform no-op analytics initialized (Debug mode)." }
|
||||
} else {
|
||||
Logger.setMinSeverity(Severity.Info)
|
||||
Logger.i { "F-Droid platform no-op analytics initialized." }
|
||||
// Configure Kermit logging: Debug level for debug builds, Info level for release builds.
|
||||
Logger.setMinSeverity(if (BuildConfig.DEBUG) Severity.Debug else Severity.Info)
|
||||
Logger.i {
|
||||
"F-Droid platform no-op analytics initialized (${if (BuildConfig.DEBUG) "Debug" else "Info"} logging)."
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDeviceAttributes(firmwareVersion: String, model: String) {
|
||||
// No-op for F-Droid
|
||||
Logger.d { "Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model" }
|
||||
if (BuildConfig.DEBUG) Logger.d { "setDeviceAttributes: firmwareVersion=$firmwareVersion, model=$model" }
|
||||
}
|
||||
|
||||
override val isPlatformServicesAvailable: Boolean
|
||||
get() = false
|
||||
|
||||
override fun track(event: String, vararg properties: DataPair) {
|
||||
Logger.d { "Track called: event=$event, properties=${properties.toList()}" }
|
||||
// No-op for F-Droid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +343,13 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic
|
||||
// Explicitly handle String
|
||||
else -> bundle.putString(it.name, value.toString()) // Fallback for other types
|
||||
}
|
||||
KermitLogger.withTag(TAG).d { "Analytics: track $event (${it.name} : $value)" }
|
||||
KermitLogger.withTag(TAG).d {
|
||||
if (BuildConfig.DEBUG) {
|
||||
"Analytics: track $event (${it.name} : $value)"
|
||||
} else {
|
||||
"Analytics: track $event (${it.name})"
|
||||
}
|
||||
}
|
||||
}
|
||||
Firebase.analytics.logEvent(event, bundle)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.network.repository
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.meshtastic.mqtt.MqttLogLevel
|
||||
import org.meshtastic.mqtt.MqttLogger
|
||||
|
||||
/**
|
||||
* Adapter that implements [MqttLogger] to send MQTT client logs to Kermit's [Logger].
|
||||
*
|
||||
* This allows the MQTTastic library logging to integrate with the app's logging infrastructure, including any
|
||||
* configured log sinks (Crashlytics, Datadog, etc.).
|
||||
*
|
||||
* The library's [tag] (e.g. "MqttClient", "MqttConnection") is forwarded as a structured Kermit tag so that Datadog
|
||||
* receives it as an indexed attribute rather than freetext in the message body, enabling per-component filtering in log
|
||||
* queries.
|
||||
*
|
||||
* Note: The production log level should be set to [MqttLogLevel.WARN] (not INFO) to prevent the library's own
|
||||
* INFO-level messages (which include endpoint addresses and topic strings) from reaching remote analytics sinks.
|
||||
*/
|
||||
class KermitMqttLogger : MqttLogger {
|
||||
override fun log(level: MqttLogLevel, tag: String, message: String, throwable: Throwable?) {
|
||||
val logger = Logger.withTag(tag)
|
||||
when (level) {
|
||||
MqttLogLevel.TRACE -> logger.v(throwable) { message }
|
||||
MqttLogLevel.DEBUG -> logger.d(throwable) { message }
|
||||
MqttLogLevel.INFO -> logger.i(throwable) { message }
|
||||
MqttLogLevel.WARN -> logger.w(throwable) { message }
|
||||
MqttLogLevel.ERROR -> logger.e(throwable) { message }
|
||||
MqttLogLevel.NONE -> return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import org.meshtastic.mqtt.ConnectionState
|
||||
import org.meshtastic.mqtt.MqttClient
|
||||
import org.meshtastic.mqtt.MqttEndpoint
|
||||
import org.meshtastic.mqtt.MqttException
|
||||
import org.meshtastic.mqtt.MqttLogLevel
|
||||
import org.meshtastic.mqtt.MqttMessage
|
||||
import org.meshtastic.mqtt.QoS
|
||||
import org.meshtastic.mqtt.packet.Subscription
|
||||
@@ -57,6 +58,7 @@ import kotlin.concurrent.Volatile
|
||||
class MQTTRepositoryImpl(
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val buildConfigProvider: org.meshtastic.core.common.BuildConfigProvider,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : MQTTRepository {
|
||||
|
||||
@@ -109,6 +111,11 @@ class MQTTRepositoryImpl(
|
||||
autoReconnect = true
|
||||
username = mqttConfig?.username
|
||||
mqttConfig?.password?.let { password(it) }
|
||||
logger = KermitMqttLogger()
|
||||
// WARN for production: the library emits endpoint addresses and topic strings at
|
||||
// INFO level. WARN messages (reconnect, timeout, retry) contain no PII and are
|
||||
// exactly the signals needed for production diagnostics.
|
||||
logLevel = if (buildConfigProvider.isDebug) MqttLogLevel.DEBUG else MqttLogLevel.WARN
|
||||
}
|
||||
client = newClient
|
||||
|
||||
@@ -139,7 +146,27 @@ class MQTTRepositoryImpl(
|
||||
launch { newClient.messages.collect { msg -> processMessage(msg) } }
|
||||
|
||||
// Forward the client's connection state to the repo-level StateFlow for UI observation.
|
||||
launch { newClient.connectionState.collect { _connectionState.value = it } }
|
||||
// Also emit structured log messages on transitions so reconnect attempt counts and
|
||||
// disconnect reason codes are visible in Crashlytics/Datadog without any PII.
|
||||
launch {
|
||||
newClient.connectionState.collect { state ->
|
||||
_connectionState.value = state
|
||||
when (state) {
|
||||
ConnectionState.Connecting -> Logger.i { "MQTT connecting" }
|
||||
|
||||
ConnectionState.Connected -> Logger.i { "MQTT connected" }
|
||||
|
||||
is ConnectionState.Reconnecting -> {
|
||||
val errorDetail = state.lastError?.message?.let { ": $it" } ?: ""
|
||||
Logger.w { "MQTT reconnecting (attempt ${state.attempt}$errorDetail)" }
|
||||
}
|
||||
|
||||
is ConnectionState.Disconnected -> {
|
||||
state.reason?.let { Logger.w { "MQTT disconnected: ${it.message}" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retry the initial connect with exponential backoff. Once established,
|
||||
// autoReconnect handles subsequent drops and re-subscribes internally.
|
||||
@@ -147,7 +174,9 @@ class MQTTRepositoryImpl(
|
||||
var reconnectDelay = INITIAL_RECONNECT_DELAY_MS
|
||||
while (true) {
|
||||
val result = safeCatching {
|
||||
Logger.i { "MQTT Connecting to $endpoint" }
|
||||
Logger.i {
|
||||
if (buildConfigProvider.isDebug) "MQTT Connecting to $endpoint" else "MQTT Connecting..."
|
||||
}
|
||||
newClient.connect(endpoint)
|
||||
if (subscriptions.isNotEmpty()) {
|
||||
Logger.d { "MQTT subscribing to ${subscriptions.size} topics" }
|
||||
@@ -181,13 +210,11 @@ class MQTTRepositoryImpl(
|
||||
private fun ProducerScope<MqttClientProxyMessage>.processMessage(msg: MqttMessage) {
|
||||
val topic = msg.topic
|
||||
val payload = msg.payload.toByteArray()
|
||||
Logger.d { "MQTT received message on topic $topic (size: ${payload.size} bytes)" }
|
||||
|
||||
if (topic.contains("/json/")) {
|
||||
try {
|
||||
val jsonStr = payload.decodeToString()
|
||||
json.decodeFromString<MqttJsonPayload>(jsonStr)
|
||||
Logger.d { "MQTT parsed JSON payload successfully" }
|
||||
trySend(MqttClientProxyMessage(topic = topic, text = jsonStr, retained = msg.retain))
|
||||
} catch (e: JsonDecodingException) {
|
||||
Logger.e(e) { "Failed to parse MQTT JSON: ${e.shortMessage} (path: ${e.path})" }
|
||||
@@ -204,10 +231,15 @@ class MQTTRepositoryImpl(
|
||||
override fun publish(topic: String, data: ByteArray, retained: Boolean) {
|
||||
val currentClient = client
|
||||
if (currentClient == null) {
|
||||
Logger.w { "MQTT publish to $topic dropped: client not connected" }
|
||||
Logger.w {
|
||||
if (buildConfigProvider.isDebug) {
|
||||
"MQTT publish to $topic dropped: client not connected"
|
||||
} else {
|
||||
"MQTT publish dropped: client not connected"
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
Logger.d { "MQTT publishing message to topic $topic (size: ${data.size} bytes, retained: $retained)" }
|
||||
scope.launch {
|
||||
publishSemaphore.withPermit {
|
||||
safeCatching {
|
||||
@@ -215,7 +247,15 @@ class MQTTRepositoryImpl(
|
||||
MqttMessage(topic = topic, payload = data, qos = QoS.AT_LEAST_ONCE, retain = retained),
|
||||
)
|
||||
}
|
||||
.onFailure { e -> Logger.w(e) { "MQTT publish to $topic failed" } }
|
||||
.onFailure { e ->
|
||||
Logger.w(e) {
|
||||
if (buildConfigProvider.isDebug) {
|
||||
"MQTT publish to $topic failed"
|
||||
} else {
|
||||
"MQTT publish (${data.size} bytes) failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user