Skip to content

Commit b1fc636

Browse files
authored
PBM-1454-Configuration-file-for-all-pbm-agent-s-options (#1065)
* init cobra and add wip version command * add flags * use viper * add cobra * add viper * wrapped static errors * ignore viper errors * fix long line * update import order * fix order * fix format * add hot reload for log * support yaml format only * tidy * add SetOpts to logger * handle update in run * validate on run * cleanup * check if different before print * move config to parameter and cleanup * fix env binding * tidy * add wip tests * update import * fix tests format * log opts getter and update print * remove change detection
1 parent c171630 commit b1fc636

File tree

381 files changed

+73337
-85
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

381 files changed

+73337
-85
lines changed

cmd/pbm-agent/commands_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"strings"
7+
"testing"
8+
9+
"github.com/spf13/cobra"
10+
11+
"github.com/percona/percona-backup-mongodb/pbm/version"
12+
)
13+
14+
func TestRootCmd_NoArgs(t *testing.T) {
15+
rootCmd, _ := setupTestCmd()
16+
err := rootCmd.Execute()
17+
if err == nil || !strings.Contains(err.Error(), "required flag mongodb-uri") {
18+
t.Fatal(err)
19+
}
20+
}
21+
22+
func TestRootCmd_Config(t *testing.T) {
23+
tmpConfig, cleanup := createTempConfigFile(`
24+
log:
25+
path: "/dev/stderr"
26+
level: "D"
27+
json: false
28+
`)
29+
defer cleanup()
30+
31+
rootCmd, _ := setupTestCmd("--config", tmpConfig)
32+
err := rootCmd.Execute()
33+
if err == nil || !strings.Contains(err.Error(), "required flag mongodb-uri") {
34+
t.Fatal(err)
35+
}
36+
}
37+
38+
func createTempConfigFile(content string) (string, func()) {
39+
temp, _ := os.CreateTemp("", "test-config-*.yaml")
40+
41+
_, _ = temp.WriteString(content)
42+
_ = temp.Close()
43+
44+
return temp.Name(), func() {
45+
_ = os.Remove(temp.Name())
46+
}
47+
}
48+
49+
func TestVersionCommand_Default(t *testing.T) {
50+
rootCmd, buf := setupTestCmd("version")
51+
err := rootCmd.Execute()
52+
if err != nil {
53+
t.Fatal(err)
54+
}
55+
56+
output := buf.String()
57+
58+
if !strings.Contains(output, "Version:") {
59+
t.Errorf("expected full version info in output, got: %s", output)
60+
}
61+
}
62+
63+
func TestVersionCommand_Short(t *testing.T) {
64+
rootCmd, buf := setupTestCmd("version", "--short")
65+
err := rootCmd.Execute()
66+
if err != nil {
67+
t.Fatal(err)
68+
}
69+
70+
output := buf.String()
71+
72+
if !strings.Contains(output, version.Current().Short()) {
73+
t.Errorf("expected short version info in output, got: %s", output)
74+
}
75+
}
76+
77+
func setupTestCmd(args ...string) (*cobra.Command, *bytes.Buffer) {
78+
cmd := rootCommand()
79+
cmd.AddCommand(versionCommand())
80+
81+
var buf bytes.Buffer
82+
cmd.SetOut(&buf)
83+
cmd.SetErr(&buf)
84+
cmd.SetArgs(args)
85+
86+
return cmd, &buf
87+
}

cmd/pbm-agent/main.go

