From 667180e6079888b69d7b2063d730e5f0b0489dc2 Mon Sep 17 00:00:00 2001 From: jkellerer Date: Tue, 19 Mar 2024 09:42:54 +0100 Subject: [PATCH] syslog: local syslog and stdout redirection (#344) --- config/global.go | 2 +- config/profile.go | 2 +- config/schedule.go | 2 +- dial/url.go | 38 ++++++++++++++---- dial/url_test.go | 21 +++++++--- docs/content/configuration/logs.md | 14 ++++++- logger.go | 2 +- syslog.go | 63 +++++++++++++++++++++++++++--- syslog_windows.go | 6 ++- wrapper.go | 4 +- 10 files changed, 125 insertions(+), 29 deletions(-) diff --git a/config/global.go b/config/global.go index 367cf5e1..06d35f2b 100644 --- a/config/global.go +++ b/config/global.go @@ -26,7 +26,7 @@ type Global struct { MinMemory uint64 `mapstructure:"min-memory" default:"100" description:"Minimum available memory (in MB) required to run any commands - see https://creativeprojects.github.io/resticprofile/usage/memory/"` Scheduler string `mapstructure:"scheduler" description:"Leave blank for the default scheduler or use \"crond\" to select cron on supported operating systems"` ScheduleDefaults *ScheduleBaseConfig `mapstructure:"schedule-defaults" default:"" description:"Sets defaults for all schedules"` - Log string `mapstructure:"log" default:"" description:"Sets the default log destination to be used if not specified in '--log' or 'schedule-log' - see https://creativeprojects.github.io/resticprofile/configuration/logs/"` + Log string `mapstructure:"log" default:"" examples:"/resticprofile.log;syslog-tcp://syslog-server:514;syslog:server;syslog:" description:"Sets the default log destination to be used if not specified in '--log' or 'schedule-log' - see https://creativeprojects.github.io/resticprofile/configuration/logs/"` LegacyArguments bool `mapstructure:"legacy-arguments" default:"false" deprecated:"0.20.0" description:"Legacy, broken arguments mode of resticprofile before version 0.15"` SystemdUnitTemplate string `mapstructure:"systemd-unit-template" default:"" description:"File containing the go template to generate a systemd unit - see https://creativeprojects.github.io/resticprofile/schedules/systemd/"` SystemdTimerTemplate string `mapstructure:"systemd-timer-template" default:"" description:"File containing the go template to generate a systemd timer - see https://creativeprojects.github.io/resticprofile/schedules/systemd/"` diff --git a/config/profile.go b/config/profile.go index e54398a3..e3c457fb 100644 --- a/config/profile.go +++ b/config/profile.go @@ -298,7 +298,7 @@ type ScheduleBaseSection struct { scheduleConfig *ScheduleConfig Schedule any `mapstructure:"schedule" show:"noshow" examples:"hourly;daily;weekly;monthly;10:00,14:00,18:00,22:00;Wed,Fri 17:48;*-*-15 02:45;Mon..Fri 00:30" description:"Configures the scheduled execution of this profile section. Can be times in systemd timer format or a config structure"` SchedulePermission string `mapstructure:"schedule-permission" show:"noshow" default:"auto" enum:"auto;system;user;user_logged_on" description:"Specify whether the schedule runs with system or user privileges - see https://creativeprojects.github.io/resticprofile/schedules/configuration/"` - ScheduleLog string `mapstructure:"schedule-log" show:"noshow" examples:"/resticprofile.log;tcp://localhost:514" description:"Redirect the output into a log file or to syslog when running on schedule"` + ScheduleLog string `mapstructure:"schedule-log" show:"noshow" examples:"/resticprofile.log;syslog-tcp://syslog-server:514;syslog:server;syslog:" description:"Redirect the output into a log file or to syslog when running on schedule"` SchedulePriority string `mapstructure:"schedule-priority" show:"noshow" default:"background" enum:"background;standard" description:"Set the priority at which the schedule is run"` ScheduleLockMode string `mapstructure:"schedule-lock-mode" show:"noshow" default:"default" enum:"default;fail;ignore" description:"Specify how locks are used when running on schedule - see https://creativeprojects.github.io/resticprofile/schedules/configuration/"` ScheduleLockWait maybe.Duration `mapstructure:"schedule-lock-wait" show:"noshow" examples:"150s;15m;30m;45m;1h;2h30m" description:"Set the maximum time to wait for acquiring locks when running on schedule"` diff --git a/config/schedule.go b/config/schedule.go index fb22d975..3fc324a1 100644 --- a/config/schedule.go +++ b/config/schedule.go @@ -35,7 +35,7 @@ const ( // ScheduleBaseConfig is the base user configuration that could be shared across all schedules. type ScheduleBaseConfig struct { Permission string `mapstructure:"permission" default:"auto" enum:"auto;system;user;user_logged_on" description:"Specify whether the schedule runs with system or user privileges - see https://creativeprojects.github.io/resticprofile/schedules/configuration/"` - Log string `mapstructure:"log" examples:"/resticprofile.log;tcp://localhost:514" description:"Redirect the output into a log file or to syslog when running on schedule"` + Log string `mapstructure:"log" examples:"/resticprofile.log;syslog-tcp://syslog-server:514;syslog:server;syslog:" description:"Redirect the output into a log file or to syslog when running on schedule"` Priority string `mapstructure:"priority" default:"background" enum:"background;standard" description:"Set the priority at which the schedule is run"` LockMode string `mapstructure:"lock-mode" default:"default" enum:"default;fail;ignore" description:"Specify how locks are used when running on schedule - see https://creativeprojects.github.io/resticprofile/schedules/configuration/"` LockWait maybe.Duration `mapstructure:"lock-wait" examples:"150s;15m;30m;45m;1h;2h30m" description:"Set the maximum time to wait for acquiring locks when running on schedule"` diff --git a/dial/url.go b/dial/url.go index 36993b95..dbefc381 100644 --- a/dial/url.go +++ b/dial/url.go @@ -1,18 +1,40 @@ package dial -import "net/url" +import ( + "net/url" + "slices" + "strings" + + "github.com/creativeprojects/clog" +) + +var validSchemes = []string{ + "udp", + "tcp", + "syslog", // local or UDP + "syslog-tcp", // TCP + // "syslog-tls", reserved for future support +} + +var noHostAllowed = []string{ + "syslog", +} // GetAddr returns scheme, host&port, isURL func GetAddr(source string) (scheme, hostPort string, isURL bool) { URL, err := url.Parse(source) - if err != nil { - return "", "", false - } - // need a minimum of udp://:12 - if len(URL.Scheme) < 3 || len(URL.Host) < 3 { - return "", "", false + if err == nil { + scheme = strings.ToLower(URL.Scheme) + hostPort = URL.Host + schemeOk := slices.Contains(validSchemes, scheme) + hostOk := len(hostPort) >= 3 || slices.Contains(noHostAllowed, scheme) + if isURL = schemeOk && hostOk; isURL { + return + } + } else { + clog.Tracef("is not an URL %q", source) } - return URL.Scheme, URL.Host, true + return "", "", false } func IsURL(source string) bool { diff --git a/dial/url_test.go b/dial/url_test.go index e7e1ee65..b46d43f0 100644 --- a/dial/url_test.go +++ b/dial/url_test.go @@ -16,13 +16,24 @@ func TestGetDialAddr(t *testing.T) { }{ // invalid {"://", "", "", false}, + // supported schemes + {"TCP://:123", "tcp", ":123", true}, + {"UDP://:123", "udp", ":123", true}, + {"tcp://:123", "tcp", ":123", true}, + {"udp://:123", "udp", ":123", true}, + {"syslog://:123", "syslog", ":123", true}, + {"syslog-tcp://:123", "syslog-tcp", ":123", true}, // url - {"scheme://:123", "scheme", ":123", true}, - {"scheme://host:123", "scheme", "host:123", true}, - {"scheme://host", "scheme", "host", true}, + {"syslog://:123", "syslog", ":123", true}, + {"syslog://host:123", "syslog", "host:123", true}, + {"syslog://host", "syslog", "host", true}, + {"syslog://", "syslog", "", true}, + {"syslog:", "syslog", "", true}, // too short - {"scheme://", "", "", false}, - {"scheme://:", "", "", false}, + {"tcp://", "", "", false}, + {"tcp:", "", "", false}, + {"syslog-tcp:", "", "", false}, + {"udp:", "", "", false}, {"c://", "", "", false}, {"c://:", "", "", false}, {"c://:123", "", "", false}, diff --git a/docs/content/configuration/logs.md b/docs/content/configuration/logs.md index 4ddd41ca..ddb53775 100644 --- a/docs/content/configuration/logs.md +++ b/docs/content/configuration/logs.md @@ -14,7 +14,7 @@ The log destination syntax is a such: * `-` {{% icon icon="arrow-right" %}} redirects all the logs to the console / stdout (is the default log destination) * `filename` {{% icon icon="arrow-right" %}} redirects all the logs to the local file called **filename** * `temp:filename` {{% icon icon="arrow-right" %}} redirects all the logs to a temporary file available during the whole session, and deleted afterwards. -* `tcp://syslog_server:514` or `udp://syslog_server:514` {{% icon icon="arrow-right" %}} redirects all the logs to the **syslog** server. +* `syslog:`, `syslog://syslog_server[:514]` or `syslog-tcp://syslog_server[:514]` {{% icon icon="arrow-right" %}} redirects all the logs to a local or remote **syslog** server. Alternative configurations for remote servers are: `udp://syslog_server:514` & `tcp://syslog_server:514`. {{% notice style="note" %}} Logging to syslog is not available on Windows. @@ -36,6 +36,8 @@ version = "1" [global] log = "resticprofile.log" +[global.schedule-defaults] +log = "scheduled-resticprofile.log" ``` {{% /tab %}} @@ -46,6 +48,8 @@ version: "1" global: log: "resticprofile.log" + schedule-defaults: + log: "scheduled-resticprofile.log" ``` {{% /tab %}} @@ -54,6 +58,9 @@ global: ```hcl "global" { "log" = "resticprofile.log" + "schedule-defaults" { + "log" = "scheduled-resticprofile.log" + } } ``` @@ -64,7 +71,10 @@ global: { "version": "1", "global": { - "log": "resticprofile.log" + "log": "resticprofile.log", + "schedule-defaults": { + "log": "scheduled-resticprofile.log" + } } } ``` diff --git a/logger.go b/logger.go index 8226e6be..08048e37 100644 --- a/logger.go +++ b/logger.go @@ -48,7 +48,7 @@ func setupTargetLogger(flags commandLineFlags, logTarget string) (io.Closer, err ) scheme, hostPort, isURL := dial.GetAddr(logTarget) if isURL { - handler, err = getSyslogHandler(scheme, hostPort) + handler, file, err = getSyslogHandler(scheme, hostPort) } else { handler, file, err = getFileHandler(logTarget) } diff --git a/syslog.go b/syslog.go index 2115d89c..0538bb54 100644 --- a/syslog.go +++ b/syslog.go @@ -3,9 +3,13 @@ package main import ( + "bytes" "errors" "fmt" + "io" "log/syslog" + "net" + "strings" "github.com/creativeprojects/clog" "github.com/creativeprojects/resticprofile/constants" @@ -48,11 +52,58 @@ func (l *Syslog) Close() error { var _ LogCloser = &Syslog{} -func getSyslogHandler(scheme, hostPort string) (*Syslog, error) { - writer, err := syslog.Dial(scheme, hostPort, syslog.LOG_USER|syslog.LOG_NOTICE, constants.ApplicationName) - if err != nil { - return nil, fmt.Errorf("cannot open syslog logger: %w", err) +const DefaultSyslogPort = "514" + +type tokenWriter struct { + separator []byte + target io.Writer +} + +func (s *tokenWriter) Write(p []byte) (n int, err error) { + var pn int + for i, part := range bytes.Split(p, s.separator) { + if err != nil { + break + } + pn, err = s.target.Write(part) + n += pn + if i > 0 { + n += len(s.separator) + } + } + return +} + +func getSyslogHandler(scheme, hostPort string) (handler *Syslog, writer io.Writer, err error) { + switch scheme { + case "udp", "tcp": + case "syslog-tcp": + scheme = "tcp" + case "syslog": + if len(hostPort) == 0 { + scheme = "local" + } else { + scheme = "udp" + } + default: + err = fmt.Errorf("unsupported scheme %q", scheme) + } + + var logger *syslog.Writer + if scheme == "local" { + logger, err = syslog.New(syslog.LOG_USER|syslog.LOG_NOTICE, constants.ApplicationName) + } else { + if _, _, e := net.SplitHostPort(hostPort); e != nil && strings.Contains(e.Error(), "missing port") { + hostPort = net.JoinHostPort(hostPort, DefaultSyslogPort) + } + logger, err = syslog.Dial(scheme, hostPort, syslog.LOG_USER|syslog.LOG_NOTICE, constants.ApplicationName) + } + + if err == nil { + writer = &tokenWriter{separator: []byte("\n"), target: logger} + handler = NewSyslogHandler(logger) + } else { + err = fmt.Errorf("cannot open syslog logger: %w", err) } - handler := NewSyslogHandler(writer) - return handler, nil + return } diff --git a/syslog_windows.go b/syslog_windows.go index 8efd921b..415c5ed9 100644 --- a/syslog_windows.go +++ b/syslog_windows.go @@ -4,8 +4,10 @@ package main import ( "errors" + "io" ) -func getSyslogHandler(scheme, hostPort string) (LogCloser, error) { - return nil, errors.New("syslog is not supported on Windows") +func getSyslogHandler(scheme, hostPort string) (_ LogCloser, _ io.Writer, err error) { + err = errors.New("syslog is not supported on Windows") + return } diff --git a/wrapper.go b/wrapper.go index 2013b000..557a7066 100644 --- a/wrapper.go +++ b/wrapper.go @@ -632,8 +632,8 @@ func (r *resticWrapper) runFinalShellCommands(command string, fail error) { term.FlushAllOutput() _, _, err := runShellCommand(rCommand) if err != nil { - clog.Errorf("run-finally command %d/%d failed ('%s' on profile '%s'): %w", - index+1, len(commands), command, r.profile.Name, err) + clog.Errorf("run-finally command %d/%d failed ('%s' on profile '%s'): %s", + index+1, len(commands), command, r.profile.Name, err.Error()) } }(i, commands[i]) }