From 15510c66d401e6737a2b15a0dba988e04ffb4ddd Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Thu, 8 May 2025 16:39:27 +0100 Subject: [PATCH] log: add --windows-event-log-level to support Windows Event Log This provides JSON logs in the Windows Event Log. --- docs/content/docs.md | 27 +++++++++++++ fs/log.go | 4 ++ fs/log/event_log.go | 15 +++++++ fs/log/event_log_windows.go | 79 +++++++++++++++++++++++++++++++++++++ fs/log/log.go | 36 ++++++++++++++--- 5 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 fs/log/event_log.go create mode 100644 fs/log/event_log_windows.go diff --git a/docs/content/docs.md b/docs/content/docs.md index 95ca98c65..4633429e0 100644 --- a/docs/content/docs.md +++ b/docs/content/docs.md @@ -1515,6 +1515,33 @@ warnings and significant events. `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 ### This switches the log format to JSON for rclone. The fields of JSON diff --git a/fs/log.go b/fs/log.go index 98350b021..2572fe612 100644 --- a/fs/log.go +++ b/fs/log.go @@ -32,6 +32,7 @@ const ( LogLevelNotice // Normal logging, -q suppresses LogLevelInfo // Transfers, needs -v LogLevelDebug // Debug level, needs -vv + LogLevelOff ) type logLevelChoices struct{} @@ -46,6 +47,7 @@ func (logLevelChoices) Choices() []string { LogLevelNotice: "NOTICE", LogLevelInfo: "INFO", LogLevelDebug: "DEBUG", + LogLevelOff: "OFF", } } @@ -66,6 +68,7 @@ const ( SlogLevelCritical = slog.Level(12) // More severe than Error SlogLevelAlert = slog.Level(16) // More severe than Critical SlogLevelEmergency = slog.Level(20) // Most severe + SlogLevelOff = slog.Level(24) // A very high value ) // Map our level numbers to slog level numbers @@ -78,6 +81,7 @@ var levelToSlog = []slog.Level{ LogLevelNotice: SlogLevelNotice, LogLevelInfo: slog.LevelInfo, LogLevelDebug: slog.LevelDebug, + LogLevelOff: SlogLevelOff, } // LogValueItem describes keyed item for a JSON log entry diff --git a/fs/log/event_log.go b/fs/log/event_log.go new file mode 100644 index 000000000..c20de7128 --- /dev/null +++ b/fs/log/event_log.go @@ -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) +} diff --git a/fs/log/event_log_windows.go b/fs/log/event_log_windows.go new file mode 100644 index 000000000..9c1a0ad2a --- /dev/null +++ b/fs/log/event_log_windows.go @@ -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) + } +} diff --git a/fs/log/log.go b/fs/log/log.go index b94f4d621..8e56a2d18 100644 --- a/fs/log/log.go +++ b/fs/log/log.go @@ -3,6 +3,7 @@ package log import ( "context" + "fmt" "io" "os" "reflect" @@ -38,15 +39,27 @@ var OptionsInfo = fs.Options{{ 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 - 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 + File string `config:"log_file"` // Log everything to this 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() { @@ -149,6 +162,11 @@ func Stack(o any, info string) { // 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 } @@ -207,6 +225,14 @@ func InitLogging() { 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