Lines changed: 149 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ package main
33
import (
44
"context"
55
"fmt"
6-
stdlog "log"
76
"os"
87
"os/signal"
98
"runtime"
10-
"strconv"
119
"strings"
1210

13-
"github.com/alecthomas/kingpin"
11+
"github.com/fsnotify/fsnotify"
1412
mtLog "github.com/mongodb/mongo-tools/common/log"
1513
"github.com/mongodb/mongo-tools/common/options"
14+
"github.com/spf13/cobra"
15+
"github.com/spf13/viper"
1616

1717
"github.com/percona/percona-backup-mongodb/pbm/connect"
1818
"github.com/percona/percona-backup-mongodb/pbm/errors"
@@ -23,84 +23,157 @@ import (
2323
const mongoConnFlag = "mongodb-uri"
2424

2525
func main() {
26+
rootCmd := rootCommand()
27+
rootCmd.AddCommand(versionCommand())
28+
29+
if err := rootCmd.Execute(); err != nil {
30+
fmt.Println(err)
31+
}
32+
}
33+
34+
func rootCommand() *cobra.Command {
35+
rootCmd := &cobra.Command{
36+
Use: "pbm-agent",
37+
Short: "Percona Backup for MongoDB",
38+
PreRunE: func(cmd *cobra.Command, args []string) error {
39+
if err := loadConfig(); err != nil {
40+
return err
41+
}
42+
return validateRootCommand()
43+
},
44+
Run: func(cmd *cobra.Command, args []string) {
45+
url := "mongodb://" + strings.Replace(viper.GetString(mongoConnFlag), "mongodb://", "", 1)
46+
47+
hidecreds()
48+
49+
logOpts := buildLogOpts()
50+
51+
l := log.NewWithOpts(nil, "", "", logOpts).NewDefaultEvent()
52+
53+
err := runAgent(url, viper.GetInt("backup.dump-parallel-collections"), logOpts)
54+
if err != nil {
55+
l.Error("Exit: %v", err)
56+
os.Exit(1)
57+
}
58+
l.Info("Exit: <nil>")
59+
},
60+
}
61+
62+
setRootFlags(rootCmd)
63+
return rootCmd
64+
}
65+
66+
func loadConfig() error {
67+
cfgFile := viper.GetString("config")
68+
if cfgFile == "" {
69+
return nil
70+
}
71+
72+
viper.SetConfigFile(cfgFile)
73+
if err := viper.ReadInConfig(); err != nil {
74+
return errors.New("failed to read config: " + err.Error())
75+
}
76+
77+
viper.WatchConfig()
78+
return nil
79+
}
80+
81+
func validateRootCommand() error {
82+
if viper.GetString(mongoConnFlag) == "" {
83+
return errors.New("required flag " + mongoConnFlag + " not set")
84+
}
85+
86+
if !isValidLogLevel(viper.GetString("log.level")) {
87+
return errors.New("invalid log level")
88+
}
89+
90+
return nil
91+
}
92+
93+
func setRootFlags(rootCmd *cobra.Command) {
94+
rootCmd.Flags().StringP("config", "f", "", "Path to the config file")
95+
_ = viper.BindPFlag("config", rootCmd.Flags().Lookup("config"))
96+
97+
rootCmd.Flags().String(mongoConnFlag, "", "MongoDB connection string")
98+
_ = viper.BindPFlag(mongoConnFlag, rootCmd.Flags().Lookup(mongoConnFlag))
99+
_ = viper.BindEnv(mongoConnFlag, "PBM_MONGODB_URI")
100+
101+
rootCmd.Flags().Int("dump-parallel-collections", 0, "Number of collections to dump in parallel")
102+
_ = viper.BindPFlag("backup.dump-parallel-collections", rootCmd.Flags().Lookup("dump-parallel-collections"))
103+
_ = viper.BindEnv("backup.dump-parallel-collections", "PBM_DUMP_PARALLEL_COLLECTIONS")
104+
viper.SetDefault("backup.dump-parallel-collections", runtime.NumCPU()/2)
105+
106+
rootCmd.Flags().String("log-path", "", "Path to file")
107+
_ = viper.BindPFlag("log.path", rootCmd.Flags().Lookup("log-path"))
108+
_ = viper.BindEnv("log.path", "LOG_PATH")
109+
viper.SetDefault("log.path", "/dev/stderr")
110+
111+
rootCmd.Flags().Bool("log-json", false, "Enable JSON logging")
112+
_ = viper.BindPFlag("log.json", rootCmd.Flags().Lookup("log-json"))
113+
_ = viper.BindEnv("log.json", "LOG_JSON")
114+
viper.SetDefault("log.json", false)
115+
116+
rootCmd.Flags().String("log-level", "",
117+
"Minimal log level based on severity level: D, I, W, E or F, low to high."+
118+
"Choosing one includes higher levels too.")
119+
_ = viper.BindPFlag("log.level", rootCmd.Flags().Lookup("log-level"))
120+
_ = viper.BindEnv("log.level", "LOG_LEVEL")
121+
viper.SetDefault("log.level", log.D)
122+
}
123+
124+
func versionCommand() *cobra.Command {
26125
var (
27-
pbmCmd = kingpin.New("pbm-agent", "Percona Backup for MongoDB")
28-
pbmAgentCmd = pbmCmd.Command("run", "Run agent").
29-
Default().
30-
Hidden()
31-
32-
mURI = pbmAgentCmd.Flag(mongoConnFlag, "MongoDB connection string").
33-
Envar("PBM_MONGODB_URI").
34-
Required().
35-
String()
36-
dumpConns = pbmAgentCmd.
37-
Flag("dump-parallel-collections", "Number of collections to dump in parallel").
38-
Envar("PBM_DUMP_PARALLEL_COLLECTIONS").
39-
Default(strconv.Itoa(runtime.NumCPU() / 2)).
40-
Int()
41-
42-
versionCmd = pbmCmd.Command("version", "PBM version info")
43-
versionShort = versionCmd.Flag("short", "Only version info").
44-
Default("false").
45-
Bool()
46-
versionCommit = versionCmd.Flag("commit", "Only git commit info").
47-
Default("false").
48-
Bool()
49-
versionFormat = versionCmd.Flag("format", "Output format <json or \"\">").
50-
Default("").
51-
String()
52-
53-
logPath = pbmCmd.Flag("log-path", "Path to file").
54-
Envar("LOG_PATH").
55-
Default("/dev/stderr").
56-
String()
57-
logJSON = pbmCmd.Flag("log-json", "Enable JSON output").
58-
Envar("LOG_JSON").
59-
Bool()
60-
logLevel = pbmCmd.Flag(
61-
"log-level",
62-
"Minimal log level based on severity level: D, I, W, E or F, low to high. Choosing one includes higher levels too.").
63-
Envar("LOG_LEVEL").
64-
Default(log.D).
65-
Enum(log.D, log.I, log.W, log.E, log.F)
126+
versionShort bool
127+
versionCommit bool
128+
versionFormat string
66129
)
67130

68-
cmd, err := pbmCmd.DefaultEnvars().Parse(os.Args[1:])
69-
if err != nil && cmd != versionCmd.FullCommand() {
70-
stdlog.Println("Error: Parse command line parameters:", err)
71-
return
131+
versionCmd := &cobra.Command{
132+
Use: "version",
133+
Short: "PBM version info",
134+
Run: func(cmd *cobra.Command, args []string) {
135+
switch {
136+
case versionShort:
137+
cmd.Println(version.Current().Short())
138+
case versionCommit:
139+
cmd.Println(version.Current().GitCommit)
140+
default:
141+
cmd.Println(version.Current().All(versionFormat))
142+
}
143+
},
72144
}
73145

74-
if cmd == versionCmd.FullCommand() {
75-
switch {
76-
case *versionCommit:
77-
fmt.Println(version.Current().GitCommit)
78-
case *versionShort:
79-
fmt.Println(version.Current().Short())
80-
default:
81-
fmt.Println(version.Current().All(*versionFormat))
146+
versionCmd.Flags().BoolVar(&versionShort, "short", false, "Only version info")
147+
versionCmd.Flags().BoolVar(&versionCommit, "commit", false, "Only git commit info")
148+
versionCmd.Flags().StringVar(&versionFormat, "format", "", "Output format <json or \"\">")
149+
150+
return versionCmd
151+
}
152+
153+
func isValidLogLevel(logLevel string) bool {
154+
validLogLevels := []string{log.D, log.I, log.W, log.E, log.F}
155+
156+
for _, validLevel := range validLogLevels {
157+
if logLevel == validLevel {
158+
return true
82159
}
83-
return
84160
}
85161

86-
// hidecreds() will rewrite the flag content, so we have to make a copy before passing it on
87-
url := "mongodb://" + strings.Replace(*mURI, "mongodb://", "", 1)
88-
89-
hidecreds()
162+
return false
163+
}
90164

91-
logOpts := &log.Opts{
92-
LogPath: *logPath,
93-
LogLevel: *logLevel,
94-
LogJSON: *logJSON,
165+
func buildLogOpts() *log.Opts {
166+
logLevel := viper.GetString("log.level")
167+
if !isValidLogLevel(logLevel) {
168+
fmt.Printf("Invalid log level: %s. Falling back to default.\n", logLevel)
169+
logLevel = log.D
95170
}
96-
l := log.NewWithOpts(nil, "", "", logOpts).NewDefaultEvent()
97171

98-
err = runAgent(url, *dumpConns, logOpts)
99-
if err != nil {
100-
l.Error("Exit: %v", err)
101-
os.Exit(1)
172+
return &log.Opts{
173+
LogPath: viper.GetString("log.path"),
174+
LogLevel: logLevel,
175+
LogJSON: viper.GetBool("log.json"),
102176
}
103-
l.Info("Exit: <nil>")
104177
}
105178

106179
func runAgent(
@@ -133,6 +206,13 @@ func runAgent(
133206
logOpts)
134207
defer logger.Close()
135208

209+
viper.OnConfigChange(func(e fsnotify.Event) {
210+
logger.SetOpts(buildLogOpts())
211+
newOpts := logger.Opts()
212+
logger.Printf("log options updated: log-path=%s, log-level:%s, log-json:%t",
213+
newOpts.LogPath, newOpts.LogLevel, newOpts.LogJSON)
214+
})
215+
136216
ctx = log.SetLoggerToContext(ctx, logger)
137217

138218
mtLog.SetDateFormat(log.LogTimeFormat)

0 commit comments

Comments
 (0)