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>
This commit is contained in:
Sven Rebhan
2026-05-04 12:06:30 +02:00
committed by GitHub
parent 9f89102a57
commit b8b3346499
3 changed files with 33 additions and 6 deletions

View File

@@ -12,6 +12,10 @@ import (
"github.com/rclone/rclone/lib/caller"
)
// logger represents the slog logging facility and should be overridden by
// the fs/log handling code.
var logger *slog.Logger = slog.Default()
// LogLevel describes rclone's logs. These are a subset of the syslog log levels.
type LogLevel = Enum[logLevelChoices]
@@ -137,7 +141,7 @@ func LogLevelToSlog(level LogLevel) slog.Level {
}
func logSlog(level LogLevel, text string, attrs []any) {
slog.Log(context.Background(), LogLevelToSlog(level), text, attrs...)
logger.Log(context.Background(), LogLevelToSlog(level), text, attrs...)
}
func logSlogWithObject(level LogLevel, o any, text string, attrs []any) {
@@ -337,3 +341,8 @@ func PrettyPrint(in any, label string, level LogLevel) {
}
LogPrintf(level, label, "\n%s\n", string(inBytes))
}
// SetLogger overrides the slog logger using the specified handler
func SetLogger(h slog.Handler) {
logger = slog.New(h)
}

View File

@@ -5,6 +5,7 @@ import (
"context"
"fmt"
"io"
"log/slog"
"os"
"reflect"
"runtime"
@@ -201,7 +202,21 @@ func init() {
}
// 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())

View File

@@ -27,6 +27,12 @@ var Handler = defaultHandler()
// This will be adjusted by InitLogging to be the configured levels
// but it is important we have a logger running regardless of whether
// InitLogging has been called yet or not.
//
// Note that this only sets rclone's private logger via fs.SetLogger
// so that importing this package has no side effects on the
// process-wide default slog logger. The CLI and other rclone-as-a-
// program entry points pick up the default logger redirection in
// InitLogging instead.
func defaultHandler() *OutputHandler {
// Default options for default handler
opts := &slog.HandlerOptions{
@@ -36,11 +42,8 @@ func defaultHandler() *OutputHandler {
// Create our handler
h := NewOutputHandler(os.Stderr, opts, logFormatDate|logFormatTime)
// Set the slog default handler
slog.SetDefault(slog.New(h))
// Make log.Printf logs at level Notice
slog.SetLogLoggerLevel(fs.SlogLevelNotice)
// Set rclone's internal logger so rclone logging works
fs.SetLogger(h)
return h
}