diff --git a/.gitignore b/.gitignore index 69fb5ee7..047644e1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ load-generator integration-tester dist /vendor +/.vscode diff --git a/cmd/process-exporter/main.go b/cmd/process-exporter/main.go index 7ca5a41e..6dfc0a08 100644 --- a/cmd/process-exporter/main.go +++ b/cmd/process-exporter/main.go @@ -179,6 +179,7 @@ func main() { "log debugging information to stdout") showVersion = flag.Bool("version", false, "print version information and exit") + removeEmptyGroups = flag.Bool("remove-empty-groups", false, "forget process groups with no processes") ) flag.Parse() @@ -240,14 +241,15 @@ func main() { pc, err := collector.NewProcessCollector( collector.ProcessCollectorOption{ - ProcFSPath: *procfsPath, - Children: *children, - Threads: *threads, - GatherSMaps: *smaps, - Namer: matchnamer, - Recheck: *recheck, - RecheckTimeLimit: *recheckTimeLimit, - Debug: *debug, + ProcFSPath: *procfsPath, + Children: *children, + Threads: *threads, + GatherSMaps: *smaps, + Namer: matchnamer, + Recheck: *recheck, + RecheckTimeLimit: *recheckTimeLimit, + Debug: *debug, + RemoveEmptyGroups: *removeEmptyGroups, }, ) if err != nil { diff --git a/collector/process_collector.go b/collector/process_collector.go index 6ef04405..8f41ba47 100644 --- a/collector/process_collector.go +++ b/collector/process_collector.go @@ -156,14 +156,15 @@ type ( } ProcessCollectorOption struct { - ProcFSPath string - Children bool - Threads bool - GatherSMaps bool - Namer common.MatchNamer - Recheck bool - RecheckTimeLimit time.Duration - Debug bool + ProcFSPath string + Children bool + Threads bool + GatherSMaps bool + Namer common.MatchNamer + Recheck bool + RecheckTimeLimit time.Duration + Debug bool + RemoveEmptyGroups bool } NamedProcessCollector struct { @@ -188,7 +189,7 @@ func NewProcessCollector(options ProcessCollectorOption) (*NamedProcessCollector fs.GatherSMaps = options.GatherSMaps p := &NamedProcessCollector{ scrapeChan: make(chan scrapeRequest), - Grouper: proc.NewGrouper(options.Namer, options.Children, options.Threads, options.Recheck, options.RecheckTimeLimit, options.Debug), + Grouper: proc.NewGrouper(options.Namer, options.Children, options.Threads, options.Recheck, options.RecheckTimeLimit, options.Debug, options.RemoveEmptyGroups), source: fs, threads: options.Threads, smaps: options.GatherSMaps, diff --git a/proc/grouper.go b/proc/grouper.go index 1b507209..bbf240ca 100644 --- a/proc/grouper.go +++ b/proc/grouper.go @@ -13,10 +13,11 @@ type ( Grouper struct { // groupAccum records the historical accumulation of a group so that // we can avoid ever decreasing the counts we return. - groupAccum map[string]Counts - tracker *Tracker - threadAccum map[string]map[string]Threads - debug bool + groupAccum map[string]Counts + tracker *Tracker + threadAccum map[string]map[string]Threads + debug bool + removeEmptyGroups bool } // GroupByName maps group name to group metrics. @@ -49,12 +50,13 @@ type ( func lessThreads(x, y Threads) bool { return seq.Compare(x, y) < 0 } // NewGrouper creates a grouper. -func NewGrouper(namer common.MatchNamer, trackChildren, trackThreads, recheck bool, recheckTimeLimit time.Duration, debug bool) *Grouper { +func NewGrouper(namer common.MatchNamer, trackChildren, trackThreads, recheck bool, recheckTimeLimit time.Duration, debug bool, removeEmptyGroups bool) *Grouper { g := Grouper{ - groupAccum: make(map[string]Counts), - threadAccum: make(map[string]map[string]Threads), - tracker: NewTracker(namer, trackChildren, recheck, recheckTimeLimit, debug), - debug: debug, + groupAccum: make(map[string]Counts), + threadAccum: make(map[string]map[string]Threads), + tracker: NewTracker(namer, trackChildren, recheck, recheckTimeLimit, debug), + debug: debug, + removeEmptyGroups: removeEmptyGroups, } return &g } @@ -96,10 +98,10 @@ func groupadd(grp Group, ts Update) Group { // These are aggregated by groupname, augmented by accumulated counts // from the past, and returned. Note that while the Tracker reports // only what counts have changed since last cycle, Grouper.Update -// returns counts that never decrease. Even once the last process -// with name X disappears, name X will still appear in the results -// with the same counts as before; of course, all non-count metrics -// will be zero. +// returns counts that never decrease. If removeEmptyGroups is false, +// then even once the last process with name X disappears, name X will +// still appear in the results with the same counts as before; of course, +// all non-count metrics will be zero. func (g *Grouper) Update(iter Iter) (CollectErrors, GroupByName, error) { cerrs, tracked, err := g.tracker.Update(iter) if err != nil { @@ -132,10 +134,15 @@ func (g *Grouper) groups(tracked []Update) GroupByName { groups[gname] = group } - // Now add any groups that were observed in the past but aren't running now. + // Now add any groups that were observed in the past but aren't running now (or delete them, if removeEmptyGroups is true). for gname, gcounts := range g.groupAccum { if _, ok := groups[gname]; !ok { - groups[gname] = Group{Counts: gcounts} + if g.removeEmptyGroups { + delete(g.groupAccum, gname) + delete(g.threadAccum, gname) + } else { + groups[gname] = Group{Counts: gcounts} + } } } diff --git a/proc/grouper_test.go b/proc/grouper_test.go index ff291aef..826d1e97 100644 --- a/proc/grouper_test.go +++ b/proc/grouper_test.go @@ -73,7 +73,7 @@ func TestGrouperBasic(t *testing.T) { }, } - gr := NewGrouper(newNamer(n1, n2), false, false, false, 0, false) + gr := NewGrouper(newNamer(n1, n2), false, false, false, 0, false, false) for i, tc := range tests { got := rungroup(t, gr, procInfoIter(tc.procs...)) if diff := cmp.Diff(got, tc.want); diff != "" { @@ -128,7 +128,7 @@ func TestGrouperProcJoin(t *testing.T) { }, } - gr := NewGrouper(newNamer(n1), false, false, false, 0, false) + gr := NewGrouper(newNamer(n1), false, false, false, 0, false, false) for i, tc := range tests { got := rungroup(t, gr, procInfoIter(tc.procs...)) if diff := cmp.Diff(got, tc.want); diff != "" { @@ -138,7 +138,7 @@ func TestGrouperProcJoin(t *testing.T) { } // TestGrouperNonDecreasing tests the disappearance of a process. Its previous -// contribution to the counts should not go away when that happens. +// contribution to the counts should not go away when that happens if removeEmptyGroups is false. func TestGrouperNonDecreasing(t *testing.T) { p1, p2 := 1, 2 n1, n2 := "g1", "g1" @@ -171,7 +171,49 @@ func TestGrouperNonDecreasing(t *testing.T) { }, } - gr := NewGrouper(newNamer(n1), false, false, false, 0, false) + gr := NewGrouper(newNamer(n1), false, false, false, 0, false, false) + for i, tc := range tests { + got := rungroup(t, gr, procInfoIter(tc.procs...)) + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("%d: curgroups differs: (-got +want)\n%s", i, diff) + } + } +} + +// TestGrouperNonDecreasing tests the disappearance of a process. +// We want the group to disappear if removeEmptyGroups is true. +func TestGrouperRemoveEmptyGroups(t *testing.T) { + p1, p2 := 1, 2 + n1, n2 := "g1", "g2" + starttime := time.Unix(0, 0).UTC() + + tests := []struct { + procs []IDInfo + want GroupByName + }{ + { + []IDInfo{ + piinfo(p1, n1, Counts{3, 4, 5, 6, 7, 8, 0, 0}, Memory{3, 4, 0, 0, 0}, Filedesc{4, 400}, 2), + piinfo(p2, n2, Counts{1, 1, 1, 1, 1, 1, 0, 0}, Memory{1, 2, 0, 0, 0}, Filedesc{40, 400}, 3), + }, + GroupByName{ + n1: Group{Counts{}, States{}, msi{}, 1, Memory{3, 4, 0, 0, 0}, starttime, 4, 0.01, 2, nil}, + n2: Group{Counts{}, States{}, msi{}, 1, Memory{1, 2, 0, 0, 0}, starttime, 40, 0.1, 3, nil}, + }, + }, { + []IDInfo{ + piinfo(p1, n1, Counts{4, 5, 6, 7, 8, 9, 0, 0}, Memory{1, 5, 0, 0, 0}, Filedesc{4, 400}, 2), + }, + GroupByName{ + n1: Group{Counts{1, 1, 1, 1, 1, 1, 0, 0}, States{}, msi{}, 1, Memory{1, 5, 0, 0, 0}, starttime, 4, 0.01, 2, nil}, + }, + }, { + []IDInfo{}, + GroupByName{}, + }, + } + + gr := NewGrouper(newNamer(n1, n2), false, false, false, 0, false, true) for i, tc := range tests { got := rungroup(t, gr, procInfoIter(tc.procs...)) if diff := cmp.Diff(got, tc.want); diff != "" { @@ -224,7 +266,7 @@ func TestGrouperThreads(t *testing.T) { } opts := cmpopts.SortSlices(lessThreads) - gr := NewGrouper(newNamer(n), false, true, false, 0, false) + gr := NewGrouper(newNamer(n), false, true, false, 0, false, false) for i, tc := range tests { got := rungroup(t, gr, procInfoIter(tc.proc)) if diff := cmp.Diff(got, tc.want, opts); diff != "" {