Implement sending logs to developer (#190)

* Save logs to a file

* Send logs via email

* Enable network logs in release builds

* Remove useless chooser title

* Append to logs file and ignore I/O errors

* Ensure email and password are not logged

* Ensure base URL is never logged

* Add logs disclaimer
This commit is contained in:
Kirill Kamakin
2023-12-10 12:49:03 +01:00
committed by GitHub
parent f6f44c7592
commit 36a72b63de
29 changed files with 500 additions and 157 deletions

View File

@@ -8,12 +8,13 @@ import dagger.multibindings.IntoSet
@Module
@InstallIn(SingletonComponent::class)
interface LoggingModule {
@Binds
fun bindLogger(loggerImpl: LoggerImpl): Logger
internal interface AppenderModule {
@Binds
@IntoSet
fun bindLogcatAppender(logcatAppender: LogcatAppender): Appender
@Binds
@IntoSet
fun bindFileAppender(fileAppender: FileAppender): Appender
}

View File

@@ -0,0 +1,109 @@
package gq.kirmanak.mealient.logging
import android.app.Application
import gq.kirmanak.mealient.architecture.configuration.AppDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import java.io.BufferedWriter
import java.io.FileWriter
import java.io.IOException
import java.io.Writer
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import javax.inject.Inject
import javax.inject.Singleton
private const val MAX_LOG_FILE_SIZE = 1024 * 1024 * 10L // 10 MB
@Singleton
internal class FileAppender @Inject constructor(
private val application: Application,
dispatchers: AppDispatchers,
) : Appender {
private data class LogInfo(
val logTime: Instant,
val logLevel: LogLevel,
val tag: String,
val message: String,
)
private val fileWriter: Writer? = createFileWriter()
private val logChannel = Channel<LogInfo>(
capacity = 100,
onBufferOverflow = BufferOverflow.DROP_LATEST,
)
private val coroutineScope = CoroutineScope(dispatchers.io + SupervisorJob())
private val dateTimeFormatter =
DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault())
init {
startLogWriter()
}
private fun createFileWriter(): Writer? {
val file = application.getLogFile()
if (file.length() > MAX_LOG_FILE_SIZE) {
file.delete()
}
val writer = try {
FileWriter(file, /* append = */ true)
} catch (e: IOException) {
return null
}
return BufferedWriter(writer)
}
private fun startLogWriter() {
if (fileWriter == null) {
return
}
coroutineScope.launch {
for (logInfo in logChannel) {
val time = dateTimeFormatter.format(logInfo.logTime)
val level = logInfo.logLevel.name.first()
logInfo.message.lines().forEach {
try {
fileWriter.appendLine("$time $level ${logInfo.tag}: $it")
} catch (e: IOException) {
// Ignore
}
}
}
}
}
override fun isLoggable(logLevel: LogLevel): Boolean = true
override fun isLoggable(logLevel: LogLevel, tag: String): Boolean = true
override fun log(logLevel: LogLevel, tag: String, message: String) {
val logInfo = LogInfo(
logTime = Instant.now(),
logLevel = logLevel,
tag = tag,
message = message,
)
logChannel.trySend(logInfo)
}
protected fun finalize() {
coroutineScope.cancel("Object is being destroyed")
try {
fileWriter?.close()
} catch (e: IOException) {
// Ignore
}
}
}

View File

@@ -0,0 +1,6 @@
package gq.kirmanak.mealient.logging
interface LogRedactor {
fun redact(message: String): String
}

View File

@@ -4,7 +4,7 @@ import android.util.Log
import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration
import javax.inject.Inject
class LogcatAppender @Inject constructor(
internal class LogcatAppender @Inject constructor(
private val buildConfiguration: BuildConfiguration,
) : Appender {

View File

@@ -1,7 +1,12 @@
package gq.kirmanak.mealient.logging
import android.content.Context
import java.io.File
typealias MessageSupplier = () -> String
private const val LOG_FILE_NAME = "log.txt"
interface Logger {
fun v(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
@@ -13,4 +18,8 @@ interface Logger {
fun w(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
fun e(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
}
fun Context.getLogFile(): File {
return File(filesDir, LOG_FILE_NAME)
}

View File

@@ -6,6 +6,7 @@ import javax.inject.Inject
class LoggerImpl @Inject constructor(
private val appenders: Set<@JvmSuppressWildcards Appender>,
private val redactors: Set<@JvmSuppressWildcards LogRedactor>,
) : Logger {
override fun v(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
@@ -45,12 +46,23 @@ class LoggerImpl @Inject constructor(
if (appender.isLoggable(logLevel, logTag).not()) continue
message = message ?: (messageSupplier() + createStackTrace(t))
message = message ?: buildLogMessage(messageSupplier, t)
appender.log(logLevel, logTag, message)
}
}
private fun buildLogMessage(
messageSupplier: MessageSupplier,
t: Throwable?
): String {
var message = messageSupplier() + createStackTrace(t)
for (redactor in redactors) {
message = redactor.redact(message)
}
return message
}
private fun createStackTrace(throwable: Throwable?): String =
throwable?.let { Log.getStackTraceString(it) }
?.takeUnless { it.isBlank() }

View File

@@ -0,0 +1,15 @@
package gq.kirmanak.mealient.logging
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface LoggerModule {
@Binds
fun bindLogger(loggerImpl: LoggerImpl): Logger
}