Logger for debug logs
parent
6b08074a70
commit
c09dd92cff
@ -0,0 +1,128 @@
|
|||||||
|
package org.koitharu.kotatsu.core.logs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import org.koitharu.kotatsu.utils.ext.subdir
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
private const val DIR = "logs"
|
||||||
|
private const val FLUSH_DELAY = 2_000L
|
||||||
|
private const val MAX_SIZE_BYTES = 1024 * 1024 // 1 MB
|
||||||
|
|
||||||
|
class FileLogger(
|
||||||
|
context: Context,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
name: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val file by lazy {
|
||||||
|
val dir = context.getExternalFilesDir(DIR) ?: context.filesDir.subdir(DIR)
|
||||||
|
File(dir, "$name.log")
|
||||||
|
}
|
||||||
|
val isEnabled: Boolean
|
||||||
|
get() = settings.isLoggingEnabled
|
||||||
|
private val dateFormat = SimpleDateFormat.getDateTimeInstance(
|
||||||
|
SimpleDateFormat.SHORT,
|
||||||
|
SimpleDateFormat.SHORT,
|
||||||
|
Locale.ROOT,
|
||||||
|
)
|
||||||
|
private val buffer = ConcurrentLinkedQueue<String>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private var flushJob: Job? = null
|
||||||
|
|
||||||
|
fun log(message: String, e: Throwable? = null) {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val text = buildString {
|
||||||
|
append(dateFormat.format(Date()))
|
||||||
|
append(": ")
|
||||||
|
if (e != null) {
|
||||||
|
append("E!")
|
||||||
|
}
|
||||||
|
append(message)
|
||||||
|
if (e != null) {
|
||||||
|
append(' ')
|
||||||
|
append(e.stackTraceToString())
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.add(text)
|
||||||
|
postFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun flush() {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flushJob?.cancelAndJoin()
|
||||||
|
flushImpl()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postFlush() {
|
||||||
|
if (flushJob?.isActive == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flushJob = processLifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
delay(FLUSH_DELAY)
|
||||||
|
runCatchingCancellable {
|
||||||
|
flushImpl()
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTraceDebug()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun flushImpl() {
|
||||||
|
mutex.withLock {
|
||||||
|
if (buffer.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runInterruptible(Dispatchers.IO) {
|
||||||
|
if (file.length() > MAX_SIZE_BYTES) {
|
||||||
|
rotate()
|
||||||
|
}
|
||||||
|
FileOutputStream(file, true).use {
|
||||||
|
while (true) {
|
||||||
|
val message = buffer.poll() ?: break
|
||||||
|
it.write(message.toByteArray())
|
||||||
|
it.write('\n'.code)
|
||||||
|
}
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun rotate() {
|
||||||
|
val length = file.length()
|
||||||
|
val bakFile = File(file.parentFile, file.name + ".bak")
|
||||||
|
file.renameTo(bakFile)
|
||||||
|
bakFile.inputStream().use { input ->
|
||||||
|
input.skip(length - MAX_SIZE_BYTES / 2)
|
||||||
|
file.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bakFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.core.logs
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class TrackerLogger
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package org.koitharu.kotatsu.core.logs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.collection.arraySetOf
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dagger.multibindings.ElementsIntoSet
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object LoggersModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@TrackerLogger
|
||||||
|
fun provideTrackerLogger(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
settings: AppSettings,
|
||||||
|
) = FileLogger(context, settings, "tracker")
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ElementsIntoSet
|
||||||
|
fun provideAllLoggers(
|
||||||
|
@TrackerLogger trackerLogger: FileLogger,
|
||||||
|
): Set<@JvmSuppressWildcards FileLogger> = arraySetOf(
|
||||||
|
trackerLogger,
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue