Skip to content
This repository was archived by the owner on Jul 19, 2023. It is now read-only.

Commit 3b273c4

Browse files
authored
Add -modules support (#497)
This adds the ability to list modules, display a basic flag `-help` and `-help-all` including all flags. And handles `-version` from the main package.
1 parent 245bf03 commit 3b273c4

File tree

8 files changed

+556
-22
lines changed

8 files changed

+556
-22
lines changed

cmd/phlare/main.go

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,96 @@ import (
55
"flag"
66
"fmt"
77
"os"
8+
"sort"
9+
10+
"github.com/grafana/dskit/flagext"
11+
"github.com/prometheus/common/version"
812

913
"github.com/grafana/phlare/pkg/cfg"
1014
"github.com/grafana/phlare/pkg/phlare"
15+
"github.com/grafana/phlare/pkg/usage"
1116
_ "github.com/grafana/phlare/pkg/util/build"
1217
)
1318

19+
type mainFlags struct {
20+
phlare.Config
21+
22+
PrintVersion bool
23+
PrintModules bool
24+
PrintHelp bool
25+
PrintHelpAll bool
26+
}
27+
28+
func (mf *mainFlags) Clone() flagext.Registerer {
29+
return func(mf mainFlags) *mainFlags {
30+
return &mf
31+
}(*mf)
32+
}
33+
34+
func (mf *mainFlags) PhlareConfig() *phlare.Config {
35+
return &mf.Config
36+
}
37+
38+
func (mf *mainFlags) RegisterFlags(fs *flag.FlagSet) {
39+
mf.Config.RegisterFlags(fs)
40+
fs.BoolVar(&mf.PrintVersion, "version", false, "Show the version of phlare and exit")
41+
fs.BoolVar(&mf.PrintModules, "modules", false, "List available modules that can be used as target and exit.")
42+
fs.BoolVar(&mf.PrintHelp, "h", false, "Print basic help.")
43+
fs.BoolVar(&mf.PrintHelp, "help", false, "Print basic help.")
44+
fs.BoolVar(&mf.PrintHelpAll, "help-all", false, "Print help, also including advanced and experimental parameters.")
45+
}
46+
1447
func main() {
15-
var config phlare.Config
16-
if err := cfg.DynamicUnmarshal(&config, os.Args[1:], flag.CommandLine); err != nil {
48+
var (
49+
flags mainFlags
50+
)
51+
52+
if err := cfg.DynamicUnmarshal(&flags, os.Args[1:], flag.CommandLine); err != nil {
1753
fmt.Fprintf(os.Stderr, "failed parsing config: %v\n", err)
1854
os.Exit(1)
1955
}
2056

21-
f, err := phlare.New(config)
57+
f, err := phlare.New(flags.Config)
2258
if err != nil {
2359
fmt.Fprintf(os.Stderr, "failed creating phlare: %v\n", err)
2460
os.Exit(1)
2561
}
2662

63+
if flags.PrintVersion {
64+
fmt.Println(version.Print("phlare"))
65+
return
66+
}
67+
68+
if flags.PrintModules {
69+
allDeps := f.ModuleManager.DependenciesForModule(phlare.All)
70+
71+
for _, m := range f.ModuleManager.UserVisibleModuleNames() {
72+
ix := sort.SearchStrings(allDeps, m)
73+
included := ix < len(allDeps) && allDeps[ix] == m
74+
75+
if included {
76+
fmt.Fprintln(os.Stdout, m, "*")
77+
} else {
78+
fmt.Fprintln(os.Stdout, m)
79+
}
80+
}
81+
82+
fmt.Fprintln(os.Stdout)
83+
fmt.Fprintln(os.Stdout, "Modules marked with * are included in target All.")
84+
return
85+
}
86+
87+
if flags.PrintHelp || flags.PrintHelpAll {
88+
// Print available parameters to stdout, so that users can grep/less them easily.
89+
flag.CommandLine.SetOutput(os.Stdout)
90+
if err := usage.Usage(flags.PrintHelpAll, &flags); err != nil {
91+
fmt.Fprintf(os.Stderr, "error printing usage: %s\n", err)
92+
os.Exit(1)
93+
}
94+
95+
return
96+
}
97+
2798
err = f.Run()
2899
if err != nil {
29100
fmt.Fprintf(os.Stderr, "failed running phlare: %v\n", err)

cmd/phlare/main_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"os"
6+
"strings"
7+
"testing"
8+
9+
"github.com/prometheus/client_golang/prometheus"
10+
11+
"github.com/grafana/phlare/pkg/test"
12+
)
13+
14+
func TestFlagParsing(t *testing.T) {
15+
for name, tc := range map[string]struct {
16+
arguments []string
17+
stdoutMessage string // string that must be included in stdout
18+
stderrMessage string // string that must be included in stderr
19+
stdoutExcluded string // string that must NOT be included in stdout
20+
stderrExcluded string // string that must NOT be included in stderr
21+
}{
22+
"help-short": {
23+
arguments: []string{"-h"},
24+
stdoutMessage: "Usage of", // Usage must be on stdout, not stderr.
25+
stderrExcluded: "Usage of",
26+
},
27+
"help": {
28+
arguments: []string{"-help"},
29+
stdoutMessage: "Usage of", // Usage must be on stdout, not stderr.
30+
stderrExcluded: "Usage of",
31+
},
32+
"help-all": {
33+
arguments: []string{"-help-all"},
34+
stdoutMessage: "Usage of", // Usage must be on stdout, not stderr.
35+
stderrExcluded: "Usage of",
36+
},
37+
"user visible module listing": {
38+
arguments: []string{"-modules"},
39+
stdoutMessage: "ingester *\n",
40+
stderrExcluded: "ingester\n",
41+
},
42+
"version": {
43+
arguments: []string{"-version"},
44+
stdoutMessage: "phlare, version",
45+
stderrExcluded: "phlare, version",
46+
},
47+
} {
48+
t.Run(name, func(t *testing.T) {
49+
_ = os.Setenv("TARGET", "ingester")
50+
oldDefaultRegistry := prometheus.DefaultRegisterer
51+
defer func() {
52+
prometheus.DefaultRegisterer = oldDefaultRegistry
53+
}()
54+
// We need to reset the default registry to avoid
55+
// "duplicate metrics collector registration attempted" errors.
56+
prometheus.DefaultRegisterer = prometheus.NewRegistry()
57+
testSingle(t, tc.arguments, tc.stdoutMessage, tc.stderrMessage, tc.stdoutExcluded, tc.stderrExcluded)
58+
})
59+
}
60+
}
61+
62+
func testSingle(t *testing.T, arguments []string, stdoutMessage, stderrMessage, stdoutExcluded, stderrExcluded string) {
63+
t.Helper()
64+
oldArgs, oldStdout, oldStderr := os.Args, os.Stdout, os.Stderr
65+
restored := false
66+
restoreIfNeeded := func() {
67+
if restored {
68+
return
69+
}
70+
os.Stdout = oldStdout
71+
os.Stderr = oldStderr
72+
os.Args = oldArgs
73+
restored = true
74+
}
75+
defer restoreIfNeeded()
76+
77+
arguments = append([]string{"./phlare"}, arguments...)
78+
79+
os.Args = arguments
80+
co := test.CaptureOutput(t)
81+
82+
// reset default flags
83+
flag.CommandLine = flag.NewFlagSet(arguments[0], flag.ExitOnError)
84+
85+
main()
86+
87+
stdout, stderr := co.Done()
88+
89+
// Restore stdout and stderr before reporting errors to make them visible.
90+
restoreIfNeeded()
91+
if !strings.Contains(stdout, stdoutMessage) {
92+
t.Errorf("Expected on stdout: %q, stdout: %s\n", stdoutMessage, stdout)
93+
}
94+
if !strings.Contains(stderr, stderrMessage) {
95+
t.Errorf("Expected on stderr: %q, stderr: %s\n", stderrMessage, stderr)
96+
}
97+
if len(stdoutExcluded) > 0 && strings.Contains(stdout, stdoutExcluded) {
98+
t.Errorf("Unexpected output on stdout: %q, stdout: %s\n", stdoutExcluded, stdout)
99+
}
100+
if len(stderrExcluded) > 0 && strings.Contains(stderr, stderrExcluded) {
101+
t.Errorf("Unexpected output on stderr: %q, stderr: %s\n", stderrExcluded, stderr)
102+
}
103+
}

pkg/cfg/files.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,10 @@ func YAMLFlag(args []string, name string) Source {
7474
// parsing out the config file location.
7575
dst.Clone().RegisterFlags(freshFlags)
7676

77-
usage := freshFlags.Usage
7877
freshFlags.Usage = func() { /* don't do anything by default, we will print usage ourselves, but only when requested. */ }
7978

80-
err := freshFlags.Parse(args)
81-
if err == flag.ErrHelp {
82-
// print available parameters to stdout, so that users can grep/less it easily
83-
freshFlags.SetOutput(os.Stdout)
84-
usage()
85-
os.Exit(2)
86-
} else if err != nil {
87-
fmt.Fprintln(freshFlags.Output(), "Run with -help to get list of available parameters")
79+
if err := freshFlags.Parse(args); err != nil {
80+
fmt.Fprintln(freshFlags.Output(), "Run with -help to get a list of available parameters")
8881
os.Exit(2)
8982
}
9083

@@ -99,6 +92,5 @@ func YAMLFlag(args []string, name string) Source {
9992
}
10093

10194
return YAML(f.Value.String(), expandEnv)(dst)
102-
10395
}
10496
}

pkg/phlare/phlare.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ type Config struct {
7575
Analytics usagestats.Config `yaml:"analytics"`
7676

7777
ConfigFile string `yaml:"-"`
78-
ShowVersion bool `yaml:"-"`
7978
ConfigExpandEnv bool `yaml:"-"`
8079
}
8180

@@ -106,7 +105,6 @@ func (c *Config) RegisterFlagsWithContext(ctx context.Context, f *flag.FlagSet)
106105
f.Var(&c.Target, "target", "Comma-separated list of Phlare modules to load. "+
107106
"The alias 'all' can be used in the list to load a number of core modules and will enable single-binary mode. ")
108107
f.BoolVar(&c.MultitenancyEnabled, "auth.multitenancy-enabled", false, "When set to true, incoming HTTP requests must specify tenant ID in HTTP X-Scope-OrgId header. When set to false, tenant ID anonymous is used instead.")
109-
f.BoolVar(&c.ShowVersion, "version", false, "Show the version of phlare and exit")
110108
f.BoolVar(&c.ConfigExpandEnv, "config.expand-env", false, "Expands ${var} in config according to the values of the environment variables.")
111109

112110
c.registerServerFlagsWithChangedDefaultValues(f)
@@ -165,6 +163,10 @@ func (c *Config) Validate() error {
165163
return c.AgentConfig.Validate()
166164
}
167165

166+
type phlareConfigGetter interface {
167+
PhlareConfig() *Config
168+
}
169+
168170
func (c *Config) ApplyDynamicConfig() cfg.Source {
169171
c.Ingester.LifecyclerConfig.RingConfig.KVStore.Store = "memberlist"
170172
c.Distributor.DistributorRing.KVStore.Store = c.Ingester.LifecyclerConfig.RingConfig.KVStore.Store
@@ -175,10 +177,11 @@ func (c *Config) ApplyDynamicConfig() cfg.Source {
175177
c.Worker.MaxConcurrentRequests = 4 // todo we might want this as a config flags.
176178

177179
return func(dst cfg.Cloneable) error {
178-
r, ok := dst.(*Config)
180+
g, ok := dst.(phlareConfigGetter)
179181
if !ok {
180-
return errors.New("dst is not a Phlare config")
182+
return fmt.Errorf("dst is not a Phlare config getter %T", dst)
181183
}
184+
r := g.PhlareConfig()
182185
if r.AgentConfig.ClientConfig.URL.String() == "" {
183186
listenAddress := "0.0.0.0"
184187
if c.Server.HTTPListenAddress != "" {
@@ -233,11 +236,6 @@ func New(cfg Config) (*Phlare, error) {
233236
logger := initLogger(&cfg.Server)
234237
usagestats.Edition("oss")
235238

236-
if cfg.ShowVersion {
237-
fmt.Println(version.Print("phlare"))
238-
os.Exit(0)
239-
}
240-
241239
phlare := &Phlare{
242240
Cfg: cfg,
243241
logger: logger,

pkg/phlare/phlare_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ func TestFlagDefaults(t *testing.T) {
2222
f.PrintDefaults()
2323

2424
const delim = '\n'
25+
// Because this is a short flag, it will be printed on the same line as the
26+
// flag name. So we need to ignore this special case.
27+
const ignoredHelpFlags = "-h\tPrint basic help."
2528

2629
// Populate map with parsed default flags.
2730
// Key is the flag and value is the default text.
@@ -33,6 +36,10 @@ func TestFlagDefaults(t *testing.T) {
3336
}
3437
require.NoError(t, err)
3538

39+
if strings.Contains(line, ignoredHelpFlags) {
40+
continue
41+
}
42+
3643
nextLine, err := buf.ReadString(delim)
3744
require.NoError(t, err)
3845

pkg/test/capture.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package test
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"os"
7+
"sync"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
type CapturedOutput struct {
14+
stdoutBuf bytes.Buffer
15+
stderrBuf bytes.Buffer
16+
17+
wg sync.WaitGroup
18+
stdoutReader, stdoutWriter *os.File
19+
stderrReader, stderrWriter *os.File
20+
}
21+
22+
// CaptureOutput replaces os.Stdout and os.Stderr with new pipes, that will
23+
// write output to buffers. Buffers are accessible by calling Done on returned
24+
// struct.
25+
//
26+
// os.Stdout and os.Stderr must be reverted to previous values manually.
27+
func CaptureOutput(t *testing.T) *CapturedOutput {
28+
stdoutR, stdoutW, err := os.Pipe()
29+
require.NoError(t, err)
30+
31+
stderrR, stderrW, err := os.Pipe()
32+
require.NoError(t, err)
33+
34+
os.Stdout = stdoutW
35+
os.Stderr = stderrW
36+
37+
co := &CapturedOutput{
38+
stdoutReader: stdoutR,
39+
stdoutWriter: stdoutW,
40+
stderrReader: stderrR,
41+
stderrWriter: stderrW,
42+
}
43+
co.wg.Add(1)
44+
go func() {
45+
defer co.wg.Done()
46+
_, _ = io.Copy(&co.stdoutBuf, stdoutR)
47+
}()
48+
49+
co.wg.Add(1)
50+
go func() {
51+
defer co.wg.Done()
52+
_, _ = io.Copy(&co.stderrBuf, stderrR)
53+
}()
54+
55+
return co
56+
}
57+
58+
// Done waits until all captured output has been written to buffers,
59+
// and then returns the buffers.
60+
func (co *CapturedOutput) Done() (stdout string, stderr string) {
61+
// we need to close writers for readers to stop
62+
_ = co.stdoutWriter.Close()
63+
_ = co.stderrWriter.Close()
64+
65+
co.wg.Wait()
66+
67+
return co.stdoutBuf.String(), co.stderrBuf.String()
68+
}

0 commit comments

Comments
 (0)