@cryptotaxi247 / netdata-1 / commits / 50c3b9181

go.d systemdunits add unit files state (#17606)

Ilya Mashchenko committed May 7, 2024 at 12:16 UTC 50c3b9181284938ebe225fbd191030b7fb7baf9c
12 files changed +674 -222
src/go/collectors/go.d.plugin/modules/systemdunits/charts.go
+83 -41
@@ -7,6 +7,8 @@ package systemdunits
7
8 import (
9 "fmt"
10 + "path/filepath"
11 + "strings"
12
13 "github.com/netdata/netdata/go/go.d.plugin/agent/module"
14
@@ -15,62 +17,102 @@ import (
17 )
18
19 const (
18 - prioServiceUnitState = module.Priority + iota
19 - prioSocketUnitState
20 - prioTargetUnitState
21 - prioPathUnitState
22 - prioDeviceUnitState
23 - prioMountUnitState
24 - prioAutomountUnitState
25 - prioSwapUnitState
26 - prioTimerUnitState
27 - prioScopeUnitState
28 - prioSliceUnitState
20 + prioUnitState = module.Priority + iota
21 + prioUnitFileState
22 )
23
31 -var prioMap = map[string]int{
32 - unitTypeService: prioServiceUnitState,
33 - unitTypeSocket: prioSocketUnitState,
34 - unitTypeTarget: prioTargetUnitState,
35 - unitTypePath: prioPathUnitState,
36 - unitTypeDevice: prioDeviceUnitState,
37 - unitTypeMount: prioMountUnitState,
38 - unitTypeAutomount: prioAutomountUnitState,
39 - unitTypeSwap: prioSwapUnitState,
40 - unitTypeTimer: prioTimerUnitState,
41 - unitTypeScope: prioScopeUnitState,
42 - unitTypeSlice: prioSliceUnitState,
43 -}
44 -
45 -func newTypedUnitStateChartTmpl(name, typ string) *module.Chart {
24 +func (s *SystemdUnits) addUnitCharts(name, typ string) {
25 chart := module.Chart{
47 - ID: fmt.Sprintf("unit_%s_%s_state", name, typ),
48 - Title: fmt.Sprintf("%s Unit State", cases.Title(language.English, cases.Compact).String(typ)),
26 + ID: "unit_%s_%s_state",
27 + Title: "%s Unit State",
28 Units: "state",
50 - Fam: fmt.Sprintf("%s units", typ),
51 - Ctx: fmt.Sprintf("systemd.%s_unit_state", typ),
52 - Priority: prioMap[typ],
29 + Fam: "%s units",
30 + Ctx: "systemd.%s_unit_state",
31 + Priority: prioUnitState,
32 Labels: []module.Label{
33 {Key: "unit_name", Value: name},
34 },
35 Dims: module.Dims{
57 - {Name: unitStateActive},
58 - {Name: unitStateInactive},
59 - {Name: unitStateActivating},
60 - {Name: unitStateDeactivating},
61 - {Name: unitStateFailed},
36 + {ID: "unit_%s_%s_state_%s", Name: unitStateActive},
37 + {ID: "unit_%s_%s_state_%s", Name: unitStateInactive},
38 + {ID: "unit_%s_%s_state_%s", Name: unitStateActivating},
39 + {ID: "unit_%s_%s_state_%s", Name: unitStateDeactivating},
40 + {ID: "unit_%s_%s_state_%s", Name: unitStateFailed},
41 },
42 }
43 +
44 + chart.ID = fmt.Sprintf(chart.ID, name, typ)
45 + chart.Title = fmt.Sprintf(chart.Title, cases.Title(language.English, cases.Compact).String(typ))
46 + chart.Fam = fmt.Sprintf(chart.Fam, typ)
47 + chart.Ctx = fmt.Sprintf(chart.Ctx, typ)
48 +
49 for _, d := range chart.Dims {
65 - d.ID = fmt.Sprintf("unit_%s_%s_state_%s", name, typ, d.Name)
50 + d.ID = fmt.Sprintf(d.ID, name, typ, d.Name)
51 + }
52 +
53 + if err := s.Charts().Add(&chart); err != nil {
54 + s.Warning(err)
55 }
67 - return &chart
56 }
57
70 -func (s *SystemdUnits) addUnitToCharts(name, typ string) {
71 - chart := newTypedUnitStateChartTmpl(name, typ)
58 +func (s *SystemdUnits) removeUnitCharts(name, typ string) {
59 + px := fmt.Sprintf("unit_%s_%s_", name, typ)
60 + s.removeCharts(px)
61 +}
62 +
63 +func (s *SystemdUnits) addUnitFileCharts(unitPath string) {
64 + _, unitName := filepath.Split(unitPath)
65 + unitType := strings.TrimPrefix(filepath.Ext(unitPath), ".")
66 +
67 + chart := module.Chart{
68 + ID: "unit_file_%s_state",
69 + Title: "Unit File State",
70 + Units: "state",
71 + Fam: "unit files",
72 + Ctx: "systemd.unit_file_state",
73 + Type: module.Line,
74 + Priority: prioUnitFileState,
75 + Labels: []module.Label{
76 + {Key: "unit_file_name", Value: unitName},
77 + {Key: "unit_file_type", Value: unitType},
78 + },
79 + Dims: module.Dims{
80 + {ID: "unit_file_%s_state_enabled", Name: "enabled"},
81 + {ID: "unit_file_%s_state_enabled-runtime", Name: "enabled-runtime"},
82 + {ID: "unit_file_%s_state_linked", Name: "linked"},
83 + {ID: "unit_file_%s_state_linked-runtime", Name: "linked-runtime"},
84 + {ID: "unit_file_%s_state_alias", Name: "alias"},
85 + {ID: "unit_file_%s_state_masked", Name: "masked"},
86 + {ID: "unit_file_%s_state_masked-runtime", Name: "masked-runtime"},
87 + {ID: "unit_file_%s_state_static", Name: "static"},
88 + {ID: "unit_file_%s_state_disabled", Name: "disabled"},
89 + {ID: "unit_file_%s_state_indirect", Name: "indirect"},
90 + {ID: "unit_file_%s_state_generated", Name: "generated"},
91 + {ID: "unit_file_%s_state_transient", Name: "transient"},
92 + {ID: "unit_file_%s_state_bad", Name: "bad"},
93 + },
94 + }
95 +
96 + chart.ID = fmt.Sprintf(chart.ID, strings.ReplaceAll(unitPath, ".", "_"))
97 + for _, dim := range chart.Dims {
98 + dim.ID = fmt.Sprintf(dim.ID, unitPath)
99 + }
100
73 - if err := s.Charts().Add(chart); err != nil {
101 + if err := s.Charts().Add(&chart); err != nil {
102 s.Warning(err)
103 }
104 }
105 +
106 +func (s *SystemdUnits) removeUnitFileCharts(unitPath string) {
107 + px := fmt.Sprintf("unit_file_%s_", strings.ReplaceAll(unitPath, ".", "_"))
108 + s.removeCharts(px)
109 +}
110 +
111 +func (s *SystemdUnits) removeCharts(prefix string) {
112 + for _, chart := range *s.Charts() {
113 + if strings.HasPrefix(chart.ID, prefix) {
114 + chart.MarkRemove()
115 + chart.MarkNotCreated()
116 + }
117 + }
118 +}
src/go/collectors/go.d.plugin/modules/systemdunits/client.go
+1
@@ -19,6 +19,7 @@ type systemdConnection interface {
19 GetManagerProperty(string) (string, error)
20 ListUnitsContext(ctx context.Context) ([]dbus.UnitStatus, error)
21 ListUnitsByPatternsContext(ctx context.Context, states []string, patterns []string) ([]dbus.UnitStatus, error)
22 + ListUnitFilesByPatternsContext(ctx context.Context, states []string, patterns []string) ([]dbus.UnitFile, error)
23 }
24
25 type systemdDBusClient struct{}
src/go/collectors/go.d.plugin/modules/systemdunits/collect.go
+8 -131
@@ -6,45 +6,9 @@
6 package systemdunits
7
8 import (
9 - "context"
9 "fmt"
10 "regexp"
11 "strconv"
13 - "strings"
14 -
15 - "github.com/coreos/go-systemd/v22/dbus"
16 -)
17 -
18 -const (
19 - // https://www.freedesktop.org/software/systemd/man/systemd.html
20 - unitStateActive = "active"
21 - unitStateInactive = "inactive"
22 - unitStateActivating = "activating"
23 - unitStateDeactivating = "deactivating"
24 - unitStateFailed = "failed"
25 -
26 - // https://www.freedesktop.org/software/systemd/man/systemd.html
27 - unitTypeService = "service"
28 - unitTypeSocket = "socket"
29 - unitTypeTarget = "target"
30 - unitTypePath = "path"
31 - unitTypeDevice = "device"
32 - unitTypeMount = "mount"
33 - unitTypeAutomount = "automount"
34 - unitTypeSwap = "swap"
35 - unitTypeTimer = "timer"
36 - unitTypeScope = "scope"
37 - unitTypeSlice = "slice"
38 -)
39 -
40 -var (
41 - unitStates = []string{
42 - unitStateActive,
43 - unitStateActivating,
44 - unitStateFailed,
45 - unitStateInactive,
46 - unitStateDeactivating,
47 - }
12 )
13
14 func (s *SystemdUnits) collect() (map[string]int64, error) {
@@ -62,47 +26,23 @@ func (s *SystemdUnits) collect() (map[string]int64, error) {
26 s.systemdVersion = ver
27 }
28
65 - var units []dbus.UnitStatus
66 - if s.systemdVersion >= 230 {
67 - // https://github.com/systemd/systemd/pull/3142
68 - units, err = s.getLoadedUnitsByPatterns(conn)
69 - } else {
70 - units, err = s.getLoadedUnits(conn)
71 - }
72 - if err != nil {
29 + mx := make(map[string]int64)
30 +
31 + if err := s.collectUnits(mx, conn); err != nil {
32 s.closeConnection()
33 return nil, err
34 }
35
77 - if len(units) == 0 {
78 - return nil, nil
36 + if s.CollectUnitFiles && len(s.IncludeUnitFiles) > 0 {
37 + if err := s.collectUnitFiles(mx, conn); err != nil {
38 + s.closeConnection()
39 + return mx, err
40 + }
41 }
42
81 - mx := make(map[string]int64)
82 - s.collectUnitsStates(mx, units)
83 -
43 return mx, nil
44 }
45
87 -func (s *SystemdUnits) collectUnitsStates(mx map[string]int64, units []dbus.UnitStatus) {
88 - for _, unit := range units {
89 - name, typ := extractUnitNameType(cleanUnitName(unit.Name))
90 - if name == "" || typ == "" {
91 - continue
92 - }
93 -
94 - if !s.units[unit.Name] {
95 - s.units[unit.Name] = true
96 - s.addUnitToCharts(name, typ)
97 - }
98 -
99 - for _, s := range unitStates {
100 - mx[fmt.Sprintf("unit_%s_%s_state_%s", name, typ, s)] = 0
101 - }
102 - mx[fmt.Sprintf("unit_%s_%s_state_%s", name, typ, unit.ActiveState)] = 1
103 - }
104 -}
105 -
46 func (s *SystemdUnits) getConnection() (systemdConnection, error) {
47 if s.conn == nil {
48 conn, err := s.client.connect()
@@ -146,66 +86,3 @@ func (s *SystemdUnits) getSystemdVersion(conn systemdConnection) (int, error) {
86
87 return ver, nil
88 }
149 -
150 -func (s *SystemdUnits) getLoadedUnits(conn systemdConnection) ([]dbus.UnitStatus, error) {
151 - ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
152 - defer cancel()
153 -
154 - s.Debugf("calling function 'ListUnits'")
155 - units, err := conn.ListUnitsContext(ctx)
156 - if err != nil {
157 - return nil, fmt.Errorf("error on ListUnits: %v", err)
158 - }
159 -
160 - loaded := units[:0]
161 - for _, unit := range units {
162 - if unit.LoadState == "loaded" && s.sr.MatchString(unit.Name) {
163 - loaded = append(loaded, unit)
164 - }
165 - }
166 - s.Debugf("got total/loaded %d/%d units", len(units), len(loaded))
167 -
168 - return loaded, nil
169 -}
170 -
171 -func (s *SystemdUnits) getLoadedUnitsByPatterns(conn systemdConnection) ([]dbus.UnitStatus, error) {
172 - ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
173 - defer cancel()
174 -
175 - s.Debugf("calling function 'ListUnitsByPatterns'")
176 -
177 - units, err := conn.ListUnitsByPatternsContext(ctx, unitStates, s.Include)
178 - if err != nil {
179 - return nil, fmt.Errorf("error on ListUnitsByPatterns: %v", err)
180 - }
181 -
182 - loaded := units[:0]
183 - for _, unit := range units {
184 - if unit.LoadState == "loaded" {
185 - loaded = append(loaded, unit)
186 - }
187 - }
188 - s.Debugf("got total/loaded %d/%d units", len(units), len(loaded))
189 -
190 - return loaded, nil
191 -}
192 -
193 -func extractUnitNameType(name string) (string, string) {
194 - idx := strings.LastIndexByte(name, '.')
195 - if idx <= 0 {
196 - return "", ""
197 - }
198 - return name[:idx], name[idx+1:]
199 -}
200 -
201 -func cleanUnitName(name string) string {
202 - // dev-disk-by\x2duuid-DE44\x2dCEE0.device => dev-disk-by-uuid-DE44-CEE0.device
203 - if strings.IndexByte(name, '\\') == -1 {
204 - return name
205 - }
206 - v, err := strconv.Unquote("\"" + name + "\"")
207 - if err != nil {
208 - return name
209 - }
210 - return v
211 -}
src/go/collectors/go.d.plugin/modules/systemdunits/collect_unit_files.go new
+94
@@ -0,0 +1,94 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +//go:build linux
4 +// +build linux
5 +
6 +package systemdunits
7 +
8 +import (
9 + "context"
10 + "fmt"
11 + "strings"
12 + "time"
13 +
14 + "github.com/coreos/go-systemd/v22/dbus"
15 +)
16 +
17 +// https://github.com/systemd/systemd/blob/3d320785c4bbba74459096b07e85a79c4f0cdffb/src/shared/install.c#L3785
18 +// see "is-enabled" in https://www.man7.org/linux/man-pages/man1/systemctl.1.html
19 +var unitFileStates = []string{
20 + "enabled",
21 + "enabled-runtime",
22 + "linked",
23 + "linked-runtime",
24 + "alias",
25 + "masked",
26 + "masked-runtime",
27 + "static",
28 + "disabled",
29 + "indirect",
30 + "generated",
31 + "transient",
32 + "bad",
33 +}
34 +
35 +func (s *SystemdUnits) collectUnitFiles(mx map[string]int64, conn systemdConnection) error {
36 + if s.systemdVersion < 230 {
37 + return nil
38 + }
39 +
40 + if now := time.Now(); now.After(s.lastListUnitFilesTime.Add(s.CollectUnitFilesEvery.Duration())) {
41 + unitFiles, err := s.getUnitFilesByPatterns(conn)
42 + if err != nil {
43 + return err
44 + }
45 + s.lastListUnitFilesTime = now
46 + s.cachedUnitFiles = unitFiles
47 + }
48 +
49 + seen := make(map[string]bool)
50 +
51 + for _, unitFile := range s.cachedUnitFiles {
52 + seen[unitFile.Path] = true
53 +
54 + if !s.seenUnitFiles[unitFile.Path] {
55 + s.seenUnitFiles[unitFile.Path] = true
56 + s.addUnitFileCharts(unitFile.Path)
57 + }
58 +
59 + px := fmt.Sprintf("unit_file_%s_state_", unitFile.Path)
60 + for _, st := range unitFileStates {
61 + mx[px+st] = 0
62 + }
63 + mx[px+strings.ToLower(unitFile.Type)] = 1
64 + }
65 +
66 + for k := range s.seenUnitFiles {
67 + if !seen[k] {
68 + delete(s.seenUnitFiles, k)
69 + s.removeUnitFileCharts(k)
70 + }
71 + }
72 +
73 + return nil
74 +}
75 +
76 +func (s *SystemdUnits) getUnitFilesByPatterns(conn systemdConnection) ([]dbus.UnitFile, error) {
77 + ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
78 + defer cancel()
79 +
80 + s.Debugf("calling function 'ListUnitFilesByPatterns'")
81 +
82 + unitFiles, err := conn.ListUnitFilesByPatternsContext(ctx, nil, []string{"*.service"})
83 + if err != nil {
84 + return nil, fmt.Errorf("error on ListUnitFilesByPatterns: %v", err)
85 + }
86 +
87 + for i := range unitFiles {
88 + unitFiles[i].Path = cleanUnitName(unitFiles[i].Path)
89 + }
90 +
91 + s.Debugf("got %d unit files", len(unitFiles))
92 +
93 + return unitFiles, nil
94 +}
src/go/collectors/go.d.plugin/modules/systemdunits/collect_units.go new
+151
@@ -0,0 +1,151 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +//go:build linux
4 +// +build linux
5 +
6 +package systemdunits
7 +
8 +import (
9 + "context"
10 + "fmt"
11 + "strconv"
12 + "strings"
13 +
14 + "github.com/coreos/go-systemd/v22/dbus"
15 +)
16 +
17 +const (
18 + // https://www.freedesktop.org/software/systemd/man/systemd.html
19 + unitStateActive = "active"
20 + unitStateInactive = "inactive"
21 + unitStateActivating = "activating"
22 + unitStateDeactivating = "deactivating"
23 + unitStateFailed = "failed"
24 +)
25 +
26 +var unitStates = []string{
27 + unitStateActive,
28 + unitStateActivating,
29 + unitStateFailed,
30 + unitStateInactive,
31 + unitStateDeactivating,
32 +}
33 +
34 +func (s *SystemdUnits) collectUnits(mx map[string]int64, conn systemdConnection) error {
35 + var units []dbus.UnitStatus
36 + var err error
37 +
38 + if s.systemdVersion >= 230 {
39 + // https://github.com/systemd/systemd/pull/3142
40 + units, err = s.getLoadedUnitsByPatterns(conn)
41 + } else {
42 + units, err = s.getLoadedUnits(conn)
43 + }
44 + if err != nil {
45 + return err
46 + }
47 +
48 + seen := make(map[string]bool)
49 +
50 + for _, unit := range units {
51 + name, typ, ok := extractUnitNameType(unit.Name)
52 + if !ok {
53 + continue
54 + }
55 +
56 + seen[unit.Name] = true
57 +
58 + if !s.seenUnits[unit.Name] {
59 + s.seenUnits[unit.Name] = true
60 + s.addUnitCharts(name, typ)
61 + }
62 +
63 + for _, s := range unitStates {
64 + mx[fmt.Sprintf("unit_%s_%s_state_%s", name, typ, s)] = 0
65 + }
66 + mx[fmt.Sprintf("unit_%s_%s_state_%s", name, typ, unit.ActiveState)] = 1
67 + }
68 +
69 + for k := range s.seenUnits {
70 + if !seen[k] {
71 + delete(s.seenUnits, k)
72 + if name, typ, ok := extractUnitNameType(k); ok {
73 + s.removeUnitCharts(name, typ)
74 + }
75 + }
76 + }
77 +
78 + return nil
79 +}
80 +
81 +func (s *SystemdUnits) getLoadedUnits(conn systemdConnection) ([]dbus.UnitStatus, error) {
82 + ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
83 + defer cancel()
84 +
85 + s.Debugf("calling function 'ListUnits'")
86 + units, err := conn.ListUnitsContext(ctx)
87 + if err != nil {
88 + return nil, fmt.Errorf("error on ListUnits: %v", err)
89 + }
90 +
91 + for i := range units {
92 + units[i].Name = cleanUnitName(units[i].Name)
93 + }
94 +
95 + loaded := units[:0]
96 + for _, unit := range units {
97 + if unit.LoadState == "loaded" && s.unitSr.MatchString(unit.Name) {
98 + loaded = append(loaded, unit)
99 + }
100 + }
101 +
102 + s.Debugf("got total/loaded %d/%d units", len(units), len(loaded))
103 +
104 + return loaded, nil
105 +}
106 +
107 +func (s *SystemdUnits) getLoadedUnitsByPatterns(conn systemdConnection) ([]dbus.UnitStatus, error) {
108 + ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
109 + defer cancel()
110 +
111 + s.Debugf("calling function 'ListUnitsByPatterns'")
112 +
113 + units, err := conn.ListUnitsByPatternsContext(ctx, unitStates, s.Include)
114 + if err != nil {
115 + return nil, fmt.Errorf("error on ListUnitsByPatterns: %v", err)
116 + }
117 +
118 + for i := range units {
119 + units[i].Name = cleanUnitName(units[i].Name)
120 + }
121 +
122 + loaded := units[:0]
123 + for _, unit := range units {
124 + if unit.LoadState == "loaded" {
125 + loaded = append(loaded, unit)
126 + }
127 + }
128 + s.Debugf("got total/loaded %d/%d units", len(units), len(loaded))
129 +
130 + return loaded, nil
131 +}
132 +
133 +func extractUnitNameType(name string) (string, string, bool) {
134 + idx := strings.LastIndexByte(name, '.')
135 + if idx <= 0 {
136 + return "", "", false
137 + }
138 + return name[:idx], name[idx+1:], true
139 +}
140 +
141 +func cleanUnitName(name string) string {
142 + // dev-disk-by\x2duuid-DE44\x2dCEE0.device => dev-disk-by-uuid-DE44-CEE0.device
143 + if strings.IndexByte(name, '\\') == -1 {
144 + return name
145 + }
146 + v, err := strconv.Unquote("\"" + name + "\"")
147 + if err != nil {
148 + return name
149 + }
150 + return v
151 +}
src/go/collectors/go.d.plugin/modules/systemdunits/config_schema.json
+54
@@ -34,6 +34,36 @@
34 "default": [
35 "*.service"
36 ]
37 + },
38 + "collect_unit_files": {
39 + "title": "Collect unit files",
40 + "description": "If set, collect the state of installed unit files. **Enabling this may increase system overhead**, particularly if the pattern matches a large number of unit files.",
41 + "type": "boolean",
42 + "default": false
43 + },
44 + "collect_unit_files_every": {
45 + "title": "Unit files polling interval",
46 + "description": "Interval for querying systemd about unit files and their enablement state, measured in seconds. Data is cached for this interval to reduce system overhead.",
47 + "type": "number",
48 + "minimum": 1,
49 + "default": 300
50 + },
51 + "include_unit_files": {
52 + "title": "Include unit files",
53 + "description": "Configuration for monitoring specific systemd unit files. Include systemd unit files whose names match any of the specified [patterns](https://golang.org/pkg/path/filepath/#Match).",
54 + "type": [
55 + "array",
56 + "null"
57 + ],
58 + "uniqueItems": true,
59 + "minItems": 1,
60 + "items": {
61 + "title": "Unit file name pattern",
62 + "type": "string"
63 + },
64 + "default": [
65 + "*.service"
66 + ]
67 }
68 },
69 "required": [
@@ -48,11 +78,35 @@
78 "uiOptions": {
79 "fullPage": true
80 },
81 + "ui:flavour": "tabs",
82 + "ui:options": {
83 + "tabs": [
84 + {
85 + "title": "Base",
86 + "fields": [
87 + "update_every",
88 + "timeout",
89 + "include"
90 + ]
91 + },
92 + {
93 + "title": "Unit Files",
94 + "fields": [
95 + "collect_unit_files",
96 + "collect_unit_files_every",
97 + "include_unit_files"
98 + ]
99 + }
100 + ]
101 + },
102 "timeout": {
103 "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
104 },
105 "include": {
106 "ui:listFlavour": "list"
107 + },
108 + "include_unit_files": {
109 + "ui:listFlavour": "list"
110 }
111 }
112 }
src/go/collectors/go.d.plugin/modules/systemdunits/init.go
+1 -1
@@ -19,7 +19,7 @@ func (s *SystemdUnits) validateConfig() error {
19 return nil
20 }
21
22 -func (s *SystemdUnits) initSelector() (matcher.Matcher, error) {
22 +func (s *SystemdUnits) initUnitSelector() (matcher.Matcher, error) {
23 if len(s.Include) == 0 {
24 return matcher.TRUE(), nil
25 }
src/go/collectors/go.d.plugin/modules/systemdunits/metadata.yaml
+55 -5
@@ -21,7 +21,7 @@ modules:
21 overview:
22 data_collection:
23 metrics_description: |
24 - This collector monitors Systemd units state.
24 + This collector monitors the state of Systemd units and unit files.
25 method_description: ""
26 supported_platforms:
27 include: []
@@ -57,8 +57,12 @@ modules:
57 description: Recheck interval in seconds. Zero means no recheck will be scheduled.
58 default_value: 0
59 required: false
60 + - name: timeout
61 + description: System bus requests timeout.
62 + default_value: 1
63 + required: false
64 - name: include
61 - description: Systemd units filter.
65 + description: Systemd units selector.
66 default_value: "*.service"
67 required: false
68 detailed_description: |
@@ -73,10 +77,30 @@ modules:
77 - pattern1
78 - pattern2
79 ```
76 - - name: timeout
77 - description: System bus requests timeout.
78 - default_value: 1
80 + - name: collect_unit_files
81 + description: If set to true, collect the state of installed unit files. Enabling this may increase system overhead.
82 + default_value: "false"
83 required: false
84 + - name: collect_unit_files_every
85 + description: Interval for querying systemd about unit files and their enablement state, measured in seconds. Data is cached for this interval to reduce system overhead.
86 + default_value: 300
87 + required: false
88 + - name: include_unit_files
89 + description: Systemd unit files selector.
90 + default_value: "*.service"
91 + required: false
92 + detailed_description: |
93 + Systemd unit files matching the selector will be monitored.
94 +
95 + - Logic: (pattern1 OR pattern2)
96 + - Pattern syntax: [shell file name pattern](https://golang.org/pkg/path/filepath/#Match)
97 + - Syntax:
98 +
99 + ```yaml
100 + includes:
101 + - pattern1
102 + - pattern2
103 + ```
104 examples:
105 folding:
106 title: Config
@@ -288,3 +312,29 @@ modules:
312 - name: activating
313 - name: deactivating
314 - name: failed
315 + - name: unit file
316 + description: These metrics refer to the systemd unit file.
317 + labels:
318 + - name: unit_file_name
319 + description: systemd unit file name
320 + - name: unit_file_type
321 + description: systemd unit file type
322 + metrics:
323 + - name: systemd.unit_file_state
324 + description: Unit File State
325 + unit: state
326 + chart_type: line
327 + dimensions:
328 + - name: enabled
329 + - name: enabled-runtime
330 + - name: linked
331 + - name: linked-runtime
332 + - name: alias
333 + - name: masked
334 + - name: masked-runtime
335 + - name: static
336 + - name: disabled
337 + - name: indirect
338 + - name: generated
339 + - name: transient
340 + - name: bad
src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits.go
+33 -19
@@ -13,6 +13,8 @@ import (
13 "github.com/netdata/netdata/go/go.d.plugin/agent/module"
14 "github.com/netdata/netdata/go/go.d.plugin/pkg/matcher"
15 "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
16 +
17 + "github.com/coreos/go-systemd/v22/dbus"
18 )
19
20 //go:embed "config_schema.json"
@@ -31,22 +33,26 @@ func init() {
33 func New() *SystemdUnits {
34 return &SystemdUnits{
35 Config: Config{
34 - Timeout: web.Duration(time.Second * 2),
35 - Include: []string{
36 - "*.service",
37 - },
36 + Timeout: web.Duration(time.Second * 2),
37 + Include: []string{"*.service"},
38 + CollectUnitFiles: false,
39 + IncludeUnitFiles: []string{"*.service"},
40 + CollectUnitFilesEvery: web.Duration(time.Minute * 5),
41 },
39 -
40 - charts: &module.Charts{},
41 - client: newSystemdDBusClient(),
42 - units: make(map[string]bool),
42 + charts: &module.Charts{},
43 + client: newSystemdDBusClient(),
44 + seenUnits: make(map[string]bool),
45 + seenUnitFiles: make(map[string]bool),
46 }
47 }
48
49 type Config struct {
47 - UpdateEvery int `yaml:"update_every" json:"update_every"`
48 - Timeout web.Duration `yaml:"timeout" json:"timeout"`
49 - Include []string `yaml:"include" json:"include"`
50 + UpdateEvery int `yaml:"update_every" json:"update_every"`
51 + Timeout web.Duration `yaml:"timeout" json:"timeout"`
52 + Include []string `yaml:"include" json:"include"`
53 + CollectUnitFiles bool `yaml:"collect_unit_files" json:"collect_unit_files"`
54 + IncludeUnitFiles []string `yaml:"include_unit_files" json:"include_unit_files"`
55 + CollectUnitFilesEvery web.Duration `yaml:"collect_unit_files_every" json:"collect_unit_files_every"`
56 }
57
58 type SystemdUnits struct {
@@ -57,8 +63,13 @@ type SystemdUnits struct {
63 conn systemdConnection
64
65 systemdVersion int
60 - units map[string]bool
61 - sr matcher.Matcher
66 +
67 + seenUnits map[string]bool
68 + unitSr matcher.Matcher
69 +
70 + lastListUnitFilesTime time.Time
71 + cachedUnitFiles []dbus.UnitFile
72 + seenUnitFiles map[string]bool
73
74 charts *module.Charts
75 }
@@ -68,21 +79,22 @@ func (s *SystemdUnits) Configuration() any {
79 }
80
81 func (s *SystemdUnits) Init() error {
71 - err := s.validateConfig()
72 - if err != nil {
82 + if err := s.validateConfig(); err != nil {
83 s.Errorf("config validation: %v", err)
84 return err
85 }
86
77 - sr, err := s.initSelector()
87 + sr, err := s.initUnitSelector()
88 if err != nil {
79 - s.Errorf("init selector: %v", err)
89 + s.Errorf("init unit selector: %v", err)
90 return err
91 }
82 - s.sr = sr
92 + s.unitSr = sr
93
84 - s.Debugf("unit names patterns: %v", s.Include)
94 s.Debugf("timeout: %s", s.Timeout)
95 + s.Debugf("units: patterns '%v'", s.Include)
96 + s.Debugf("unit files: enabled '%v', every '%s', patterns: %v",
97 + s.CollectUnitFiles, s.CollectUnitFilesEvery, s.IncludeUnitFiles)
98
99 return nil
100 }
@@ -93,9 +105,11 @@ func (s *SystemdUnits) Check() error {
105 s.Error(err)
106 return err
107 }
108 +
109 if len(mx) == 0 {
110 return errors.New("no metrics collected")
111 }
112 +
113 return nil
114 }
115
src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits_test.go
+184 -24
@@ -11,6 +11,7 @@ import (
11 "fmt"
12 "os"
13 "path/filepath"
14 + "slices"
15 "testing"
16
17 "github.com/netdata/netdata/go/go.d.plugin/agent/module"
@@ -168,7 +169,7 @@ func TestSystemdUnits_Collect(t *testing.T) {
169 prepare func() *SystemdUnits
170 wantCollected map[string]int64
171 }{
171 - "success on systemd v230+ on collecting all unit type": {
172 + "success v230+ on collecting all unit type": {
173 prepare: func() *SystemdUnits {
174 systemd := New()
175 systemd.Include = []string{"*"}
@@ -383,7 +384,7 @@ func TestSystemdUnits_Collect(t *testing.T) {
384 "unit_var-lib-nfs-rpc_pipefs_mount_state_inactive": 1,
385 },
386 },
386 - "success on systemd v230- on collecting all unit types": {
387 + "success v230- on collecting all unit types": {
388 prepare: func() *SystemdUnits {
389 systemd := New()
390 systemd.Include = []string{"*"}
@@ -598,7 +599,7 @@ func TestSystemdUnits_Collect(t *testing.T) {
599 "unit_var-lib-nfs-rpc_pipefs_mount_state_inactive": 1,
600 },
601 },
601 - "success on systemd v230+ on collecting only 'service' unit type": {
602 + "success v230+ on collecting only 'service' units": {
603 prepare: func() *SystemdUnits {
604 systemd := New()
605 systemd.Include = []string{"*.service"}
@@ -628,7 +629,7 @@ func TestSystemdUnits_Collect(t *testing.T) {
629 "unit_user@1000_service_state_inactive": 0,
630 },
631 },
631 - "success on systemd v230- on collecting only 'service' unit type": {
632 + "success v230- on collecting only 'service' units": {
633 prepare: func() *SystemdUnits {
634 systemd := New()
635 systemd.Include = []string{"*.service"}
@@ -658,6 +659,89 @@ func TestSystemdUnits_Collect(t *testing.T) {
659 "unit_user@1000_service_state_inactive": 0,
660 },
661 },
662 + "success v230+ on collecting only 'service' units and files": {
663 + prepare: func() *SystemdUnits {
664 + systemd := New()
665 + systemd.Include = []string{"*.service"}
666 + systemd.CollectUnitFiles = true
667 + systemd.client = prepareOKClient(230)
668 + return systemd
669 + },
670 + wantCollected: map[string]int64{
671 + "unit_file_/lib/systemd/system/uuidd.service_state_alias": 0,
672 + "unit_file_/lib/systemd/system/uuidd.service_state_bad": 0,
673 + "unit_file_/lib/systemd/system/uuidd.service_state_disabled": 0,
674 + "unit_file_/lib/systemd/system/uuidd.service_state_enabled": 0,
675 + "unit_file_/lib/systemd/system/uuidd.service_state_enabled-runtime": 0,
676 + "unit_file_/lib/systemd/system/uuidd.service_state_generated": 0,
677 + "unit_file_/lib/systemd/system/uuidd.service_state_indirect": 1,
678 + "unit_file_/lib/systemd/system/uuidd.service_state_linked": 0,
679 + "unit_file_/lib/systemd/system/uuidd.service_state_linked-runtime": 0,
680 + "unit_file_/lib/systemd/system/uuidd.service_state_masked": 0,
681 + "unit_file_/lib/systemd/system/uuidd.service_state_masked-runtime": 0,
682 + "unit_file_/lib/systemd/system/uuidd.service_state_static": 0,
683 + "unit_file_/lib/systemd/system/uuidd.service_state_transient": 0,
684 + "unit_file_/lib/systemd/system/x11-common.service_state_alias": 0,
685 + "unit_file_/lib/systemd/system/x11-common.service_state_bad": 0,
686 + "unit_file_/lib/systemd/system/x11-common.service_state_disabled": 0,
687 + "unit_file_/lib/systemd/system/x11-common.service_state_enabled": 0,
688 + "unit_file_/lib/systemd/system/x11-common.service_state_enabled-runtime": 0,
689 + "unit_file_/lib/systemd/system/x11-common.service_state_generated": 0,
690 + "unit_file_/lib/systemd/system/x11-common.service_state_indirect": 0,
691 + "unit_file_/lib/systemd/system/x11-common.service_state_linked": 0,
692 + "unit_file_/lib/systemd/system/x11-common.service_state_linked-runtime": 0,
693 + "unit_file_/lib/systemd/system/x11-common.service_state_masked": 1,
694 + "unit_file_/lib/systemd/system/x11-common.service_state_masked-runtime": 0,
695 + "unit_file_/lib/systemd/system/x11-common.service_state_static": 0,
696 + "unit_file_/lib/systemd/system/x11-common.service_state_transient": 0,
697 + "unit_file_/run/systemd/generator.late/monit.service_state_alias": 0,
698 + "unit_file_/run/systemd/generator.late/monit.service_state_bad": 0,
699 + "unit_file_/run/systemd/generator.late/monit.service_state_disabled": 0,
700 + "unit_file_/run/systemd/generator.late/monit.service_state_enabled": 0,
701 + "unit_file_/run/systemd/generator.late/monit.service_state_enabled-runtime": 0,
702 + "unit_file_/run/systemd/generator.late/monit.service_state_generated": 1,
703 + "unit_file_/run/systemd/generator.late/monit.service_state_indirect": 0,
704 + "unit_file_/run/systemd/generator.late/monit.service_state_linked": 0,
705 + "unit_file_/run/systemd/generator.late/monit.service_state_linked-runtime": 0,
706 + "unit_file_/run/systemd/generator.late/monit.service_state_masked": 0,
707 + "unit_file_/run/systemd/generator.late/monit.service_state_masked-runtime": 0,
708 + "unit_file_/run/systemd/generator.late/monit.service_state_static": 0,
709 + "unit_file_/run/systemd/generator.late/monit.service_state_transient": 0,
710 + "unit_file_/run/systemd/generator.late/sendmail.service_state_alias": 0,
711 + "unit_file_/run/systemd/generator.late/sendmail.service_state_bad": 0,
712 + "unit_file_/run/systemd/generator.late/sendmail.service_state_disabled": 0,
713 + "unit_file_/run/systemd/generator.late/sendmail.service_state_enabled": 0,
714 + "unit_file_/run/systemd/generator.late/sendmail.service_state_enabled-runtime": 0,
715 + "unit_file_/run/systemd/generator.late/sendmail.service_state_generated": 1,
716 + "unit_file_/run/systemd/generator.late/sendmail.service_state_indirect": 0,
717 + "unit_file_/run/systemd/generator.late/sendmail.service_state_linked": 0,
718 + "unit_file_/run/systemd/generator.late/sendmail.service_state_linked-runtime": 0,
719 + "unit_file_/run/systemd/generator.late/sendmail.service_state_masked": 0,
720 + "unit_file_/run/systemd/generator.late/sendmail.service_state_masked-runtime": 0,
721 + "unit_file_/run/systemd/generator.late/sendmail.service_state_static": 0,
722 + "unit_file_/run/systemd/generator.late/sendmail.service_state_transient": 0,
723 + "unit_systemd-ask-password-wall_service_state_activating": 0,
724 + "unit_systemd-ask-password-wall_service_state_active": 0,
725 + "unit_systemd-ask-password-wall_service_state_deactivating": 0,
726 + "unit_systemd-ask-password-wall_service_state_failed": 0,
727 + "unit_systemd-ask-password-wall_service_state_inactive": 1,
728 + "unit_systemd-fsck-root_service_state_activating": 0,
729 + "unit_systemd-fsck-root_service_state_active": 0,
730 + "unit_systemd-fsck-root_service_state_deactivating": 0,
731 + "unit_systemd-fsck-root_service_state_failed": 0,
732 + "unit_systemd-fsck-root_service_state_inactive": 1,
733 + "unit_user-runtime-dir@1000_service_state_activating": 0,
734 + "unit_user-runtime-dir@1000_service_state_active": 1,
735 + "unit_user-runtime-dir@1000_service_state_deactivating": 0,
736 + "unit_user-runtime-dir@1000_service_state_failed": 0,
737 + "unit_user-runtime-dir@1000_service_state_inactive": 0,
738 + "unit_user@1000_service_state_activating": 0,
739 + "unit_user@1000_service_state_active": 1,
740 + "unit_user@1000_service_state_deactivating": 0,
741 + "unit_user@1000_service_state_failed": 0,
742 + "unit_user@1000_service_state_inactive": 0,
743 + },
744 + },
745 "fails when all unites are filtered": {
746 prepare: func() *SystemdUnits {
747 systemd := New()
@@ -698,15 +782,15 @@ func TestSystemdUnits_Collect(t *testing.T) {
782 systemd := test.prepare()
783 require.NoError(t, systemd.Init())
784
701 - var collected map[string]int64
785 + var mx map[string]int64
786
787 for i := 0; i < 10; i++ {
704 - collected = systemd.Collect()
788 + mx = systemd.Collect()
789 }
790
707 - assert.Equal(t, test.wantCollected, collected)
791 + assert.Equal(t, test.wantCollected, mx)
792 if len(test.wantCollected) > 0 {
709 - ensureCollectedHasAllChartsDimsVarsIDs(t, systemd, collected)
793 + ensureCollectedHasAllChartsDimsVarsIDs(t, systemd, mx)
794 }
795 })
796 }
@@ -747,8 +831,9 @@ func ensureCollectedHasAllChartsDimsVarsIDs(t *testing.T, sd *SystemdUnits, coll
831 func prepareOKClient(ver int) *mockClient {
832 return &mockClient{
833 conn: &mockConn{
750 - version: ver,
751 - units: mockSystemdUnits,
834 + version: ver,
835 + units: mockSystemdUnits,
836 + unitFiles: mockSystemdUnitFiles,
837 },
838 }
839 }
@@ -795,10 +880,15 @@ func (m *mockClient) connect() (systemdConnection, error) {
880
881 type mockConn struct {
882 version int
798 - units []dbus.UnitStatus
883 errOnGetManagerProperty bool
800 - errOnListUnits bool
801 - closeCalled bool
884 +
885 + units []dbus.UnitStatus
886 + errOnListUnits bool
887 +
888 + unitFiles []dbus.UnitFile
889 + errOnListUnitFiles bool
890 +
891 + closeCalled bool
892 }
893
894 func (m *mockConn) Close() {
@@ -812,6 +902,7 @@ func (m *mockConn) GetManagerProperty(prop string) (string, error) {
902 if prop != versionProperty {
903 return "", fmt.Errorf("'GetManagerProperty' unkown property: %s", prop)
904 }
905 +
906 return fmt.Sprintf("%d.6-1-manjaro", m.version), nil
907 }
908
@@ -822,10 +913,11 @@ func (m *mockConn) ListUnitsContext(_ context.Context) ([]dbus.UnitStatus, error
913 if m.version >= 230 {
914 return nil, errors.New("'ListUnits' unsupported function error")
915 }
916 +
917 return append([]dbus.UnitStatus{}, m.units...), nil
918 }
919
828 -func (m *mockConn) ListUnitsByPatternsContext(_ context.Context, _ []string, ps []string) ([]dbus.UnitStatus, error) {
920 +func (m *mockConn) ListUnitsByPatternsContext(_ context.Context, _ []string, patterns []string) ([]dbus.UnitStatus, error) {
921 if m.errOnListUnits {
922 return nil, errors.New("'ListUnitsByPatterns' call error")
923 }
@@ -833,22 +925,50 @@ func (m *mockConn) ListUnitsByPatternsContext(_ context.Context, _ []string, ps
925 return nil, errors.New("'ListUnitsByPatterns' unsupported function error")
926 }
927
836 - matches := func(name string) bool {
837 - for _, p := range ps {
928 + if len(m.units) == 0 {
929 + return nil, nil
930 + }
931 +
932 + units := append([]dbus.UnitStatus{}, m.units...)
933 +
934 + units = slices.DeleteFunc(units, func(u dbus.UnitStatus) bool {
935 + name := cleanUnitName(u.Name)
936 + for _, p := range patterns {
937 if ok, _ := filepath.Match(p, name); ok {
839 - return true
938 + return false
939 }
940 }
842 - return false
941 + return true
942 + })
943 +
944 + return units, nil
945 +}
946 +
947 +func (m *mockConn) ListUnitFilesByPatternsContext(_ context.Context, _ []string, patterns []string) ([]dbus.UnitFile, error) {
948 + if m.errOnListUnitFiles {
949 + return nil, errors.New("'ListUnitFilesByPatternsContex' call error")
950 + }
951 + if m.version < 230 {
952 + return nil, errors.New("'ListUnitFilesByPatternsContex' unsupported function error")
953 }
954
845 - var units []dbus.UnitStatus
846 - for _, unit := range m.units {
847 - if matches(unit.Name) {
848 - units = append(units, unit)
849 - }
955 + if len(m.unitFiles) == 0 {
956 + return nil, nil
957 }
851 - return units, nil
958 +
959 + unitFiles := append([]dbus.UnitFile{}, m.unitFiles...)
960 +
961 + unitFiles = slices.DeleteFunc(unitFiles, func(file dbus.UnitFile) bool {
962 + _, name := filepath.Split(file.Path)
963 + for _, p := range patterns {
964 + if ok, _ := filepath.Match(p, name); ok {
965 + return false
966 + }
967 + }
968 + return true
969 + })
970 +
971 + return unitFiles, nil
972 }
973
974 var mockSystemdUnits = []dbus.UnitStatus{
@@ -904,3 +1024,43 @@ var mockSystemdUnits = []dbus.UnitStatus{
1024 {Name: `shadow.timer`, LoadState: "loaded", ActiveState: "active"},
1025 {Name: `logrotate.timer`, LoadState: "loaded", ActiveState: "active"},
1026 }
1027 +
1028 +var mockSystemdUnitFiles = []dbus.UnitFile{
1029 + {Path: "/lib/systemd/system/systemd-tmpfiles-clean.timer", Type: "static"},
1030 + {Path: "/lib/systemd/system/sysstat-summary.timer", Type: "disabled"},
1031 + {Path: "/lib/systemd/system/sysstat-collect.timer", Type: "disabled"},
1032 + {Path: "/lib/systemd/system/pg_dump@.timer", Type: "disabled"},
1033 +
1034 + {Path: "/lib/systemd/system/veritysetup.target", Type: "static"},
1035 + {Path: "/lib/systemd/system/veritysetup-pre.target", Type: "static"},
1036 + {Path: "/lib/systemd/system/usb-gadget.target", Type: "static"},
1037 + {Path: "/lib/systemd/system/umount.target", Type: "static"},
1038 +
1039 + {Path: "/lib/systemd/system/syslog.socket", Type: "static"},
1040 + {Path: "/lib/systemd/system/ssh.socket", Type: "disabled"},
1041 + {Path: "/lib/systemd/system/docker.socket", Type: "enabled"},
1042 + {Path: "/lib/systemd/system/dbus.socket", Type: "static"},
1043 +
1044 + {Path: "/lib/systemd/system/user.slice", Type: "static"},
1045 + {Path: "/lib/systemd/system/system-systemd\x2dcryptsetup.slice", Type: "static"},
1046 + {Path: "/lib/systemd/system/machine.slice", Type: "static"},
1047 +
1048 + {Path: "/run/systemd/generator.late/sendmail.service", Type: "generated"},
1049 + {Path: "/run/systemd/generator.late/monit.service", Type: "generated"},
1050 + {Path: "/lib/systemd/system/x11-common.service", Type: "masked"},
1051 + {Path: "/lib/systemd/system/uuidd.service", Type: "indirect"},
1052 +
1053 + {Path: "/run/systemd/transient/session-144.scope", Type: "transient"},
1054 + {Path: "/run/systemd/transient/session-139.scope", Type: "transient"},
1055 + {Path: "/run/systemd/transient/session-132.scope", Type: "transient"},
1056 +
1057 + {Path: "/lib/systemd/system/systemd-ask-password-wall.path", Type: "static"},
1058 + {Path: "/lib/systemd/system/systemd-ask-password-console.path", Type: "static"},
1059 + {Path: "/lib/systemd/system/postfix-resolvconf.path", Type: "disabled"},
1060 + {Path: "/lib/systemd/system/ntpsec-systemd-netif.path", Type: "enabled"},
1061 +
1062 + {Path: "/run/systemd/generator/media-cdrom0.mount", Type: "generated"},
1063 + {Path: "/run/systemd/generator/boot.mount", Type: "generated"},
1064 + {Path: "/run/systemd/generator/-.mount", Type: "generated"},
1065 + {Path: "/lib/systemd/system/sys-kernel-tracing.mount", Type: "static"},
1066 +}
src/go/collectors/go.d.plugin/modules/systemdunits/testdata/config.json
+5
@@ -3,5 +3,10 @@
3 "timeout": 123.123,
4 "include": [
5 "ok"
6 + ],
7 + "collect_unit_files": true,
8 + "collect_unit_files_every": 123.123,
9 + "include_unit_files": [
10 + "ok"
11 ]
12 }
src/go/collectors/go.d.plugin/modules/systemdunits/testdata/config.yaml
+5 -1
@@ -1,4 +1,8 @@
1 update_every: 123
2 timeout: 123.123
3 include:
4 - - "ok"
4 + - ok
5 +collect_unit_files: true
6 +collect_unit_files_every: 123.123
7 +include_unit_files:
8 + - ok