Логирование работы Android приложения в txt файлы

Roman Kryvolapov
5 min readSep 13, 2023

Пример, если у вас не подключен 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.BODY
OkHttpClient.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 каждый раз скинуть файл.

import android.annotation.SuppressLint
import android.os.Environment
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date

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()

}

object LogUtil {

private const val QUEUE_CAPACITY = 10000
private const val CURRENT_TAG = "LogUtilExecutionStatusTag"
private const val LOG_APP_FOLDER_NAME = "ProjectNAme"
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 timeDirectoryName: String
private val queue = ArrayDeque<LogData>(QUEUE_CAPACITY)
private var saveLogsToTxtFileJob: Job? = null

@Volatile
private var isSaveLogsToTxtFile = false

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, exception.message, exception)
enqueue(
LogData.ErrorMessageWithException(
tag = tag,
time = System.currentTimeMillis(),
message = message,
exception = exception,
)
)
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, "Created new directory with 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) {
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")
}
}

var text: String? = buildString {
queue.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 {
queue.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 {
queue.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
}
}

}

Если вы заметили ошибку- напишите пожалуйста на:
Telegram @RomanKryvolapov или
roman.kryvolapov@gmail.com или
https://www.linkedin.com/in/roman-kryvolapov/
Статья будет дополняться и обновляться по мере наличия желания и свободного времени. Если вдруг так получилось, что эта статья оказалась для вас ценной, я не буду возражать против благодарности revolut.me/romanzjqo потому что у нее, как и у всего, что я делаю помимо работы, к сожалению нет никакой монетизации.

--

--