Skip to content

Instantly share code, notes, and snippets.

@marius-m
Created November 11, 2022 12:47
Show Gist options
  • Save marius-m/3d6a9176bd17242d4633b9d05e631cf9 to your computer and use it in GitHub Desktop.
Save marius-m/3d6a9176bd17242d4633b9d05e631cf9 to your computer and use it in GitHub Desktop.
Timber logging tree to file system and auto file rotation
import android.content.Context
import android.os.Environment
import android.util.Log
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.classic.html.HTMLLayout
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.encoder.LayoutWrappingEncoder
import ch.qos.logback.core.rolling.RollingFileAppender
import ch.qos.logback.core.rolling.RollingPolicy
import ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP
import ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy
import ch.qos.logback.core.util.FileSize
import ch.qos.logback.core.util.StatusPrinter
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import timber.log.Timber.DebugTree
import java.io.File
class FSLogTree private constructor(
private val context: Context,
private val logName: String,
private val onlyAllowedTags: List<String>,
loggerType: LoggerType,
loggerTarget: LoggerTarget,
) : DebugTree() {
private val targetDir: File
init {
targetDir = targetDirByType(logName, loggerTarget)
configureByType(targetDir, loggerType)
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (priority == Log.VERBOSE) {
return
}
if (!isTagAllowed(onlyAllowedTags, tag)) {
return
}
val logMessage = StringBuilder()
.append("[")
.append(tag)
.append("]: ")
.append(message)
.toString()
when (priority) {
Log.DEBUG -> LoggerInstance.logger.debug(logMessage)
Log.INFO -> LoggerInstance.logger.info(logMessage)
Log.WARN -> LoggerInstance.logger.warn(logMessage, t)
Log.ERROR -> LoggerInstance.logger.error(logMessage, t)
}
}
//region Convenience
private fun targetDirByType(logName: String, loggerTarget: LoggerTarget): File {
val internalDir = File(context.filesDir, logName)
return when (loggerTarget) {
LoggerTarget.INTERNAL -> internalDir
LoggerTarget.EXTERNAL -> {
val externalPath = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
logName
)
if (!externalPath.exists()) {
val mkdirResult = externalPath.mkdirs()
check(mkdirResult) { "Nowhere to save logs!" }
}
externalPath
}
}
}
private fun configureByType(targetDir: File, loggerType: LoggerType) {
when (loggerType) {
LoggerType.BASIC -> configureBasic(targetDir, logName)
LoggerType.HTML -> configureHtml(targetDir, logName)
}
}
private fun configureBasic(
logDir: File,
logPrefix: String
) {
val logPath = logDir.absolutePath + File.separator + logPrefix + LOG_EXTENSION
val logPathArchive = logDir.absolutePath + File.separator + logPrefix + LOG_FILE_FORMAT
val loggerContext: LoggerContext = LoggerFactory.getILoggerFactory() as LoggerContext
loggerContext.reset()
val fileAppender: RollingFileAppender<ILoggingEvent> = RollingFileAppender()
fileAppender.context = loggerContext
fileAppender.isAppend = true
fileAppender.file = logPath
val namingPolicy = createNamingPolicy(loggerContext)
val rollingPolicy =
createRollingPolicy(loggerContext, logPathArchive, namingPolicy, fileAppender)
val layoutEncoder = PatternLayoutEncoder()
layoutEncoder.context = loggerContext
layoutEncoder.charset = Charsets.UTF_8
layoutEncoder.pattern = LOG_PATTERN
layoutEncoder.start()
fileAppender.rollingPolicy = rollingPolicy
fileAppender.encoder = layoutEncoder
fileAppender.start()
val root: ch.qos.logback.classic.Logger =
LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger
root.level = Level.DEBUG
root.addAppender(fileAppender)
StatusPrinter.print(loggerContext)
}
private fun configureHtml(
logDir: File,
logPrefix: String
) {
val logPath = logDir.absolutePath + File.separator + logPrefix + HTML_EXTENSION
val logPathArchive = logDir.absolutePath + File.separator + logPrefix + HTML_FILE_FORMAT
val loggerContext: LoggerContext = LoggerFactory.getILoggerFactory() as LoggerContext
loggerContext.reset()
val fileAppender = createFileAppended(loggerContext, logPath)
val namingPolicy = createNamingPolicy(loggerContext)
val rollingPolicy =
createRollingPolicy(loggerContext, logPathArchive, namingPolicy, fileAppender)
val htmlLayout = HTMLLayout()
htmlLayout.context = loggerContext
htmlLayout.pattern = HTML_PATTERN
htmlLayout.start()
val htmlEncoder: LayoutWrappingEncoder<ILoggingEvent> = LayoutWrappingEncoder()
htmlEncoder.context = loggerContext
htmlEncoder.layout = htmlLayout
htmlEncoder.start()
fileAppender.rollingPolicy = rollingPolicy
fileAppender.encoder = htmlEncoder
fileAppender.start()
val root: ch.qos.logback.classic.Logger = LoggerFactory
.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger
root.level = Level.DEBUG
root.addAppender(fileAppender)
StatusPrinter.print(loggerContext)
}
private fun createFileAppended(
loggerContext: LoggerContext,
logPath: String
): RollingFileAppender<ILoggingEvent> {
val fileAppender: RollingFileAppender<ILoggingEvent> = RollingFileAppender()
fileAppender.context = loggerContext
fileAppender.isAppend = true
fileAppender.file = logPath
return fileAppender
}
private fun createNamingPolicy(
loggerContext: LoggerContext
): TimeBasedFileNamingAndTriggeringPolicy<ILoggingEvent> {
val namingPolicy: SizeAndTimeBasedFNATP<ILoggingEvent> = SizeAndTimeBasedFNATP()
namingPolicy.context = loggerContext
namingPolicy.setMaxFileSize(FileSize.valueOf(FILE_SIZE))
return namingPolicy
}
private fun createRollingPolicy(
loggerContext: LoggerContext,
logPathArchive: String,
namingPolicy: TimeBasedFileNamingAndTriggeringPolicy<ILoggingEvent>,
fileAppender: RollingFileAppender<ILoggingEvent>
): RollingPolicy {
val rollingPolicy: TimeBasedRollingPolicy<ILoggingEvent> = TimeBasedRollingPolicy()
rollingPolicy.context = loggerContext
rollingPolicy.fileNamePattern = logPathArchive
rollingPolicy.maxHistory = MAX_HISTORY
rollingPolicy.timeBasedFileNamingAndTriggeringPolicy = namingPolicy
rollingPolicy.setParent(fileAppender)
rollingPolicy.start()
return rollingPolicy
}
//endregion
//region Classes
internal enum class LoggerType {
BASIC, HTML
}
internal enum class LoggerTarget {
INTERNAL, EXTERNAL
}
//endregion
//region Builder
internal class FSLogTreeBuilder private constructor(
val context: Context
) {
private var logName = "app"
private var loggerType = LoggerType.BASIC
private var loggerTarget = LoggerTarget.INTERNAL
private var allowedTags = listOf<String>()
fun withLogName(logName: String): FSLogTreeBuilder {
this.logName = logName
return this
}
fun withLoggerType(loggerType: LoggerType): FSLogTreeBuilder {
this.loggerType = loggerType
return this
}
fun withLoggerTarget(loggerTarget: LoggerTarget): FSLogTreeBuilder {
this.loggerTarget = loggerTarget
return this
}
fun withAllowedTags(vararg tags: String): FSLogTreeBuilder {
this.allowedTags = tags.toList()
return this
}
fun build(): FSLogTree {
return FSLogTree(context, logName, allowedTags, loggerType, loggerTarget)
}
companion object {
fun create(context: Context): FSLogTreeBuilder {
return FSLogTreeBuilder(context)
}
}
}
//endregion
companion object {
private const val FILE_SIZE = "2MB"
private const val MAX_HISTORY = 5
private const val HTML_EXTENSION = ".html"
private const val LOG_EXTENSION = ".log"
private const val HTML_FILE_FORMAT = ".%d{yyyy-MM-dd}.%i$HTML_EXTENSION"
private const val LOG_FILE_FORMAT = ".%d{yyyy-MM-dd}.%i$LOG_EXTENSION"
private const val HTML_PATTERN = "%d{HH:mm:ss.SSS}%level%thread%msg"
private const val LOG_PATTERN = "%date %level [%thread] %msg%n"
/**
* When there is restriction on tags, it would only allow logging of selected tags
* @param onlyAllowedTags contains tag restrictions. Empty list means no restriction
*/
fun isTagAllowed(onlyAllowedTags: List<String>, tag: String?): Boolean {
if (onlyAllowedTags.isEmpty()) {
return true
}
return onlyAllowedTags.contains(tag)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment