log: add --windows-event-log-level to support Windows Event Log

This provides JSON logs in the Windows Event Log.
This commit is contained in:
Nick Craig-Wood 2025-05-08 16:39:27 +01:00
parent dfa4d94827
commit 15510c66d4
5 changed files with 156 additions and 5 deletions

View File

@ -1515,6 +1515,33 @@ warnings and significant events.
`ERROR` is equivalent to `-q`. It only outputs error messages. `ERROR` is equivalent to `-q`. It only outputs error messages.
### --windows-event-log LEVEL ###
If this is configured (the default is `OFF`) then logs of this level
and above will be logged to the Windows event log in **addition** to
the normal logs. These will be logged in JSON format as described
below regardless of what format the main logs are configured for.
The Windows event log only has 3 levels of severity `Info`, `Warning`
and `Error`. If enabled we map rclone levels like this.
- `Error``ERROR` (and above)
- `Warning``WARNING` (note that this level is defined but not currently used).
- `Info``NOTICE`, `INFO` and `DEBUG`.
Rclone will declare its log source as "rclone" if it is has enough
permissions to create the registry key needed. If not then logs will
appear as "Application". You can run `rclone version --windows-event-log DEBUG`
once as administrator to create the registry key in advance.
**Note** that the `--windows-event-log` level must be greater (more
severe) than or equal to the `--log-level`. For example to log DEBUG
to a log file but ERRORs to the event log you would use
--log-file rclone.log --log-level DEBUG --windows-event-log ERROR
This option is only supported Windows platforms.
### --use-json-log ### ### --use-json-log ###
This switches the log format to JSON for rclone. The fields of JSON This switches the log format to JSON for rclone. The fields of JSON

View File

@ -32,6 +32,7 @@ const (
LogLevelNotice // Normal logging, -q suppresses LogLevelNotice // Normal logging, -q suppresses
LogLevelInfo // Transfers, needs -v LogLevelInfo // Transfers, needs -v
LogLevelDebug // Debug level, needs -vv LogLevelDebug // Debug level, needs -vv
LogLevelOff
) )
type logLevelChoices struct{} type logLevelChoices struct{}
@ -46,6 +47,7 @@ func (logLevelChoices) Choices() []string {
LogLevelNotice: "NOTICE", LogLevelNotice: "NOTICE",
LogLevelInfo: "INFO", LogLevelInfo: "INFO",
LogLevelDebug: "DEBUG", LogLevelDebug: "DEBUG",
LogLevelOff: "OFF",
} }
} }
@ -66,6 +68,7 @@ const (
SlogLevelCritical = slog.Level(12) // More severe than Error SlogLevelCritical = slog.Level(12) // More severe than Error
SlogLevelAlert = slog.Level(16) // More severe than Critical SlogLevelAlert = slog.Level(16) // More severe than Critical
SlogLevelEmergency = slog.Level(20) // Most severe SlogLevelEmergency = slog.Level(20) // Most severe
SlogLevelOff = slog.Level(24) // A very high value
) )
// Map our level numbers to slog level numbers // Map our level numbers to slog level numbers
@ -78,6 +81,7 @@ var levelToSlog = []slog.Level{
LogLevelNotice: SlogLevelNotice, LogLevelNotice: SlogLevelNotice,
LogLevelInfo: slog.LevelInfo, LogLevelInfo: slog.LevelInfo,
LogLevelDebug: slog.LevelDebug, LogLevelDebug: slog.LevelDebug,
LogLevelOff: SlogLevelOff,
} }
// LogValueItem describes keyed item for a JSON log entry // LogValueItem describes keyed item for a JSON log entry

15
fs/log/event_log.go Normal file
View File

@ -0,0 +1,15 @@
// Windows event logging stubs for non windows machines
//go:build !windows
package log
import (
"fmt"
"runtime"
)
// Starts windows event log if configured.
func startWindowsEventLog(*OutputHandler) error {
return fmt.Errorf("windows event log not supported on %s platform", runtime.GOOS)
}

View File

