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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package gq.kirmanak.mealient.logging
|
||||
|
||||
interface LogRedactor {
|
||||
|
||||
fun redact(message: String): String
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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() }
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user