Enhance file logging
This commit is contained in:
parent
67aafab46a
commit
a35e7871e8
@ -16,7 +16,6 @@ import com.elvishew.xlog.LogLevel
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.elvishew.xlog.printer.AndroidPrinter
|
||||
import com.elvishew.xlog.printer.Printer
|
||||
import com.elvishew.xlog.printer.file.FilePrinter
|
||||
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
|
||||
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
|
||||
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
||||
@ -36,6 +35,7 @@ import exh.debug.DebugToggles
|
||||
import exh.log.CrashlyticsPrinter
|
||||
import exh.log.EHDebugModeOverlay
|
||||
import exh.log.EHLogLevel
|
||||
import exh.log.EnhancedFilePrinter
|
||||
import exh.syDebugVersion
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
@ -50,6 +50,8 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||
import java.io.File
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.Security
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import javax.net.ssl.SSLContext
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.time.ExperimentalTime
|
||||
@ -193,16 +195,24 @@ open class App : Application(), LifecycleObserver {
|
||||
"logs"
|
||||
)
|
||||
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
printers += FilePrinter
|
||||
printers += EnhancedFilePrinter
|
||||
.Builder(logFolder.absolutePath)
|
||||
.fileNameGenerator(
|
||||
object : DateFileNameGenerator() {
|
||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}.log"
|
||||
return super.generateFileName(
|
||||
logLevel,
|
||||
timestamp
|
||||
) + "-${BuildConfig.BUILD_TYPE}.log"
|
||||
}
|
||||
}
|
||||
)
|
||||
.flattener { timeMillis, level, tag, message ->
|
||||
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
|
||||
}
|
||||
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
|
||||
.backupStrategy(NeverBackupStrategy())
|
||||
.build()
|
||||
|
408
app/src/main/java/exh/log/EnhancedFilePrinter.kt
Normal file
408
app/src/main/java/exh/log/EnhancedFilePrinter.kt
Normal file
@ -0,0 +1,408 @@
|
||||
package exh.log
|
||||
|
||||
import com.elvishew.xlog.flattener.Flattener2
|
||||
import com.elvishew.xlog.internal.DefaultsFactory
|
||||
import com.elvishew.xlog.printer.Printer
|
||||
import com.elvishew.xlog.printer.file.backup.BackupStrategy
|
||||
import com.elvishew.xlog.printer.file.clean.CleanStrategy
|
||||
import com.elvishew.xlog.printer.file.naming.FileNameGenerator
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
|
||||
/**
|
||||
* Log [Printer] using file system. When print a log, it will print it to the specified file.
|
||||
*
|
||||
*
|
||||
* Use the [Builder] to construct a [EnhancedFilePrinter] object.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class EnhancedFilePrinter internal constructor(builder: Builder) : Printer {
|
||||
/**
|
||||
* The folder path of log file.
|
||||
*/
|
||||
private val folderPath: String
|
||||
|
||||
/**
|
||||
* The file name generator for log file.
|
||||
*/
|
||||
private val fileNameGenerator: FileNameGenerator
|
||||
|
||||
/**
|
||||
* The backup strategy for log file.
|
||||
*/
|
||||
private val backupStrategy: BackupStrategy
|
||||
|
||||
/**
|
||||
* The clean strategy for log file.
|
||||
*/
|
||||
private val cleanStrategy: CleanStrategy
|
||||
|
||||
/**
|
||||
* The flattener when print a log.
|
||||
*/
|
||||
private val flattener: Flattener2
|
||||
|
||||
/**
|
||||
* Log writer.
|
||||
*/
|
||||
private val writer: Writer
|
||||
|
||||
@Volatile
|
||||
private var worker: Worker? = null
|
||||
|
||||
/**
|
||||
* Make sure the folder of log file exists.
|
||||
*/
|
||||
private fun checkLogFolder() {
|
||||
val folder = File(folderPath)
|
||||
if (!folder.exists()) {
|
||||
folder.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
override fun println(logLevel: Int, tag: String, msg: String) {
|
||||
val timeMillis = System.currentTimeMillis()
|
||||
if (USE_WORKER) {
|
||||
val worker = worker ?: return
|
||||
if (!worker.isStarted()) {
|
||||
worker.start()
|
||||
}
|
||||
worker.enqueue(LogItem(timeMillis, logLevel, tag, msg))
|
||||
} else {
|
||||
doPrintln(timeMillis, logLevel, tag, msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the real job of writing log to file.
|
||||
*/
|
||||
private fun doPrintln(timeMillis: Long, logLevel: Int, tag: String, msg: String) {
|
||||
var lastFileName = writer.lastFileName
|
||||
if (lastFileName == null || fileNameGenerator.isFileNameChangeable) {
|
||||
val newFileName = fileNameGenerator.generateFileName(logLevel, System.currentTimeMillis())
|
||||
require(!(newFileName == null || newFileName.trim { it <= ' ' }.isEmpty())) { "File name should not be empty." }
|
||||
if (newFileName != lastFileName) {
|
||||
if (writer.isOpened) {
|
||||
writer.close()
|
||||
}
|
||||
cleanLogFilesIfNecessary()
|
||||
if (writer.open(newFileName).not()) {
|
||||
return
|
||||
}
|
||||
lastFileName = newFileName
|
||||
}
|
||||
}
|
||||
val lastFile = writer.file ?: return
|
||||
if (backupStrategy.shouldBackup(lastFile)) {
|
||||
// Backup the log file, and create a new log file.
|
||||
writer.close()
|
||||
val backupFile = File(folderPath, "$lastFileName.bak")
|
||||
if (backupFile.exists()) {
|
||||
backupFile.delete()
|
||||
}
|
||||
lastFile.renameTo(backupFile)
|
||||
if (writer.open(lastFileName).not()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
val flattenedLog = flattener.flatten(timeMillis, logLevel, tag, msg).toString()
|
||||
writer.appendLog(flattenedLog)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean log files if should clean follow strategy
|
||||
*/
|
||||
private fun cleanLogFilesIfNecessary() {
|
||||
val logDir = File(folderPath)
|
||||
logDir.listFiles().orEmpty()
|
||||
.asSequence()
|
||||
.filter { cleanStrategy.shouldClean(it) }
|
||||
.forEach { it.delete() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for [EnhancedFilePrinter].
|
||||
*/
|
||||
class Builder
|
||||
/**
|
||||
* Construct a builder.
|
||||
*
|
||||
* @param folderPath the folder path of log file
|
||||
*/(
|
||||
/**
|
||||
* The folder path of log file.
|
||||
*/
|
||||
val folderPath: String,
|
||||
) {
|
||||
/**
|
||||
* The file name generator for log file.
|
||||
*/
|
||||
var fileNameGenerator: FileNameGenerator? = null
|
||||
|
||||
/**
|
||||
* The backup strategy for log file.
|
||||
*/
|
||||
var backupStrategy: BackupStrategy? = null
|
||||
|
||||
/**
|
||||
* The clean strategy for log file.
|
||||
*/
|
||||
var cleanStrategy: CleanStrategy? = null
|
||||
|
||||
/**
|
||||
* The flattener when print a log.
|
||||
*/
|
||||
var flattener: Flattener2? = null
|
||||
|
||||
/**
|
||||
* Set the file name generator for log file.
|
||||
*
|
||||
* @param fileNameGenerator the file name generator for log file
|
||||
* @return the builder
|
||||
*/
|
||||
fun fileNameGenerator(fileNameGenerator: FileNameGenerator): Builder {
|
||||
this.fileNameGenerator = fileNameGenerator
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the backup strategy for log file.
|
||||
*
|
||||
* @param backupStrategy the backup strategy for log file
|
||||
* @return the builder
|
||||
*/
|
||||
fun backupStrategy(backupStrategy: BackupStrategy): Builder {
|
||||
this.backupStrategy = backupStrategy
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the clean strategy for log file.
|
||||
*
|
||||
* @param cleanStrategy the clean strategy for log file
|
||||
* @return the builder
|
||||
*/
|
||||
fun cleanStrategy(cleanStrategy: CleanStrategy): Builder {
|
||||
this.cleanStrategy = cleanStrategy
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flattener when print a log.
|
||||
*
|
||||
* @param flattener the flattener when print a log
|
||||
* @return the builder
|
||||
* @since 1.6.0
|
||||
*/
|
||||
fun flattener(flattener: Flattener2): Builder {
|
||||
this.flattener = flattener
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Build configured [EnhancedFilePrinter] object.
|
||||
*
|
||||
* @return the built configured [EnhancedFilePrinter] object
|
||||
*/
|
||||
fun build(): EnhancedFilePrinter {
|
||||
fillEmptyFields()
|
||||
return EnhancedFilePrinter(this)
|
||||
}
|
||||
|
||||
private fun fillEmptyFields() {
|
||||
if (fileNameGenerator == null) {
|
||||
fileNameGenerator = DefaultsFactory.createFileNameGenerator()
|
||||
}
|
||||
if (backupStrategy == null) {
|
||||
backupStrategy = DefaultsFactory.createBackupStrategy()
|
||||
}
|
||||
if (cleanStrategy == null) {
|
||||
cleanStrategy = DefaultsFactory.createCleanStrategy()
|
||||
}
|
||||
if (flattener == null) {
|
||||
flattener = DefaultsFactory.createFlattener2()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LogItem(
|
||||
var timeMillis: Long,
|
||||
var level: Int,
|
||||
var tag: String,
|
||||
var msg: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Work in background, we can enqueue the logs, and the worker will dispatch them.
|
||||
*/
|
||||
private inner class Worker : Runnable {
|
||||
private val logs: BlockingQueue<LogItem> = LinkedBlockingQueue()
|
||||
|
||||
@Volatile
|
||||
private var started = false
|
||||
|
||||
/**
|
||||
* Enqueue the log.
|
||||
*
|
||||
* @param log the log to be written to file
|
||||
*/
|
||||
fun enqueue(log: LogItem) {
|
||||
try {
|
||||
logs.put(log)
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the worker is started.
|
||||
*
|
||||
* @return true if started, false otherwise
|
||||
*/
|
||||
fun isStarted(): Boolean {
|
||||
synchronized(this) { return started }
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the worker.
|
||||
*/
|
||||
fun start() {
|
||||
synchronized(this) {
|
||||
Thread(this).start()
|
||||
started = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
var log: LogItem
|
||||
try {
|
||||
while (logs.take().also { log = it } != null) {
|
||||
doPrintln(log.timeMillis, log.level, log.tag, log.msg)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
synchronized(this) { started = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to write the flattened logs to the log file.
|
||||
*/
|
||||
private inner class Writer {
|
||||
/**
|
||||
* Get the name of last used log file.
|
||||
* @return the name of last used log file, maybe null
|
||||
*/
|
||||
/**
|
||||
* The file name of last used log file.
|
||||
*/
|
||||
var lastFileName: String? = null
|
||||
private set
|
||||
/**
|
||||
* Get the current log file.
|
||||
*
|
||||
* @return the current log file, maybe null
|
||||
*/
|
||||
/**
|
||||
* The current log file.
|
||||
*/
|
||||
var file: File? = null
|
||||
private set
|
||||
|
||||
private var bufferedWriter: BufferedWriter? = null
|
||||
|
||||
/**
|
||||
* Whether the log file is opened.
|
||||
*
|
||||
* @return true if opened, false otherwise
|
||||
*/
|
||||
val isOpened: Boolean
|
||||
get() = bufferedWriter != null
|
||||
|
||||
/**
|
||||
* Open the file of specific name to be written into.
|
||||
*
|
||||
* @param newFileName the specific file name
|
||||
* @return true if opened successfully, false otherwise
|
||||
*/
|
||||
fun open(newFileName: String): Boolean {
|
||||
return try {
|
||||
val file = File(folderPath, newFileName)
|
||||
if (file.exists().not()) {
|
||||
(file.parentFile ?: File(file.absolutePath.substringBeforeLast(File.separatorChar))).mkdirs()
|
||||
file.createNewFile()
|
||||
}
|
||||
bufferedWriter = FileWriter(file, true).buffered()
|
||||
lastFileName = newFileName
|
||||
this.file = file
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the current log file if it is opened.
|
||||
*
|
||||
* @return true if closed successfully, false otherwise
|
||||
*/
|
||||
fun close(): Boolean {
|
||||
if (bufferedWriter != null) {
|
||||
try {
|
||||
bufferedWriter?.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
} finally {
|
||||
bufferedWriter = null
|
||||
lastFileName = null
|
||||
file = null
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the flattened log to the end of current opened log file.
|
||||
*
|
||||
* @param flattenedLog the flattened log
|
||||
*/
|
||||
fun appendLog(flattenedLog: String) {
|
||||
val bufferedWriter = bufferedWriter
|
||||
require(bufferedWriter != null)
|
||||
try {
|
||||
bufferedWriter.write(flattenedLog)
|
||||
bufferedWriter.newLine()
|
||||
bufferedWriter.flush()
|
||||
} catch (e: IOException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Use worker, write logs asynchronously.
|
||||
*/
|
||||
private const val USE_WORKER = true
|
||||
}
|
||||
|
||||
/*package*/
|
||||
init {
|
||||
folderPath = builder.folderPath
|
||||
fileNameGenerator = builder.fileNameGenerator!!
|
||||
backupStrategy = builder.backupStrategy!!
|
||||
cleanStrategy = builder.cleanStrategy!!
|
||||
flattener = builder.flattener!!
|
||||
writer = Writer()
|
||||
if (USE_WORKER) {
|
||||
worker = Worker()
|
||||
}
|
||||
checkLogFolder()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user