add lumberjack log rotation (#5696)

* [logging] add UI and graphql for maximum log size
* [logger] set default size to 0MB and don't rotate
This commit is contained in:
feederbox826 2025-11-17 22:04:22 -05:00 committed by GitHub
parent 2f65a1da3e
commit 78aeb06f20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 64 additions and 19 deletions

View file

@ -110,7 +110,7 @@ func main() {
// Logs only error level message to stderr. // Logs only error level message to stderr.
func initLogTemp() *log.Logger { func initLogTemp() *log.Logger {
l := log.NewLogger() l := log.NewLogger()
l.Init("", true, "Error") l.Init("", true, "Error", 0)
logger.Logger = l logger.Logger = l
return l return l
@ -118,7 +118,7 @@ func initLogTemp() *log.Logger {
func initLog(cfg *config.Config) *log.Logger { func initLog(cfg *config.Config) *log.Logger {
l := log.NewLogger() l := log.NewLogger()
l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel()) l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel(), cfg.GetLogFileMaxSize())
logger.Logger = l logger.Logger = l
return l return l

1
go.mod
View file

@ -63,6 +63,7 @@ require (
golang.org/x/text v0.25.0 golang.org/x/text v0.25.0
golang.org/x/time v0.10.0 golang.org/x/time v0.10.0
gopkg.in/guregu/null.v4 v4.0.0 gopkg.in/guregu/null.v4 v4.0.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )

2
go.sum
View file

@ -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.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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -155,6 +155,8 @@ input ConfigGeneralInput {
logLevel: String logLevel: String
"Whether to log http access" "Whether to log http access"
logAccess: Boolean logAccess: Boolean
"Maximum log size"
logFileMaxSize: Int
"True if galleries should be created from folders with images" "True if galleries should be created from folders with images"
createGalleriesFromFolders: Boolean createGalleriesFromFolders: Boolean
"Regex used to identify images as gallery covers" "Regex used to identify images as gallery covers"
@ -279,6 +281,8 @@ type ConfigGeneralResult {
logLevel: String! logLevel: String!
"Whether to log http access" "Whether to log http access"
logAccess: Boolean! logAccess: Boolean!
"Maximum log size"
logFileMaxSize: Int!
"Array of video file extensions" "Array of video file extensions"
videoExtensions: [String!]! videoExtensions: [String!]!
"Array of image file extensions" "Array of image file extensions"

View file

@ -334,6 +334,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
logger.SetLogLevel(*input.LogLevel) logger.SetLogLevel(*input.LogLevel)
} }
if input.LogFileMaxSize != nil && *input.LogFileMaxSize != c.GetLogFileMaxSize() {
c.SetInt(config.LogFileMaxSize, *input.LogFileMaxSize)
}
if input.Excludes != nil { if input.Excludes != nil {
for _, r := range input.Excludes { for _, r := range input.Excludes {
_, err := regexp.Compile(r) _, err := regexp.Compile(r)

View file

@ -115,6 +115,7 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
LogOut: config.GetLogOut(), LogOut: config.GetLogOut(),
LogLevel: config.GetLogLevel(), LogLevel: config.GetLogLevel(),
LogAccess: config.GetLogAccess(), LogAccess: config.GetLogAccess(),
LogFileMaxSize: config.GetLogFileMaxSize(),
VideoExtensions: config.GetVideoExtensions(), VideoExtensions: config.GetVideoExtensions(),
ImageExtensions: config.GetImageExtensions(), ImageExtensions: config.GetImageExtensions(),
GalleryExtensions: config.GetGalleryExtensions(), GalleryExtensions: config.GetGalleryExtensions(),

View file

@ -3,12 +3,14 @@ package log
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
) )
type LogItem struct { type LogItem struct {
@ -41,8 +43,8 @@ func NewLogger() *Logger {
} }
// Init initialises the logger based on a logging configuration // Init initialises the logger based on a logging configuration
func (log *Logger) Init(logFile string, logOut bool, logLevel string) { func (log *Logger) Init(logFile string, logOut bool, logLevel string, logFileMaxSize int) {
var file *os.File var logger io.WriteCloser
customFormatter := new(logrus.TextFormatter) customFormatter := new(logrus.TextFormatter)
customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.TimestampFormat = "2006-01-02 15:04:05"
customFormatter.ForceColors = true customFormatter.ForceColors = true
@ -57,30 +59,38 @@ func (log *Logger) Init(logFile string, logOut bool, logLevel string) {
// the access log colouring not being applied // the access log colouring not being applied
_, _ = customFormatter.Format(logrus.NewEntry(log.logger)) _, _ = customFormatter.Format(logrus.NewEntry(log.logger))
// if size is 0, disable rotation
if logFile != "" { if logFile != "" {
var err error if logFileMaxSize == 0 {
file, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) var err error
logger, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil { if err != nil {
fmt.Printf("Could not open '%s' for log output due to error: %s\n", logFile, err.Error()) 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 { if logOut {
// log to file separately disabling colours // log to file separately disabling colours
fileFormatter := new(logrus.TextFormatter) fileFormatter := new(logrus.TextFormatter)
fileFormatter.TimestampFormat = customFormatter.TimestampFormat fileFormatter.TimestampFormat = customFormatter.TimestampFormat
fileFormatter.FullTimestamp = customFormatter.FullTimestamp fileFormatter.FullTimestamp = customFormatter.FullTimestamp
log.logger.AddHook(&fileLogHook{ log.logger.AddHook(&fileLogHook{
Writer: file, Writer: logger,
Formatter: fileFormatter, Formatter: fileFormatter,
}) })
} else { } else {
// logging to file only // logging to file only
// turn off the colouring for the file // turn off the colouring for the file
customFormatter.ForceColors = false customFormatter.ForceColors = false
log.logger.Out = file log.logger.Out = logger
} }
} }

View file

@ -252,13 +252,15 @@ const (
DLNAPortDefault = 1338 DLNAPortDefault = 1338
// Logging options // Logging options
LogFile = "logfile" LogFile = "logfile"
LogOut = "logout" LogOut = "logout"
defaultLogOut = true defaultLogOut = true
LogLevel = "loglevel" LogLevel = "loglevel"
defaultLogLevel = "Info" defaultLogLevel = "Info"
LogAccess = "logaccess" LogAccess = "logaccess"
defaultLogAccess = true defaultLogAccess = true
LogFileMaxSize = "logfile_max_size"
defaultLogFileMaxSize = 0 // megabytes, default disabled
// Default settings // Default settings
DefaultScanSettings = "defaults.scan_task" DefaultScanSettings = "defaults.scan_task"
@ -1636,6 +1638,16 @@ func (i *Config) GetLogAccess() bool {
return i.getBoolDefault(LogAccess, defaultLogAccess) 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 // Max allowed graphql upload size in megabytes
func (i *Config) GetMaxUploadSize() int64 { func (i *Config) GetMaxUploadSize() int64 {
i.RLock() i.RLock()

View file

@ -37,6 +37,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
logOut logOut
logLevel logLevel
logAccess logAccess
logFileMaxSize
createGalleriesFromFolders createGalleriesFromFolders
galleryCoverRegex galleryCoverRegex
videoExtensions videoExtensions

View file

@ -465,6 +465,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
checked={general.logAccess ?? false} checked={general.logAccess ?? false}
onChange={(v) => saveGeneral({ logAccess: v })} onChange={(v) => saveGeneral({ logAccess: v })}
/> />
<NumberSetting
id="log-file-max-size"
headingID="config.general.auth.log_file_max_size"
subHeadingID="config.general.auth.log_file_max_size_desc"
value={general.logFileMaxSize ?? 10}
onChange={(v) => saveGeneral({ logFileMaxSize: v })}
/>
</SettingSection> </SettingSection>
</> </>
); );

View file

@ -301,6 +301,8 @@
"log_http_desc": "Logs http access to the terminal. Requires restart.", "log_http_desc": "Logs http access to the terminal. Requires restart.",
"log_to_terminal": "Log to terminal", "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_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": "Maximum Session Age",
"maximum_session_age_desc": "Maximum idle time before a login session is expired, in seconds. Requires restart.", "maximum_session_age_desc": "Maximum idle time before a login session is expired, in seconds. Requires restart.",
"password": "Password", "password": "Password",