Files
rclone/fs/log/log.go
Sven Rebhan b8b3346499 log: fix side effects when importing rclone as a library
Avoid side effects by using own logger instance

- Importing fs/log only sets rclone's private logger via fs.SetLogger,
  so internal rclone logging works from the moment the package is
  imported but the process-wide slog default is left untouched.

- slog.SetDefault and slog.SetLogLoggerLevel move into InitLogging,
  which is called explicitly from the CLI (cmd/cmd.go), the librclone
  wrapper and the integration test framework. So rclone-as-a-program
  keeps capturing log.Print/log.Fatal and slog.Default() output as
  before.

Library consumers that import fs/log without calling InitLogging now
keep their own slog default and can safely route rclone output back
into it via log.Handler.SetOutput without recursing.

Fixes #8907

Co-authored-by: Nick Craig-Wood <nick@craig-wood.com>
2026-05-04 11:06:30 +01:00

305 lines
8.4 KiB
Go

// Package log provides logging for rclone
package log
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"reflect"
"runtime"
"strings"
"time"
"github.com/rclone/rclone/fs"
"gopkg.in/natefinch/lumberjack.v2"
)
// OptionsInfo descripts the Options in use
var OptionsInfo = fs.Options{{
Name: "log_file",
Default: "",
Help: "Log everything to this file",
Groups: "Logging",
}, {
Name: "log_file_max_size",
Default: fs.SizeSuffix(-1),
Help: `Maximum size of the log file before it's rotated (eg "10M")`,
Groups: "Logging",
}, {
Name: "log_file_max_backups",
Default: 0,
Help: "Maximum number of old log files to retain.",
Groups: "Logging",
}, {
Name: "log_file_max_age",
Default: fs.Duration(0),
Help: `Maximum duration to retain old log files (eg "7d")`,
Groups: "Logging",
}, {
Name: "log_file_compress",
Default: false,
Help: "If set, compress rotated log files using gzip.",
Groups: "Logging",
}, {
Name: "log_format",
Default: logFormatDate | logFormatTime,
Help: "Comma separated list of log format options",
Groups: "Logging",
}, {
Name: "syslog",
Default: false,
Help: "Use Syslog for logging",
Groups: "Logging",
}, {
Name: "syslog_facility",
Default: "DAEMON",
Help: "Facility for syslog, e.g. KERN,USER",
Groups: "Logging",
}, {
Name: "log_systemd",
Default: false,
Help: "Activate systemd integration for the logger",
Groups: "Logging",
}, {
Name: "windows_event_log_level",
Default: fs.LogLevelOff,
Help: "Windows Event Log level DEBUG|INFO|NOTICE|ERROR|OFF",
Groups: "Logging",
Hide: func() fs.OptionVisibility {
if runtime.GOOS == "windows" {
return 0
}
return fs.OptionHideBoth
}(),
}}
// Options contains options for controlling the logging
type Options struct {
File string `config:"log_file"` // Log everything to this file
MaxSize fs.SizeSuffix `config:"log_file_max_size"` // Max size of log file
MaxBackups int `config:"log_file_max_backups"` // Max backups of log file
MaxAge fs.Duration `config:"log_file_max_age"` // Max age of log file
Compress bool `config:"log_file_compress"` // Set to compress log file
Format logFormat `config:"log_format"` // Comma separated list of log format options
UseSyslog bool `config:"syslog"` // Use Syslog for logging
SyslogFacility string `config:"syslog_facility"` // Facility for syslog, e.g. KERN,USER,...
LogSystemdSupport bool `config:"log_systemd"` // set if using systemd logging
WindowsEventLogLevel fs.LogLevel `config:"windows_event_log_level"`
}
func init() {
fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "log", Opt: &Opt, Options: OptionsInfo})
}
// Opt is the options for the logger
var Opt Options
// enum for the log format
type logFormat = fs.Bits[logFormatChoices]
const (
logFormatDate logFormat = 1 << iota
logFormatTime
logFormatMicroseconds
logFormatUTC
logFormatLongFile
logFormatShortFile
logFormatPid
logFormatNoLevel
logFormatJSON
)
type logFormatChoices struct{}
func (logFormatChoices) Choices() []fs.BitsChoicesInfo {
return []fs.BitsChoicesInfo{
{Bit: uint64(logFormatDate), Name: "date"},
{Bit: uint64(logFormatTime), Name: "time"},
{Bit: uint64(logFormatMicroseconds), Name: "microseconds"},
{Bit: uint64(logFormatUTC), Name: "UTC"},
{Bit: uint64(logFormatLongFile), Name: "longfile"},
{Bit: uint64(logFormatShortFile), Name: "shortfile"},
{Bit: uint64(logFormatPid), Name: "pid"},
{Bit: uint64(logFormatNoLevel), Name: "nolevel"},
{Bit: uint64(logFormatJSON), Name: "json"},
}
}
// fnName returns the name of the calling +2 function
func fnName() string {
pc, _, _, ok := runtime.Caller(2)
name := "*Unknown*"
if ok {
name = runtime.FuncForPC(pc).Name()
dot := strings.LastIndex(name, ".")
if dot >= 0 {
name = name[dot+1:]
}
}
return name
}
// Trace debugs the entry and exit of the calling function
//
// It is designed to be used in a defer statement, so it returns a
// function that logs the exit parameters.
//
// Any pointers in the exit function will be dereferenced
func Trace(o any, format string, a ...any) func(string, ...any) {
if fs.GetConfig(context.Background()).LogLevel < fs.LogLevelDebug {
return func(format string, a ...any) {}
}
name := fnName()
fs.LogPrintf(fs.LogLevelDebug, o, name+": "+format, a...)
return func(format string, a ...any) {
for i := range a {
// read the values of the pointed to items
typ := reflect.TypeOf(a[i])
if typ.Kind() == reflect.Pointer {
value := reflect.ValueOf(a[i])
if value.IsNil() {
a[i] = nil
} else {
pointedToValue := reflect.Indirect(value)
a[i] = pointedToValue.Interface()
}
}
}
fs.LogPrintf(fs.LogLevelDebug, o, ">"+name+": "+format, a...)
}
}
// Stack logs a stack trace of callers with the o and info passed in
func Stack(o any, info string) {
if fs.GetConfig(context.Background()).LogLevel < fs.LogLevelDebug {
return
}
arr := [16 * 1024]byte{}
buf := arr[:]
n := runtime.Stack(buf, false)
buf = buf[:n]
fs.LogPrintf(fs.LogLevelDebug, o, "%s\nStack trace:\n%s", info, buf)
}
// This is called from fs when the config is reloaded
//
// The config should really be here but we can't move it as it is
// externally visible in the rc.
func logReload(ci *fs.ConfigInfo) error {
Handler.SetLevel(fs.LogLevelToSlog(ci.LogLevel))
if Opt.WindowsEventLogLevel != fs.LogLevelOff && Opt.WindowsEventLogLevel > ci.LogLevel {
return fmt.Errorf("--windows-event-log-level %q must be >= --log-level %q", Opt.WindowsEventLogLevel, ci.LogLevel)
}
return nil
}
func init() {
fs.LogReload = logReload
}
// InitLogging start the logging as per the command line flags
//
// This is called explicitly from the CLI, the librclone wrapper and
// the test framework, but not from package init, so that importing
// rclone as a library has no side effects on the process-wide default
// slog logger.
func InitLogging() {
// Redirect the process-wide default logger through rclone's
// handler so that log.Print/log.Fatal and slog.Default() (used by
// some standard library and third party code) end up in rclone's
// log output.
slog.SetDefault(slog.New(Handler))
// Make log.Printf logs at level Notice
slog.SetLogLoggerLevel(fs.SlogLevelNotice)
// Note that ci only has the defaults in at this point
// We set real values in logReload
ci := fs.GetConfig(context.Background())
// Log file output
if Opt.File != "" {
var w io.Writer
if Opt.MaxSize < 0 {
// No log rotation - just open the file as normal
// We'll capture tracebacks like this too.
f, err := os.OpenFile(Opt.File, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640)
if err != nil {
fs.Fatalf(nil, "Failed to open log file: %v", err)
}
redirectStderr(f)
w = f
} else {
// Round with a minimum of 1 if set
round := func(x float64) int {
if x <= 0 {
return 0
} else if x <= 1 {
return 1
}
return int(x + 0.5)
}
// Log rotation active
f := &lumberjack.Logger{
Filename: Opt.File,
MaxSize: round(float64(Opt.MaxSize) / float64(fs.Mebi)), // MiB
MaxBackups: Opt.MaxBackups,
MaxAge: round(time.Duration(Opt.MaxAge).Hours() / 24), // Days
Compress: Opt.Compress,
LocalTime: true, // format log file names in localtime
}
w = f
}
Handler.setWriter(w)
}
// --use-json-log implies JSON formatting
if ci.UseJSONLog {
Opt.Format |= logFormatJSON
}
// Set slog level to initial log level
Handler.SetLevel(fs.LogLevelToSlog(fs.InitialLogLevel()))
// Set the format to the configured format
Handler.setFormat(Opt.Format)
// Syslog output
if Opt.UseSyslog {
if Opt.File != "" {
fs.Fatalf(nil, "Can't use --syslog and --log-file together")
}
startSysLog(Handler)
}
// Activate systemd logger support if systemd invocation ID is
// detected and output is going to stderr (not logging to a file or syslog)
if !Redirected() {
if isJournalStream() {
Opt.LogSystemdSupport = true
}
}
// Systemd logging output
if Opt.LogSystemdSupport {
startSystemdLog(Handler)
}
// Windows event logging
if Opt.WindowsEventLogLevel != fs.LogLevelOff {
err := startWindowsEventLog(Handler)
if err != nil {
fs.Fatalf(nil, "Failed to start windows event log: %v", err)
}
}
}
// Redirected returns true if the log has been redirected from stdout
func Redirected() bool {
return Opt.UseSyslog || Opt.File != ""
}