Enhance file logging

This commit is contained in:
Jobobby04 2020-11-11 17:28:09 -05:00
parent 67aafab46a
commit a35e7871e8
2 changed files with 421 additions and 3 deletions

View File

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

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