Содержание:
➤ Описание
➤ Использование
➤ Структура данных LogData
➤ Реализация LogUtil
➤ Описание
Пример, если у вас не подключен 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 каждый раз скинуть файл.
➤ Структура данных 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.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
/**
* 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)
}
is LogData.ErrorMessageWithData -> {
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)
}
}
}
}
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
}
}
}