Skip to content

Commit

Permalink
syslog: local syslog and stdout redirection (#344)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkellerer authored Mar 19, 2024
1 parent e5b17d3 commit 667180e
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 29 deletions.
2 changes: 1 addition & 1 deletion config/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/"`
Expand Down
2 changes: 1 addition & 1 deletion config/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
2 changes: 1 addition & 1 deletion config/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
38 changes: 30 additions & 8 deletions dial/url.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
21 changes: 16 additions & 5 deletions dial/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
14 changes: 12 additions & 2 deletions docs/content/configuration/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -36,6 +36,8 @@ version = "1"

[global]
log = "resticprofile.log"
[global.schedule-defaults]
log = "scheduled-resticprofile.log"
```

{{% /tab %}}
Expand All @@ -46,6 +48,8 @@ version: "1"

global:
log: "resticprofile.log"
schedule-defaults:
log: "scheduled-resticprofile.log"
```
{{% /tab %}}
Expand All @@ -54,6 +58,9 @@ global:
```hcl
"global" {
"log" = "resticprofile.log"
"schedule-defaults" {
"log" = "scheduled-resticprofile.log"
}
}
```

Expand All @@ -64,7 +71,10 @@ global:
{
"version": "1",
"global": {
"log": "resticprofile.log"
"log": "resticprofile.log",
"schedule-defaults": {
"log": "scheduled-resticprofile.log"
}
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
63 changes: 57 additions & 6 deletions syslog.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
package main

import (
"bytes"
"errors"
"fmt"
"io"
"log/syslog"
"net"
"strings"

"github.com/creativeprojects/clog"
"github.com/creativeprojects/resticprofile/constants"
Expand Down Expand Up @@ -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
}
6 changes: 4 additions & 2 deletions syslog_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
Expand Down

0 comments on commit 667180e

Please sign in to comment.