diff --git a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/Logging.kt b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/Logging.kt index 83812ca6..7325c4a5 100644 --- a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/Logging.kt +++ b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/Logging.kt @@ -19,9 +19,25 @@ import mu.KotlinLogging import org.slf4j.Logger import org.slf4j.LoggerFactory +private fun fileSizeValueOfOrDefault( + fileSizeStr: String, + default: String, +): FileSize { + return try { + FileSize.valueOf(fileSizeStr) + } catch (e: IllegalArgumentException) { + FileSize.valueOf(default) + } +} + +private const val FILE_APPENDER_NAME = "SuwayomiDefaultAppender" + private fun createRollingFileAppender( logContext: LoggerContext, logDirPath: String, + maxFiles: Int, + maxFileSize: String, + maxTotalSize: String, ): RollingFileAppender { val logFilename = "application" @@ -34,7 +50,7 @@ private fun createRollingFileAppender( val appender = RollingFileAppender().apply { - name = "FILE" + name = FILE_APPENDER_NAME context = logContext encoder = logEncoder file = "$logDirPath/$logFilename.log" @@ -45,9 +61,9 @@ private fun createRollingFileAppender( context = logContext setParent(appender) fileNamePattern = "$logDirPath/${logFilename}_%d{yyyy-MM-dd}_%i.log.gz" - setMaxFileSize(FileSize.valueOf("10mb")) - maxHistory = 14 - setTotalSizeCap(FileSize.valueOf("1gb")) + maxHistory = maxFiles + setMaxFileSize(fileSizeValueOfOrDefault(maxFileSize, "10mb")) + setTotalSizeCap(fileSizeValueOfOrDefault(maxTotalSize, "100mb")) start() } @@ -66,16 +82,44 @@ private fun getLogger(name: String): ch.qos.logback.classic.Logger { return context.getLogger(name) } -fun initLoggerConfig(appRootPath: String) { +fun initLoggerConfig( + appRootPath: String, + maxFiles: Int, + maxFileSize: String, + maxTotalSize: String, +) { val context = LoggerFactory.getILoggerFactory() as LoggerContext val logger = getBaseLogger() + // logback logs to the console by default (at least when adding a console appender logs in the console are duplicated) - logger.addAppender(createRollingFileAppender(context, "$appRootPath/logs")) + logger.addAppender(createRollingFileAppender(context, "$appRootPath/logs", maxFiles, maxFileSize, maxTotalSize)) // set "kotlin exposed" log level setLogLevelFor("Exposed", Level.ERROR) } +fun updateFileAppender( + maxFiles: Int, + maxFileSize: String, + maxTotalSize: String, +) { + val logger = getBaseLogger() + + val appender = logger.getAppender(FILE_APPENDER_NAME) as RollingFileAppender<*>? ?: return + val rollingPolicy = appender.rollingPolicy as SizeAndTimeBasedRollingPolicy<*> + rollingPolicy.apply { + maxHistory = maxFiles + setMaxFileSize(FileSize.valueOf(maxFileSize)) + setTotalSizeCap(FileSize.valueOf(maxTotalSize)) + + rollingPolicy.stop() + appender.stop() + + rollingPolicy.start() + appender.start() + } +} + const val BASE_LOGGER_NAME = "_BaseLogger" fun setLogLevelFor( diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt index 325f749f..58755948 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt @@ -9,6 +9,24 @@ import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.serverConfig import xyz.nulldev.ts.config.GlobalConfigManager +private fun validateString( + value: String?, + pattern: Regex, + name: String, +) { + validateString(value, pattern, Exception("Invalid format for \"$name\" [$value]")) +} + +private fun validateString( + value: String?, + pattern: Regex, + exception: Exception, +) { + if (value != null && !value.matches(pattern)) { + throw exception + } +} + class SettingsMutation { data class SetSettingsInput( val clientMutationId: String? = null, @@ -20,6 +38,13 @@ class SettingsMutation { val settings: SettingsType, ) + private fun validateSettings(settings: Settings) { + // misc + val logbackSizePattern = "^[0-9]+(|kb|KB|mb|MB|gb|GB)\$".toRegex() + validateString(settings.maxLogFileSize, logbackSizePattern, "maxLogFileSize") + validateString(settings.maxLogFolderSize, logbackSizePattern, "maxLogFolderSize") + } + private fun updateSetting( newSetting: SettingType?, configSetting: MutableStateFlow, @@ -82,6 +107,9 @@ class SettingsMutation { updateSetting(settings.debugLogsEnabled, serverConfig.debugLogsEnabled) updateSetting(settings.gqlDebugLogsEnabled, serverConfig.gqlDebugLogsEnabled) updateSetting(settings.systemTrayEnabled, serverConfig.systemTrayEnabled) + updateSetting(settings.maxLogFiles, serverConfig.maxLogFiles) + updateSetting(settings.maxLogFileSize, serverConfig.maxLogFileSize) + updateSetting(settings.maxLogFolderSize, serverConfig.maxLogFolderSize) // backup updateSetting(settings.backupPath, serverConfig.backupPath) @@ -104,6 +132,7 @@ class SettingsMutation { fun setSettings(input: SetSettingsInput): SetSettingsPayload { val (clientMutationId, settings) = input + validateSettings(settings) updateSettings(settings) return SetSettingsPayload(clientMutationId, SettingsType()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt index 8e0c0978..5953c6f2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt @@ -73,6 +73,9 @@ interface Settings : Node { val debugLogsEnabled: Boolean? val gqlDebugLogsEnabled: Boolean? val systemTrayEnabled: Boolean? + val maxLogFiles: Int? + val maxLogFileSize: String? + val maxLogFolderSize: String? // backup val backupPath: String? @@ -139,6 +142,9 @@ data class PartialSettingsType( override val debugLogsEnabled: Boolean?, override val gqlDebugLogsEnabled: Boolean?, override val systemTrayEnabled: Boolean?, + override val maxLogFiles: Int?, + override val maxLogFileSize: String?, + override val maxLogFolderSize: String?, // backup override val backupPath: String?, override val backupTime: String?, @@ -202,6 +208,9 @@ class SettingsType( override val debugLogsEnabled: Boolean, override val gqlDebugLogsEnabled: Boolean, override val systemTrayEnabled: Boolean, + override val maxLogFiles: Int, + override val maxLogFileSize: String, + override val maxLogFolderSize: String, // backup override val backupPath: String, override val backupTime: String, @@ -260,6 +269,9 @@ class SettingsType( config.debugLogsEnabled.value, config.gqlDebugLogsEnabled.value, config.systemTrayEnabled.value, + config.maxLogFiles.value, + config.maxLogFileSize.value, + config.maxLogFolderSize.value, // backup config.backupPath.value, config.backupTime.value, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt index 71655d57..706f0d6f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt @@ -125,6 +125,9 @@ class ServerConfig(getConfig: () -> Config, val moduleName: String = SERVER_CONF val debugLogsEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) val gqlDebugLogsEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) val systemTrayEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val maxLogFiles: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) + val maxLogFileSize: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val maxLogFolderSize: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) // backup val backupPath: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index 5874083d..5801f701 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -45,6 +45,7 @@ import xyz.nulldev.ts.config.ConfigKodeinModule import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.initLoggerConfig import xyz.nulldev.ts.config.setLogLevelFor +import xyz.nulldev.ts.config.updateFileAppender import java.io.File import java.net.Authenticator import java.net.PasswordAuthentication @@ -97,7 +98,28 @@ fun applicationSetup() { // Application dirs val applicationDirs = ApplicationDirs() - initLoggerConfig(applicationDirs.dataRoot) + initLoggerConfig( + applicationDirs.dataRoot, + serverConfig.maxLogFiles.value, + serverConfig.maxLogFileSize.value, + serverConfig.maxLogFolderSize.value, + ) + + serverConfig.subscribeTo( + combine( + serverConfig.maxLogFiles, + serverConfig.maxLogFileSize, + serverConfig.maxLogFolderSize, + ) { maxLogFiles, maxLogFileSize, maxLogFolderSize -> + Triple(maxLogFiles, maxLogFileSize, maxLogFolderSize) + }.distinctUntilChanged(), + { (maxLogFiles, maxLogFileSize, maxLogFolderSize) -> + logger.debug { + "updateFileAppender: maxLogFiles= $maxLogFiles, maxLogFileSize= $maxLogFileSize, maxLogFolderSize= $maxLogFolderSize" + } + updateFileAppender(maxLogFiles, maxLogFileSize, maxLogFolderSize) + }, + ) setupLogLevelUpdating(serverConfig.debugLogsEnabled, listOf(BASE_LOGGER_NAME)) // gql "ExecutionStrategy" spams logs with "... completing field ..." diff --git a/server/src/main/resources/server-reference.conf b/server/src/main/resources/server-reference.conf index dee08efb..8265e626 100644 --- a/server/src/main/resources/server-reference.conf +++ b/server/src/main/resources/server-reference.conf @@ -51,6 +51,9 @@ server.basicAuthPassword = "" server.debugLogsEnabled = false server.gqlDebugLogsEnabled = false # this includes logs with non privacy safe information server.systemTrayEnabled = true +server.maxLogFiles = 31 # the max number of days to keep files before they get deleted +server.maxLogFileSize = "10mb" # the max size of a log file - possible values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes) +server.maxLogFolderSize = "100mb" # the max size of all saved log files - possible values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes) # backup server.backupPath = ""