Логване на работата на Android приложение в txt файлове
Описание
Пример за случая, когато нямате свързан firebase или нещо подобно: понякога е полезно работата на приложението да се логва в текстови файлове, и за целта ще споделя едно полезно решение, което наскоро добавих в един от проектите. Някои неща може, разбира се, да не са оптимални, защото са направени набързо и на коляно; освен това би било хубаво да се използва не object, а обикновен клас, а логърът да се инжектира с помощта на di.
Използване
Работи така: пишем там, където трябва нещо да се логва:
// логване във всяка част на приложениетоlogDebug("Message", "Tag")logError("Message", "Tag")logError(exception, "Tag")logError("Message", exception, "Tag")
// за логване от Java добавяме към методите на// LogUtil анотацията @JvmStatic и послеLogUtil.logDebug("Message", "Tag")
// за мрежови заявки добавяме в OkHttpClient.Builder// (кодът е само за пример, разбира се, това трябва да се прави с помощта на di)val logger = HttpLoggingInterceptor { logNetwork(it)}logger.level = HttpLoggingInterceptor.Level.BODYOkHttpClient.Builder().apply { // bla bla bal addInterceptor(logger)}.build()
// не забравяме да добавим в манифеста// android.permission.WRITE_EXTERNAL_STORAGEСлед това в папката Документи на телефона се създава папка с името, записано в константата LOG_APP_FOLDER_NAME на класа LogUtil, в нея при всяко отваряне на приложението се създават папки с времето на отваряне, а в такава папка се създава LOG.txt с всички логове, LOG_ERRORS.txt само с това, което е изпратено чрез метода logError, и LOG_NETWORK.txt само с мрежовите заявки, във вид, леко оптимизиран за целта.
Като вариант за развитие на тази идея може да се добави и изпращане на всичко това към някакво api, за да не молим всеки път qa да пратят файла.
Структура на данните LogData
sealed class LogData {
data class DebugMessage( val tag: String, val time: Long, val message: String, ) : LogData()
data class ErrorMessage( val tag: String, val time: Long, val message: String, ) : LogData()
data class ExceptionMessage( val tag: String, val time: Long, val exception: Throwable, ) : LogData()
data class ErrorMessageWithException( val tag: String, val time: Long, val message: String, val exception: Throwable, ) : LogData()
data class NetworkMessage( val time: Long, val message: String, ) : LogData()
}Реализация на LogUtil
import android.annotation.SuppressLintimport android.os.Environmentimport android.util.Logimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.Jobimport kotlinx.coroutines.launchimport java.io.Fileimport java.io.FileOutputStreamimport java.text.SimpleDateFormatimport java.util.Date
/** * This utility saves application logs to a file */object LogUtil {
private val timeDirectoryName: String
private const val QUEUE_CAPACITY = 10000 private const val CURRENT_TAG = "LogUtilExecutionStatusTag" private const val LOG_APP_FOLDER_NAME = "eid" private const val TIME_FORMAT_FOR_LOG = "HH:mm:ss dd-MM-yyyy" private const val TIME_FORMAT_FOR_DIRECTORY = "HH-mm-ss_dd-MM-yyyy" private const val TAG = "TAG: " private const val TIME = "TIME: " private const val ERROR_STACKTRACE = "ERROR STACKTRACE: " private const val ERROR_MESSAGE = "ERROR: " private const val DEBUG_MESSAGE = "MESSAGE: " private const val NEW_LINE = "\n"
private val queue = ArrayDeque<LogData>(QUEUE_CAPACITY)
private var saveLogsToTxtFileJob: Job? = null
@Volatile private var isSaveLogsToTxtFile = false
init { Log.d(CURRENT_TAG, "init") timeDirectoryName = getCurrentTimeForDirectory() }
fun logDebug(message: String, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.d(tag, message) enqueue( LogData.DebugMessage( tag = tag, time = System.currentTimeMillis(), message = message, ) ) saveLogsToTxtFile() } } }
fun logError(message: String, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.e(tag, message) enqueue( LogData.ErrorMessage( tag = tag, time = System.currentTimeMillis(), message = message, ) ) saveLogsToTxtFile() } } }
fun logError(exception: Throwable, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.e(tag, exception.message, exception) enqueue( LogData.ExceptionMessage( tag = tag, time = System.currentTimeMillis(), exception = exception, ) ) saveLogsToTxtFile() } } }
fun logError(message: String, exception: Throwable, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.e(tag, "$message, exception: ${exception.message}", exception) enqueue( LogData.ErrorMessageWithException( tag = tag, time = System.currentTimeMillis(), message = message, exception = exception, ) ) saveLogsToTxtFile() } } }
fun logError(message: String, error: String?, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.e(tag, "$message, error: $error") enqueue( LogData.ErrorMessage( tag = tag, time = System.currentTimeMillis(), message = message, ) ) saveLogsToTxtFile() } } }
fun logNetwork(message: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { enqueue( LogData.NetworkMessage( time = System.currentTimeMillis(), message = message, ) ) saveLogsToTxtFile() } } }
@SuppressLint("SimpleDateFormat") private fun getTime(time: Long): String { return try { val date = Date(time) val timeString = SimpleDateFormat(TIME_FORMAT_FOR_LOG).format(date) timeString.ifEmpty { Log.e(CURRENT_TAG, "getTime time.ifEmpty") time.toString() } } catch (e: Exception) { Log.e(CURRENT_TAG, "getCurrentTime exception: ${e.message}", e) time.toString() } }
@SuppressLint("SimpleDateFormat") private fun getCurrentTimeForDirectory(): String { val time = System.currentTimeMillis() return try { val date = Date(time) val timeString = SimpleDateFormat(TIME_FORMAT_FOR_DIRECTORY).format(date) Log.d(CURRENT_TAG, "getCurrentTimeForDirectory time: $time") timeString.ifEmpty { Log.e(CURRENT_TAG, "getCurrentTimeForDirectory time.ifEmpty") time.toString() } } catch (e: Exception) { Log.e(CURRENT_TAG, "getCurrentTimeForDirectory exception: ${e.message}", e) time.toString() } }
private fun enqueue(message: LogData) { try { while (queue.size >= QUEUE_CAPACITY) { Log.d(CURRENT_TAG, "enqueue removeFirst") queue.removeFirst() } queue.addLast(message) } catch (e: Exception) { Log.e(CURRENT_TAG, "enqueue exception: ${e.message}", e) } }
private fun saveLogsToTxtFile() { if (isSaveLogsToTxtFile) return isSaveLogsToTxtFile = true saveLogsToTxtFileJob?.cancel() saveLogsToTxtFileJob = null saveLogsToTxtFileJob = CoroutineScope(Dispatchers.IO).launch { try { val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) .path val rootDirectory = File(path) if (!rootDirectory.exists()) { val created = rootDirectory.mkdirs() if (!created) { Log.e(CURRENT_TAG, "Root log directory not created") isSaveLogsToTxtFile = false return@launch } } val appDirectory = File(path, LOG_APP_FOLDER_NAME) if (!appDirectory.exists()) { val created = appDirectory.mkdirs() if (!created) {// Log.e(CURRENT_TAG, "App log directory not created") isSaveLogsToTxtFile = false return@launch } } val timeDirectory = File(appDirectory, timeDirectoryName) if (!timeDirectory.exists()) { val created = timeDirectory.mkdirs() if (!created) { Log.e(CURRENT_TAG, "App time directory not created") isSaveLogsToTxtFile = false return@launch } } val fileAll = File(timeDirectory, "LOG.txt") if (!fileAll.exists()) { val created = fileAll.createNewFile() if (!created) { Log.e(CURRENT_TAG, "App log file not created") } }
val currentQueue = ArrayDeque(queue) var text: String? = buildString { currentQueue.forEach { when (it) { is LogData.DebugMessage -> { append(TAG) append(it.tag) append(NEW_LINE) append(TIME) append(getTime(it.time)) append(NEW_LINE) append(DEBUG_MESSAGE) append(it.message) append(NEW_LINE) append(NEW_LINE) }
is LogData.ErrorMessage -> { append(TAG) append(it.tag) append(NEW_LINE) append(TIME) append(getTime(it.time)) append(NEW_LINE) append(ERROR_MESSAGE) append(it.message) append(NEW_LINE) append(NEW_LINE) }
is LogData.ExceptionMessage -> { append(TAG) append(it.tag) append(NEW_LINE) append(TIME) append(getTime(it.time)) append(NEW_LINE) append(ERROR_STACKTRACE) it.exception.stackTrace.forEach { element -> append(element.toString()) append(NEW_LINE) } append(NEW_LINE) }
is LogData.ErrorMessageWithException -> { append(TAG) append(it.tag) append(NEW_LINE) append(TIME) append(getTime(it.time)) append(NEW_LINE) append(ERROR_MESSAGE) append(it.message) append(NEW_LINE) append(ERROR_STACKTRACE) it.exception.stackTrace.forEach { element -> append(element.toString()) append(NEW_LINE) } append(NEW_LINE) }
is LogData.NetworkMessage -> { append(TAG) append("OkHttpClient") append(NEW_LINE) append(TIME) append(getTime(it.time)) append(NEW_LINE) append(DEBUG_MESSAGE) append(it.message) append(NEW_LINE) append(NEW_LINE) } } } } FileOutputStream(fileAll).use { outputStream -> outputStream.write(text!!.toByteArray()) outputStream.flush() } Log.d( CURRENT_TAG, "Save logs size: ${text?.length}" )
val fileErrors = File(timeDirectory, "LOG_ERRORS.txt") if (!fileErrors.exists()) { val created = fileErrors.createNewFile() if (!created) { Log.e(CURRENT_TAG, "App log error file not created") } } text = buildString { currentQueue.filter { it is LogData.ErrorMessage || it is LogData.ExceptionMessage || it is LogData.ErrorMessageWithException }.forEach { when (it) { is LogData.ErrorMessage -> { append(TAG) append(it.tag) append(NEW_LINE) append(TIME) append(getTime(it.time)) append(NEW_LINE) append(ERROR_MESSAGE) append(it.message) append(NEW_LINE) append(NEW_LINE) }
is LogData.ExceptionMessage -> { append(TAG) append(it.tag) append(NEW_LINE) append(TIME) append(getTime(it.time)) append(NEW_LINE) append(ERROR_STACKTRACE) it.exception.stackTrace.forEach { element -> append(element.toString()) append(NEW_LINE) } append(NEW_LINE) }
is LogData.ErrorMessageWithException -> { append(TAG) append(it.tag) append(NEW_LINE) append(TIME) append(getTime(it.time)) append(NEW_LINE) append(ERROR_MESSAGE) append(it.message) append(NEW_LINE) append(ERROR_STACKTRACE) it.exception.stackTrace.forEach { element -> append(element.toString()) append(NEW_LINE) } append(NEW_LINE) }
else -> { // nothing }
} } } FileOutputStream(fileErrors).use { outputStream -> outputStream.write(text!!.toByteArray()) outputStream.flush() }
val fileNetwork = File(timeDirectory, "LOG_NETWORK.txt") if (!fileNetwork.exists()) { val created = fileNetwork.createNewFile() if (!created) { Log.e(CURRENT_TAG, "App log network file not created") } } text = buildString { currentQueue.filterIsInstance<LogData.NetworkMessage>() .forEach { append(getTime(it.time)) append(NEW_LINE) append(it.message) append(NEW_LINE) append(NEW_LINE) } } FileOutputStream(fileNetwork).use { outputStream -> outputStream.write(text!!.toByteArray()) outputStream.flush() }
text = null } catch (e: Exception) { Log.e(CURRENT_TAG, "saveLogsToTxtFile exception: ${e.message}", e) } isSaveLogsToTxtFile = false } }
}