Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
16 changes: 9 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -46,7 +47,7 @@ func main() {
}

// Process vars
vars, err := ParseVars(*varsArg)
vars, err := varsArg.VarNames()
if err != nil {
log.Fatal(err)
}
Expand Down Expand Up @@ -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)
}
59 changes: 56 additions & 3 deletions utils.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"errors"
"fmt"
"net/url"
"path/filepath"
Expand All @@ -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 == ',' })
Expand Down Expand Up @@ -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
}
128 changes: 127 additions & 1 deletion utils_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package main

import "testing"
import (
"fmt"
"reflect"
"strings"
"testing"
)

func TestUtils(t *testing.T) {
str := "memstats.Alloc,memstats.Sys"
Expand Down Expand Up @@ -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)
}
})
}
}