Skip to content

Commit

Permalink
log: allow controlling command output redirection (#343)
Browse files Browse the repository at this point in the history
* log: allow controlling command output redirection

* added missing default value handling

* renamed "log-commands" to "command-output"

* added documentation

* updated cli-help, added fail on unsupported URL

* fixed default value not allowing global config
  • Loading branch information
jkellerer authored Mar 21, 2024
1 parent 655e9c5 commit 85d5afc
Show file tree
Hide file tree
Showing 18 changed files with 153 additions and 43 deletions.
3 changes: 3 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,9 @@ func prepareScheduledProfile(ctx *Context) {
if len(s.Log) > 0 {
ctx.logTarget = s.Log
}
if len(s.CommandOutput) > 0 {
ctx.commandOutput = s.CommandOutput
}
// battery
if s.IgnoreOnBatteryLessThan > 0 && !s.IgnoreOnBattery.IsStrictlyFalse() {
ctx.stopOnBattery = s.IgnoreOnBatteryLessThan
Expand Down
2 changes: 2 additions & 0 deletions commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,13 +288,15 @@ func TestShowSchedules(t *testing.T) {
schedule backup@default:
at: daily
permission: auto
command-output: auto
priority: background
lock-mode: default
capture-environment: RESTIC_*
schedule check@default:
at: weekly
permission: auto
command-output: auto
priority: background
lock-mode: default
capture-environment: RESTIC_*
Expand Down
2 changes: 2 additions & 0 deletions complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ func (c *Completer) completeFlagSetValue(flag *pflag.Flag, word string) (complet
fallthrough
case "log":
completions = []string{RequestFileCompletion}
case "command-output":
completions = []string{"auto", "log", "console", "all"}
}

completions = c.appendMatches(completions, word, list...)
Expand Down
8 changes: 7 additions & 1 deletion complete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"slices"
"sort"
"strings"
"testing"
Expand Down Expand Up @@ -62,7 +63,12 @@ func TestCompleter(t *testing.T) {
if flag.Hidden {
expected = nil
}
assert.Equal(t, expected, completer.completeFlagSet(fmt.Sprintf("--%s", flag.Name)))
actual := completer.completeFlagSet(fmt.Sprintf("--%s", flag.Name))
assert.Subset(t, actual, expected)
for _, flag := range actual {
ok := slices.ContainsFunc(expected, func(prefix string) bool { return strings.HasPrefix(flag, prefix) })
assert.True(t, ok, "prefixes not matched for %q", flag)
}

if len(flag.Shorthand) > 0 && !flag.Hidden {
expected = flagCompletion(flag, true)[0:1]
Expand Down
4 changes: 3 additions & 1 deletion config/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ 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:"" 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/"`
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/"`
CommandOutput string `mapstructure:"command-output" default:"auto" enum:"auto;log;console;all" description:"Sets the destination for command output (stderr/stdout). \"log\" sends output to the log file (if specified), \"console\" sends it to the console instead. \"auto\" sends it to \"both\" if console is a terminal otherwise to \"log\" only - 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 All @@ -47,6 +48,7 @@ func NewGlobal() *Global {
ResticLockRetryAfter: constants.DefaultResticLockRetryAfter,
ResticStaleLockAge: constants.DefaultResticStaleLockAge,
MinMemory: constants.DefaultMinMemory,
CommandOutput: constants.DefaultCommandOutput,
SenderTimeout: constants.DefaultSenderTimeout,
}
}
Expand Down
15 changes: 10 additions & 5 deletions config/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ 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;syslog-tcp://syslog-server:514;syslog:server;syslog:" 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 - see https://creativeprojects.github.io/resticprofile/configuration/logs/"`
CommandOutput string `mapstructure:"command-output" default:"auto" enum:"auto;log;console;all" description:"Sets the destination for command output (stderr/stdout). \"log\" sends output to the log file (if specified), \"console\" sends it to the console instead. \"auto\" sends it to \"both\" if console is a terminal otherwise to \"log\" only - see https://creativeprojects.github.io/resticprofile/configuration/logs/"`
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 All @@ -48,10 +49,11 @@ type ScheduleBaseConfig struct {

// scheduleBaseConfigDefaults declares built-in scheduling defaults
var scheduleBaseConfigDefaults = ScheduleBaseConfig{
Permission: "auto",
Priority: "background",
LockMode: "default",
EnvCapture: []string{"RESTIC_*"},
Permission: "auto",
CommandOutput: constants.DefaultCommandOutput,
Priority: "background",
LockMode: "default",
EnvCapture: []string{"RESTIC_*"},
}

func (s *ScheduleBaseConfig) init(defaults *ScheduleBaseConfig) {
Expand All @@ -65,6 +67,9 @@ func (s *ScheduleBaseConfig) init(defaults *ScheduleBaseConfig) {
if s.Log == "" {
s.Log = defaults.Log
}
if s.CommandOutput == "" {
s.CommandOutput = defaults.CommandOutput
}
if s.Priority == "" {
s.Priority = defaults.Priority
}
Expand Down
1 change: 1 addition & 0 deletions constants/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
DefaultVerboseFlag = false
DefaultQuietFlag = false
DefaultMinMemory = 100
DefaultCommandOutput = "auto"
DefaultSenderTimeout = 30 * time.Second
DefaultPrometheusPushFormat = "text"
BatteryFull = 100
Expand Down
24 changes: 15 additions & 9 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/creativeprojects/resticprofile/config"
"github.com/creativeprojects/resticprofile/constants"
)

type Request struct {
Expand All @@ -29,6 +30,7 @@ type Context struct {
schedule *config.Schedule // when profile is running with run-schedule command
sigChan chan os.Signal // termination request
logTarget string // where to send the log output
commandOutput string // where to send the command output when a lotTarget is set
stopOnBattery int // stop if running on battery
noLock bool // skip profile lock file
lockWait time.Duration // wait up to duration to acquire a lock
Expand All @@ -51,15 +53,16 @@ func CreateContext(flags commandLineFlags, global *config.Global, cfg *config.Co
group: "",
schedule: "",
},
flags: flags,
global: global,
config: cfg,
binary: "",
command: "",
profile: nil,
schedule: nil,
sigChan: nil,
logTarget: global.Log, // default to global (which can be empty)
flags: flags,
global: global,
config: cfg,
binary: "",
command: "",
profile: nil,
schedule: nil,
sigChan: nil,
logTarget: global.Log, // default to global (which can be empty)
commandOutput: global.CommandOutput,
}
// own commands can check the context before running
if ownCommands.Exists(command, true) {
Expand All @@ -72,6 +75,9 @@ func CreateContext(flags commandLineFlags, global *config.Global, cfg *config.Co
if flags.log != "" {
ctx.logTarget = flags.log
}
if flags.commandOutput != constants.DefaultCommandOutput {
ctx.commandOutput = flags.commandOutput
}
// same for battery configuration
if flags.ignoreOnBattery > 0 {
ctx.stopOnBattery = flags.ignoreOnBattery
Expand Down
30 changes: 16 additions & 14 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,30 +131,32 @@ func TestCreateContext(t *testing.T) {
},
{
description: "global log target",
flags: commandLineFlags{},
global: &config.Global{Log: "global"},
flags: commandLineFlags{commandOutput: "auto"},
global: &config.Global{Log: "global", CommandOutput: "global"},
cfg: &config.Config{},
ownCommands: &OwnCommands{},
context: &Context{
flags: commandLineFlags{},
request: Request{},
global: &config.Global{Log: "global"},
config: &config.Config{},
logTarget: "global",
flags: commandLineFlags{commandOutput: "auto"},
request: Request{},
global: &config.Global{Log: "global", CommandOutput: "global"},
config: &config.Config{},
logTarget: "global",
commandOutput: "global",
},
},
{
description: "log target on the command line",
flags: commandLineFlags{log: "cmdline"},
global: &config.Global{Log: "global"},
flags: commandLineFlags{log: "cmdline", commandOutput: "cmdline"},
global: &config.Global{Log: "global", CommandOutput: "global"},
cfg: &config.Config{},
ownCommands: &OwnCommands{},
context: &Context{
flags: commandLineFlags{log: "cmdline"},
request: Request{},
global: &config.Global{Log: "global"},
config: &config.Config{},
logTarget: "cmdline",
flags: commandLineFlags{log: "cmdline", commandOutput: "cmdline"},
request: Request{},
global: &config.Global{Log: "global", CommandOutput: "global"},
config: &config.Config{},
logTarget: "cmdline",
commandOutput: "cmdline",
},
},
{
Expand Down
13 changes: 10 additions & 3 deletions dial/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ var noHostAllowed = []string{
"syslog",
}

// GetAddr returns scheme, host&port, isURL
// GetAddr returns scheme, host&port, is(Supported)URL
func GetAddr(source string) (scheme, hostPort string, isURL bool) {
URL, err := url.Parse(source)
if err == nil {
scheme = strings.ToLower(URL.Scheme)
hostPort = URL.Host
schemeOk := slices.Contains(validSchemes, scheme)
hostOk := len(hostPort) >= 3 || slices.Contains(noHostAllowed, scheme)
hostOk := len(hostPort) >= 3 || (slices.Contains(noHostAllowed, scheme) && len(URL.Opaque) == 0)
if isURL = schemeOk && hostOk; isURL {
return
}
Expand All @@ -37,7 +37,14 @@ func GetAddr(source string) (scheme, hostPort string, isURL bool) {
return "", "", false
}

func IsURL(source string) bool {
// IsSupportedURL returns true if the provided source is valid for GetAddr
func IsSupportedURL(source string) bool {
_, _, isURL := GetAddr(source)
return isURL
}

// IsURL is true if the provided source is a parsable URL and no file path
func IsURL(source string) bool {
u, e := url.Parse(source)
return e == nil && len(u.Scheme) > 1
}
10 changes: 10 additions & 0 deletions dial/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestGetDialAddr(t *testing.T) {
{"syslog://", "syslog", "", true},
{"syslog:", "syslog", "", true},
// too short
{"syslog:opaque", "", "", false},
{"tcp://", "", "", false},
{"tcp:", "", "", false},
{"syslog-tcp:", "", "", false},
Expand All @@ -55,6 +56,15 @@ func TestGetDialAddr(t *testing.T) {
assert.Equal(t, fixture.isURL, isURL)
assert.Equal(t, fixture.scheme, scheme)
assert.Equal(t, fixture.hostPort, port)

assert.Equal(t, fixture.isURL, dial.IsSupportedURL(fixture.addr))
})
}
}

func TestIsUrl(t *testing.T) {
assert.True(t, dial.IsURL("ftp://"))
assert.True(t, dial.IsURL("http://"))
assert.False(t, dial.IsURL("c://"))
assert.False(t, dial.IsURL(""))
}
6 changes: 4 additions & 2 deletions docs/content/usage/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ There are not many options on the command line, most of the options are in the c
light or dark terminal (none to disable colouring)
* **[--lock-wait] duration**: Retry to acquire resticprofile and restic locks for up to the specified amount of time before failing on a lock failure.
* **[-l | --log] file path or url**: To write the logs to a file or a syslog server instead of displaying on the console.
The format of the server url is `tcp://192.168.0.1:514` or `udp://localhost:514`.
The format of the syslog server url is `syslog-tcp://192.168.0.1:514`, `syslog://udp-server:514` or `syslog:`.
For custom log forwarding, the prefix `temp:` can be used (e.g. `temp:/t/msg.log`) to create unique log output that can be fed
into a command or http hook by referencing it with `{{ tempDir }}/...` or `{{ tempFile "msg.log" }}` in the configuration file.
into a command or http hook by referencing it with `"{{ tempFile "msg.log" }}"` in the configuration file.
* **[--command-output]**: Sets how to redirect command output when a log target is specified. Can be `auto`, `log`, `console` or `all`.
* **[-w | --wait]**: Wait at the very end of the execution for the user to press enter.
This is only useful in Windows when resticprofile is started from explorer and the console window closes automatically at the end.
* **[--ignore-on-battery]**: Don't start the profile when the computer is running on battery. You can specify a value to ignore only when the % charge left is less or equal than the value.
Expand All @@ -109,6 +110,7 @@ Most flags for resticprofile can be set using environment variables. If both are
| `--format` | `RESTICPROFILE_FORMAT` | `""` |
| `--name` | `RESTICPROFILE_NAME` | `"default"` |
| `--log` | `RESTICPROFILE_LOG` | `""` |
| `--command-output` | `RESTICPROFILE_COMMAND_OUTPUT` | `"auto"` |
| `--dry-run` | `RESTICPROFILE_DRY_RUN` | `false` |
| `--no-lock` | `RESTICPROFILE_NO_LOCK` | `false` |
| `--lock-wait` | `RESTICPROFILE_LOCK_WAIT` | `0` |
Expand Down
5 changes: 4 additions & 1 deletion flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type commandLineFlags struct {
format string
name string
log string // file path or log url
commandOutput string
dryRun bool
noLock bool
lockWait time.Duration
Expand Down Expand Up @@ -80,6 +81,7 @@ func loadFlags(args []string) (*pflag.FlagSet, commandLineFlags, error) {
format: envValueOverride("", "RESTICPROFILE_FORMAT"),
name: envValueOverride(constants.DefaultProfileName, "RESTICPROFILE_NAME"),
log: envValueOverride("", "RESTICPROFILE_LOG"),
commandOutput: envValueOverride(constants.DefaultCommandOutput, "RESTICPROFILE_COMMAND_OUTPUT"),
dryRun: envValueOverride(false, "RESTICPROFILE_DRY_RUN"),
noLock: envValueOverride(false, "RESTICPROFILE_NO_LOCK"),
lockWait: envValueOverride(time.Duration(0), "RESTICPROFILE_LOCK_WAIT"),
Expand All @@ -97,7 +99,8 @@ func loadFlags(args []string) (*pflag.FlagSet, commandLineFlags, error) {
flagset.StringVarP(&flags.config, "config", "c", flags.config, "configuration file")
flagset.StringVarP(&flags.format, "format", "f", flags.format, "file format of the configuration (default is to use the file extension)")
flagset.StringVarP(&flags.name, "name", "n", flags.name, "profile name")
flagset.StringVarP(&flags.log, "log", "l", flags.log, "logs to a target instead of the console")
flagset.StringVarP(&flags.log, "log", "l", flags.log, "logs to a target instead of the console (file, syslog:[//server])")
flagset.StringVar(&flags.commandOutput, "command-output", flags.commandOutput, "redirect command output when a log target is specified (log, console, all)")
flagset.BoolVar(&flags.dryRun, "dry-run", flags.dryRun, "display the restic commands instead of running them")
flagset.BoolVar(&flags.noLock, "no-lock", flags.noLock, "skip profile lock file")
flagset.DurationVar(&flags.lockWait, "lock-wait", flags.lockWait, "wait up to duration to acquire a lock (syntax \"1h5m30s\")")
Expand Down
1 change: 1 addition & 0 deletions flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func TestEnvOverrides(t *testing.T) {
format: setEnv("custom-format", "RESTICPROFILE_FORMAT").(string),
name: setEnv("custom-profile", "RESTICPROFILE_NAME").(string),
log: setEnv("custom.log", "RESTICPROFILE_LOG").(string),
commandOutput: setEnv("log", "RESTICPROFILE_COMMAND_OUTPUT").(string),
dryRun: setEnv(true, "RESTICPROFILE_DRY_RUN").(bool),
noLock: setEnv(true, "RESTICPROFILE_NO_LOCK").(bool),
lockWait: setEnv(time.Minute*5, "RESTICPROFILE_LOCK_WAIT").(time.Duration),
Expand Down
Loading

0 comments on commit 85d5afc

Please sign in to comment.