diff --git a/cmd/stash/main.go b/cmd/stash/main.go index 86edd6276..e3a54f020 100644 --- a/cmd/stash/main.go +++ b/cmd/stash/main.go @@ -110,7 +110,7 @@ func main() { // Logs only error level message to stderr. func initLogTemp() *log.Logger { l := log.NewLogger() - l.Init("", true, "Error") + l.Init("", true, "Error", 0) logger.Logger = l return l @@ -118,7 +118,7 @@ func initLogTemp() *log.Logger { func initLog(cfg *config.Config) *log.Logger { l := log.NewLogger() - l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel()) + l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel(), cfg.GetLogFileMaxSize()) logger.Logger = l return l diff --git a/go.mod b/go.mod index bf2eb0f6e..4d6b78dc6 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( golang.org/x/text v0.25.0 golang.org/x/time v0.10.0 gopkg.in/guregu/null.v4 v4.0.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index dced0768f..bc84b1f23 100644 --- a/go.sum +++ b/go.sum @@ -1122,6 +1122,8 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.3/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 63ce3ea1c..6a1ac72be 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -155,6 +155,8 @@ input ConfigGeneralInput { logLevel: String "Whether to log http access" logAccess: Boolean + "Maximum log size" + logFileMaxSize: Int "True if galleries should be created from folders with images" createGalleriesFromFolders: Boolean "Regex used to identify images as gallery covers" @@ -279,6 +281,8 @@ type ConfigGeneralResult { logLevel: String! "Whether to log http access" logAccess: Boolean! + "Maximum log size" + logFileMaxSize: Int! "Array of video file extensions" videoExtensions: [String!]! "Array of image file extensions" diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index ba46a115a..3299c01a8 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -334,6 +334,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen logger.SetLogLevel(*input.LogLevel) } + if input.LogFileMaxSize != nil && *input.LogFileMaxSize != c.GetLogFileMaxSize() { + c.SetInt(config.LogFileMaxSize, *input.LogFileMaxSize) + } + if input.Excludes != nil { for _, r := range input.Excludes { _, err := regexp.Compile(r) diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index 5952dd41e..7213f8447 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -115,6 +115,7 @@ func makeConfigGeneralResult() *ConfigGeneralResult { LogOut: config.GetLogOut(), LogLevel: config.GetLogLevel(), LogAccess: config.GetLogAccess(), + LogFileMaxSize: config.GetLogFileMaxSize(), VideoExtensions: config.GetVideoExtensions(), ImageExtensions: config.GetImageExtensions(), GalleryExtensions: config.GetGalleryExtensions(), diff --git a/internal/log/logger.go b/internal/log/logger.go index 5f686d32d..cb07121a5 100644 --- a/internal/log/logger.go +++ b/internal/log/logger.go @@ -3,12 +3,14 @@ package log import ( "fmt" + "io" "os" "strings" "sync" "time" "github.com/sirupsen/logrus" + lumberjack "gopkg.in/natefinch/lumberjack.v2" ) type LogItem struct { @@ -41,8 +43,8 @@ func NewLogger() *Logger { } // Init initialises the logger based on a logging configuration -func (log *Logger) Init(logFile string, logOut bool, logLevel string) { - var file *os.File +func (log *Logger) Init(logFile string, logOut bool, logLevel string, logFileMaxSize int) { + var logger io.WriteCloser customFormatter := new(logrus.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.ForceColors = true @@ -57,30 +59,38 @@ func (log *Logger) Init(logFile string, logOut bool, logLevel string) { // the access log colouring not being applied _, _ = customFormatter.Format(logrus.NewEntry(log.logger)) + // if size is 0, disable rotation if logFile != "" { - var err error - file, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - - if err != nil { - fmt.Printf("Could not open '%s' for log output due to error: %s\n", logFile, err.Error()) + if logFileMaxSize == 0 { + var err error + logger, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to open log file %s: %v\n", logFile, err) + } + } else { + logger = &lumberjack.Logger{ + Filename: logFile, + MaxSize: logFileMaxSize, // Megabytes + Compress: true, + } } } - if file != nil { + if logger != nil { if logOut { // log to file separately disabling colours fileFormatter := new(logrus.TextFormatter) fileFormatter.TimestampFormat = customFormatter.TimestampFormat fileFormatter.FullTimestamp = customFormatter.FullTimestamp log.logger.AddHook(&fileLogHook{ - Writer: file, + Writer: logger, Formatter: fileFormatter, }) } else { // logging to file only // turn off the colouring for the file customFormatter.ForceColors = false - log.logger.Out = file + log.logger.Out = logger } } diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index a351cc872..eda863663 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -252,13 +252,15 @@ const ( DLNAPortDefault = 1338 // Logging options - LogFile = "logfile" - LogOut = "logout" - defaultLogOut = true - LogLevel = "loglevel" - defaultLogLevel = "Info" - LogAccess = "logaccess" - defaultLogAccess = true + LogFile = "logfile" + LogOut = "logout" + defaultLogOut = true + LogLevel = "loglevel" + defaultLogLevel = "Info" + LogAccess = "logaccess" + defaultLogAccess = true + LogFileMaxSize = "logfile_max_size" + defaultLogFileMaxSize = 0 // megabytes, default disabled // Default settings DefaultScanSettings = "defaults.scan_task" @@ -1636,6 +1638,16 @@ func (i *Config) GetLogAccess() bool { return i.getBoolDefault(LogAccess, defaultLogAccess) } +// GetLogFileMaxSize returns the maximum size of the log file in megabytes for lumberjack to rotate +func (i *Config) GetLogFileMaxSize() int { + value := i.getInt(LogFileMaxSize) + if value < 0 { + value = defaultLogFileMaxSize + } + + return value +} + // Max allowed graphql upload size in megabytes func (i *Config) GetMaxUploadSize() int64 { i.RLock() diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index 95d55864f..192fb8053 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -37,6 +37,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { logOut logLevel logAccess + logFileMaxSize createGalleriesFromFolders galleryCoverRegex videoExtensions diff --git a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx index a3ab150db..3baeca1e2 100644 --- a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx @@ -465,6 +465,14 @@ export const SettingsConfigurationPanel: React.FC = () => { checked={general.logAccess ?? false} onChange={(v) => saveGeneral({ logAccess: v })} /> + + saveGeneral({ logFileMaxSize: v })} + /> ); diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 1adcd7671..6a230736a 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -301,6 +301,8 @@ "log_http_desc": "Logs http access to the terminal. Requires restart.", "log_to_terminal": "Log to terminal", "log_to_terminal_desc": "Logs to the terminal in addition to a file. Always true if file logging is disabled. Requires restart.", + "log_file_max_size": "Maximum log size", + "log_file_max_size_desc": "Maximum size in megabytes of the log file before it is compressed. 0MB is disabled. Requires restart.", "maximum_session_age": "Maximum Session Age", "maximum_session_age_desc": "Maximum idle time before a login session is expired, in seconds. Requires restart.", "password": "Password",