@ -0,0 +1,79 @@
// Windows event logging
//go:build windows
package log
import (
"fmt"
"log/slog"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/lib/atexit"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc/eventlog"
)
const (
errorID = uint32(windows.ERROR_INTERNAL_ERROR)
infoID = uint32(windows.ERROR_SUCCESS)
sourceName = "rclone"
)
var (
windowsEventLog *eventlog.Log
)
func startWindowsEventLog(handler *OutputHandler) error {
// Don't install Windows event log if it is disabled.
if Opt.WindowsEventLogLevel == fs.LogLevelOff {
return nil
}
// Install the event source - we don't care if this fails as Windows has sensible fallbacks.
_ = eventlog.InstallAsEventCreate(sourceName, eventlog.Info|eventlog.Warning|eventlog.Error)
// Open the event log
// If sourceName didn't get registered then Windows will use "Application" instead which is fine.
// Though in my tests it seemsed to use sourceName regardless.
elog, err := eventlog.Open(sourceName)
if err != nil {
return fmt.Errorf("open event log: %w", err)
}
// Set the global for the handler
windowsEventLog = elog
// Close it on exit
atexit.Register(func() {
err := elog.Close()
if err != nil {
fs.Errorf(nil, "Failed to close Windows event log: %v", err)
}
})
// Add additional JSON logging to the eventLog handler.
handler.AddOutput(true, eventLog)
fs.Infof(nil, "Logging to Windows event log at level %v", Opt.WindowsEventLogLevel)
return nil
}
// We use levels ERROR, NOTICE, INFO, DEBUG
// Need to map to ERROR, WARNING, INFO
func eventLog(level slog.Level, text string) {
// Check to see if this level is required
if level < fs.LogLevelToSlog(Opt.WindowsEventLogLevel) {
return
}
// Now log to windows eventLog
switch level {
case fs.SlogLevelEmergency, fs.SlogLevelAlert, fs.SlogLevelCritical, slog.LevelError:
_ = windowsEventLog.Error(errorID, text)
case slog.LevelWarn:
_ = windowsEventLog.Warning(infoID, text)
case fs.SlogLevelNotice, slog.LevelInfo, slog.LevelDebug:
_ = windowsEventLog.Info(infoID, text)
}
}

View File

@ -3,6 +3,7 @@ package log
import ( import (
"context" "context"
"fmt"
"io" "io"
"os" "os"
"reflect" "reflect"
@ -38,15 +39,27 @@ var OptionsInfo = fs.Options{{
Default: false, Default: false,
Help: "Activate systemd integration for the logger", Help: "Activate systemd integration for the logger",
Groups: "Logging", 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 // Options contains options for controlling the logging
type Options struct { type Options struct {
File string `config:"log_file"` // Log everything to this file File string `config:"log_file"` // Log everything to this file
Format logFormat `config:"log_format"` // Comma separated list of log format options Format logFormat `config:"log_format"` // Comma separated list of log format options
UseSyslog bool `config:"syslog"` // Use Syslog for logging UseSyslog bool `config:"syslog"` // Use Syslog for logging
SyslogFacility string `config:"syslog_facility"` // Facility for syslog, e.g. KERN,USER,... SyslogFacility string `config:"syslog_facility"` // Facility for syslog, e.g. KERN,USER,...
LogSystemdSupport bool `config:"log_systemd"` // set if using systemd logging LogSystemdSupport bool `config:"log_systemd"` // set if using systemd logging
WindowsEventLogLevel fs.LogLevel `config:"windows_event_log_level"`
} }
func init() { func init() {
@ -149,6 +162,11 @@ func Stack(o any, info string) {
// externally visible in the rc. // externally visible in the rc.
func logReload(ci *fs.ConfigInfo) error { func logReload(ci *fs.ConfigInfo) error {
Handler.SetLevel(fs.LogLevelToSlog(ci.LogLevel)) 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 return nil
} }
@ -207,6 +225,14 @@ func InitLogging() {
if Opt.LogSystemdSupport { if Opt.LogSystemdSupport {
startSystemdLog(Handler) 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 // Redirected returns true if the log has been redirected from stdout