Created
November 11, 2022 12:47
-
-
Save marius-m/3d6a9176bd17242d4633b9d05e631cf9 to your computer and use it in GitHub Desktop.
Timber logging tree to file system and auto file rotation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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