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.XLog
|
||||||
import com.elvishew.xlog.printer.AndroidPrinter
|
import com.elvishew.xlog.printer.AndroidPrinter
|
||||||
import com.elvishew.xlog.printer.Printer
|
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.backup.NeverBackupStrategy
|
||||||
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
|
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
|
||||||
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
||||||
@ -36,6 +35,7 @@ import exh.debug.DebugToggles
|
|||||||
import exh.log.CrashlyticsPrinter
|
import exh.log.CrashlyticsPrinter
|
||||||
import exh.log.EHDebugModeOverlay
|
import exh.log.EHDebugModeOverlay
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
|
import exh.log.EnhancedFilePrinter
|
||||||
import exh.syDebugVersion
|
import exh.syDebugVersion
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
@ -50,6 +50,8 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
@ -193,16 +195,24 @@ open class App : Application(), LifecycleObserver {
|
|||||||
"logs"
|
"logs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
printers += FilePrinter
|
printers += EnhancedFilePrinter
|
||||||
.Builder(logFolder.absolutePath)
|
.Builder(logFolder.absolutePath)
|
||||||
.fileNameGenerator(
|
.fileNameGenerator(
|
||||||
object : DateFileNameGenerator() {
|
object : DateFileNameGenerator() {
|
||||||
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
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()))
|
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
|
||||||
.backupStrategy(NeverBackupStrategy())
|
.backupStrategy(NeverBackupStrategy())
|
||||||
.build()
|
.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