diff --git a/README.md b/README.md index fb44ea9..759b3d5 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ If you need to monitor more (or less) vars, you can specify them with -vars comm ./expvarmon -ports="80" ./expvarmon -ports="23000-23010,http://example.com:80-81" -i=1m ./expvarmon -ports="80,remoteapp:80" -vars="mem:memstats.Alloc,duration:Response.Mean,Counter" + ./expvarmon -ports="80,remoteapp:80" -vars="mem:memstats.Alloc" -vars="duration:Response.Mean" -vars="Counter" ./expvarmon -ports="1234-1236" -vars="Goroutines" -self For more details and docs, see README: http://github.com/divan/expvarmon @@ -119,7 +120,7 @@ If your expvar endpoint is protected by Basic Auth, you have two options: ### Vars -Expvarmon doesn't restrict you to monitor only memstats. You can publish your own counters and variables using [expvar.Publish()](http://golang.org/pkg/expvar/#Publish) method or using expvar wrappers libraries. Just pass your variables names as they appear in JSON to -var command line flag. +Expvarmon doesn't restrict you to monitor only memstats. You can publish your own counters and variables using [expvar.Publish()](http://golang.org/pkg/expvar/#Publish) method or using expvar wrappers libraries. Just pass your variables names as they appear in JSON to -vars command line flag or pass multiple -vars flags. Notation is dot-separated, for example: **memstats.Alloc** for .MemStats.Alloc field. Quick link to runtime.MemStats documentation: http://golang.org/pkg/runtime/#MemStats diff --git a/main.go b/main.go index 26330ae..431ee09 100644 --- a/main.go +++ b/main.go @@ -14,13 +14,14 @@ import ( var ( interval = flag.Duration("i", 5*time.Second, "Polling interval") urls = flag.String("ports", "", "Ports/URLs for accessing services expvars (start-end,port2,port3,https://host:port)") - varsArg = flag.String("vars", "mem:memstats.Alloc,mem:memstats.Sys,mem:memstats.HeapAlloc,mem:memstats.HeapInuse,memstats.PauseNs,memstats.PauseEnd,duration:memstats.PauseTotalNs", "Vars to monitor (comma-separated)") + varsArg = VarsFlag{"mem:memstats.Alloc,mem:memstats.Sys,mem:memstats.HeapAlloc,mem:memstats.HeapInuse,memstats.PauseNs,memstats.PauseEnd,duration:memstats.PauseTotalNs"} dummy = flag.Bool("dummy", false, "Use dummy (console) output") self = flag.Bool("self", false, "Monitor itself") endpoint = flag.String("endpoint", DefaultEndpoint, "URL endpoint for expvars") ) func main() { + flag.Var(&varsArg, "vars", "Vars to monitor (comma-separated)") flag.Usage = Usage flag.Parse() @@ -46,7 +47,7 @@ func main() { } // Process vars - vars, err := ParseVars(*varsArg) + vars, err := varsArg.VarNames() if err != nil { log.Fatal(err) } @@ -115,11 +116,12 @@ func Usage() { flag.PrintDefaults() fmt.Fprintf(os.Stderr, ` Examples: - %s -ports="80" - %s -ports="23000-23010,http://example.com:80-81" -i=1m - %s -ports="80,remoteapp:80" -vars="mem:memstats.Alloc,duration:Response.Mean,Counter" - %s -ports="1234-1236" -vars="Goroutines" -self + %s -ports="80" + %s -ports="23000-23010,http://example.com:80-81" -i=1m + %s -ports="80,remoteapp:80" -vars="mem:memstats.Alloc,duration:Response.Mean,Counter" + %s -ports="80,remoteapp:80" -vars="mem:memstats.Alloc" -vars="duration:Response.Mean,Counter" # multiple vars input + %s -ports="1234-1236" -vars="Goroutines" -self For more details and docs, see README: http://github.com/divan/expvarmon -`, progname, progname, progname, progname) +`, progname, progname, progname, progname, progname) } diff --git a/utils.go b/utils.go index 8e4c935..531e1e6 100644 --- a/utils.go +++ b/utils.go @@ -1,7 +1,6 @@ package main import ( - "errors" "fmt" "net/url" "path/filepath" @@ -10,13 +9,18 @@ import ( "github.com/bsiegert/ranges" ) -var ErrParsePorts = fmt.Errorf("cannot parse ports argument") +var ( + // ErrParsePorts is the error returned if the ports are not passed or parsable + ErrParsePorts = fmt.Errorf("cannot parse ports argument") + // ErrNoVarsSpecified is the error returned when the vars are not specified + ErrNoVarsSpecified = fmt.Errorf("no vars specified") +) // ParseVars returns parsed and validated slice of strings with // variables names that will be used for monitoring. func ParseVars(vars string) ([]VarName, error) { if vars == "" { - return nil, errors.New("no vars specified") + return nil, ErrNoVarsSpecified } ss := strings.FieldsFunc(vars, func(r rune) bool { return r == ',' }) @@ -147,3 +151,52 @@ func NewURL(port string) url.URL { Path: "/debug/vars", } } + +// VarsFlag Can read from multiple vars flags +// Usage: go run main.go -vars "mem:memstats.Alloc" -vars ",mem:memstats.Sys" +type VarsFlag []string + +// Set appends the value into the array slice +// It trims the input from spaces and commas +func (i *VarsFlag) Set(value string) error { + *i = append(*i, strings.Trim(value, " ,")) + return nil +} + +// String joins the array with "," in order to keep the +func (i *VarsFlag) String() string { + return strings.Join([]string(*i), ",") +} + +// VarNames returns a slice of VarName array from the input vars +func (i *VarsFlag) VarNames() ([]VarName, error) { + var varSlice []VarName + if len(*i) == 0 { + return nil, ErrNoVarsSpecified + } + + for _, str := range *i { + vars, err := ParseVars(str) + if err != nil { + return nil, err + } + varSlice = append(varSlice, vars...) + } + + return uniqVarNameSlice(varSlice), nil +} + +// uniqVarNameSlice returns a unique set of its values +func uniqVarNameSlice(input []VarName) []VarName { + uniq := make(map[VarName]struct{}) + ret := make([]VarName, 0, len(uniq)) + + for _, v := range input { + if _, ok := uniq[v]; !ok { + uniq[v] = struct{}{} + ret = append(ret, v) + } + } + + return ret +} diff --git a/utils_test.go b/utils_test.go index f99214f..4e11a6f 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,6 +1,11 @@ package main -import "testing" +import ( + "fmt" + "reflect" + "strings" + "testing" +) func TestUtils(t *testing.T) { str := "memstats.Alloc,memstats.Sys" @@ -119,3 +124,124 @@ func TestPorts(t *testing.T) { t.Fatalf("ParsePorts returns wrong data: %v", ports) } } + +func TestVarsFlagStringRep(t *testing.T) { + testTable := []struct { + input, expected string + }{ + {"test", "test"}, + {"test ", "test"}, + {" test ", "test"}, + {", test ,", "test"}, + {",test", "test"}, + {"test,", "test"}, + } + for _, tc := range testTable { + t.Run(tc.input, func(t *testing.T) { + v := VarsFlag{} + v.Set(tc.input) + if r := v.String(); r != tc.expected { + t.Errorf("got [%s], want [%s]", r, tc.expected) + + } + }) + } +} + +func TestVarsFlagMultiInputStringRep(t *testing.T) { + testTable := []struct { + input []string + expected string + }{ + {[]string{""}, ""}, + {[]string{"test1", "test2"}, "test1,test2"}, + {[]string{"test1 ", "test2"}, "test1,test2"}, + {[]string{"test1 ", " test2"}, "test1,test2"}, + {[]string{" test1 ", " test2 "}, "test1,test2"}, + {[]string{",test1,", " test2 "}, "test1,test2"}, + {[]string{",test1,", ",test2,"}, "test1,test2"}, + } + for _, tc := range testTable { + name := strings.Join(tc.input, ",") + t.Run(name, func(t *testing.T) { + v := VarsFlag{} + for _, s := range tc.input { + v.Set(s) + } + if r := v.String(); r != tc.expected { + t.Errorf("got [%v], want [%v]", r, tc.expected) + } + }) + } +} + +// This test is for uniqVarNameSlice function +func TestUniqVarNameSlice(t *testing.T) { + testTable := []struct { + input, expected []VarName + }{ + {[]VarName{""}, []VarName{""}}, + {[]VarName{"test1", "test2"}, []VarName{"test1", "test2"}}, + {[]VarName{"test1", "test1"}, []VarName{"test1"}}, + {[]VarName{"test1", "test1", ""}, []VarName{"test1", ""}}, + } + for i, tc := range testTable { + name := fmt.Sprintf("Case %d", i) + t.Run(name, func(t *testing.T) { + if r := uniqVarNameSlice(tc.input); !reflect.DeepEqual(r, tc.expected) { + t.Errorf("got [%v], want [%v]", r, tc.expected) + } + }) + } +} + +func TestVarNamesErrors(t *testing.T) { + testTable := []struct { + input []string + expected error + }{ + {[]string{""}, ErrNoVarsSpecified}, + {[]string{}, ErrNoVarsSpecified}, + {[]string{"test1"}, nil}, + {[]string{"test1", "test1"}, nil}, + } + for i, tc := range testTable { + name := fmt.Sprintf("Case %d", i) + t.Run(name, func(t *testing.T) { + v := VarsFlag{} + for _, s := range tc.input { + v.Set(s) + } + + if _, err := v.VarNames(); err != tc.expected { + t.Errorf("got [%v], want [%v]", err, tc.expected) + } + }) + } + +} + +// This test is for VarNames method +func TestUniqueVarNames(t *testing.T) { + testTable := []struct { + input []string + expected []VarName + }{ + {[]string{"test1"}, []VarName{"test1"}}, + {[]string{"test1", "test2"}, []VarName{"test1", "test2"}}, + {[]string{"test1", "test1"}, []VarName{"test1"}}, + {[]string{"test1,test2"}, []VarName{"test1", "test2"}}, + } + for _, tc := range testTable { + name := strings.Join(tc.input, ",") + t.Run(name, func(t *testing.T) { + v := VarsFlag{} + for _, s := range tc.input { + v.Set(s) + } + if r, _ := v.VarNames(); !reflect.DeepEqual(r, tc.expected) { + t.Errorf("got [%#v], want [%#v]", r, tc.expected) + } + }) + } +}