summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorIlya Mashchenko <ilya@netdata.cloud>2024-05-07 12:16:08 +0300
committerGitHub <noreply@github.com>2024-05-07 12:16:08 +0300
commit50c3b9181284938ebe225fbd191030b7fb7baf9c (patch)
tree46143763d6ab45757bc4deb1b64f23e4d4a3660d /src
parent4ab59ea27e568f5d3954e6de48299bf9d4e8d452 (diff)
go.d systemdunits add unit files state (#17606)
Diffstat (limited to 'src')
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/charts.go124
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/client.go1
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/collect.go139
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/collect_unit_files.go94
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/collect_units.go151
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/config_schema.json54
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/init.go2
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/metadata.yaml60
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits.go52
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits_test.go208
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/testdata/config.json5
-rw-r--r--src/go/collectors/go.d.plugin/modules/systemdunits/testdata/config.yaml6
12 files changed, 674 insertions, 222 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/charts.go b/src/go/collectors/go.d.plugin/modules/systemdunits/charts.go
index 210fc568d6..18d8838fbc 100644
--- a/src/go/collectors/go.d.plugin/modules/systemdunits/charts.go
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/charts.go
@@ -7,6 +7,8 @@ package systemdunits
import (
"fmt"
+ "path/filepath"
+ "strings"
"github.com/netdata/netdata/go/go.d.plugin/agent/module"
@@ -15,62 +17,102 @@ import (
)
const (
- prioServiceUnitState = module.Priority + iota
- prioSocketUnitState
- prioTargetUnitState
- prioPathUnitState
- prioDeviceUnitState
- prioMountUnitState
- prioAutomountUnitState
- prioSwapUnitState
- prioTimerUnitState
- prioScopeUnitState
- prioSliceUnitState
+ prioUnitState = module.Priority + iota
+ prioUnitFileState
)
-var prioMap = map[string]int{
- unitTypeService: prioServiceUnitState,
- unitTypeSocket: prioSocketUnitState,
- unitTypeTarget: prioTargetUnitState,
- unitTypePath: prioPathUnitState,
- unitTypeDevice: prioDeviceUnitState,
- unitTypeMount: prioMountUnitState,
- unitTypeAutomount: prioAutomountUnitState,
- unitTypeSwap: prioSwapUnitState,
- unitTypeTimer: prioTimerUnitState,
- unitTypeScope: prioScopeUnitState,
- unitTypeSlice: prioSliceUnitState,
-}
-
-func newTypedUnitStateChartTmpl(name, typ string) *module.Chart {
+func (s *SystemdUnits) addUnitCharts(name, typ string) {
chart := module.Chart{
- ID: fmt.Sprintf("unit_%s_%s_state", name, typ),
- Title: fmt.Sprintf("%s Unit State", cases.Title(language.English, cases.Compact).String(typ)),
+ ID: "unit_%s_%s_state",
+ Title: "%s Unit State",
Units: "state",
- Fam: fmt.Sprintf("%s units", typ),
- Ctx: fmt.Sprintf("systemd.%s_unit_state", typ),
- Priority: prioMap[typ],
+ Fam: "%s units",
+ Ctx: "systemd.%s_unit_state",
+ Priority: prioUnitState,
Labels: []module.Label{
{Key: "unit_name", Value: name},
},
Dims: module.Dims{
- {Name: unitStateActive},
- {Name: unitStateInactive},
- {Name: unitStateActivating},
- {Name: unitStateDeactivating},
- {Name: unitStateFailed},
+ {ID: "unit_%s_%s_state_%s", Name: unitStateActive},
+ {ID: "unit_%s_%s_state_%s", Name: unitStateInactive},
+ {ID: "unit_%s_%s_state_%s", Name: unitStateActivating},
+ {ID: "unit_%s_%s_state_%s", Name: unitStateDeactivating},
+ {ID: "unit_%s_%s_state_%s", Name: unitStateFailed},
},
}
+
+ chart.ID = fmt.Sprintf(chart.ID, name, typ)
+ chart.Title = fmt.Sprintf(chart.Title, cases.Title(language.English, cases.Compact).String(typ))
+ chart.Fam = fmt.Sprintf(chart.Fam, typ)
+ chart.Ctx = fmt.Sprintf(chart.Ctx, typ)
+
for _, d := range chart.Dims {
- d.ID = fmt.Sprintf("unit_%s_%s_state_%s", name, typ, d.Name)
+ d.ID = fmt.Sprintf(d.ID, name, typ, d.Name)
+ }
+
+ if err := s.Charts().Add(&chart); err != nil {
+ s.Warning(err)
}
- return &chart
}
-func (s *SystemdUnits) addUnitToCharts(name, typ string) {
- chart := newTypedUnitStateChartTmpl(name, typ)
+func (s *SystemdUnits) removeUnitCharts(name, typ string) {
+ px := fmt.Sprintf("unit_%s_%s_", name, typ)
+ s.removeCharts(px)
+}
+
+func (s *SystemdUnits) addUnitFileCharts(unitPath string) {
+ _, unitName := filepath.Split(unitPath)
+ unitType := strings.TrimPrefix(filepath.Ext(unitPath), ".")
+
+ chart := module.Chart{
+ ID: "unit_file_%s_state",
+ Title: "Unit File State",
+ Units: "state",
+ Fam: "unit files",
+ Ctx: "systemd.unit_file_state",
+ Type: module.Line,
+ Priority: prioUnitFileState,
+ Labels: []module.Label{
+ {Key: "unit_file_name", Value: unitName},
+ {Key: "unit_file_type", Value: unitType},
+ },
+ Dims: module.Dims{
+ {ID: "unit_file_%s_state_enabled", Name: "enabled"},
+ {ID: "unit_file_%s_state_enabled-runtime", Name: "enabled-runtime"},
+ {ID: "unit_file_%s_state_linked", Name: "linked"},
+ {ID: "unit_file_%s_state_linked-runtime", Name: "linked-runtime"},
+ {ID: "unit_file_%s_state_alias", Name: "alias"},
+ {ID: "unit_file_%s_state_masked", Name: "masked"},
+ {ID: "unit_file_%s_state_masked-runtime", Name: "masked-runtime"},
+ {ID: "unit_file_%s_state_static", Name: "static"},
+ {ID: "unit_file_%s_state_disabled", Name: "disabled"},
+ {ID: "unit_file_%s_state_indirect", Name: "indirect"},
+ {ID: "unit_file_%s_state_generated", Name: "generated"},
+ {ID: "unit_file_%s_state_transient", Name: "transient"},
+ {ID: "unit_file_%s_state_bad", Name: "bad"},
+ },
+ }
+
+ chart.ID = fmt.Sprintf(chart.ID, strings.ReplaceAll(unitPath, ".", "_"))
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, unitPath)
+ }
- if err := s.Charts().Add(chart); err != nil {
+ if err := s.Charts().Add(&chart); err != nil {
s.Warning(err)
}
}
+
+func (s *SystemdUnits) removeUnitFileCharts(unitPath string) {
+ px := fmt.Sprintf("unit_file_%s_", strings.ReplaceAll(unitPath, ".", "_"))
+ s.removeCharts(px)
+}
+
+func (s *SystemdUnits) removeCharts(prefix string) {
+ for _, chart := range *s.Charts() {
+ if strings.HasPrefix(chart.ID, prefix) {
+ chart.MarkRemove()
+ chart.MarkNotCreated()
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/client.go b/src/go/collectors/go.d.plugin/modules/systemdunits/client.go
index a2787c4ec1..b1152865f4 100644
--- a/src/go/collectors/go.d.plugin/modules/systemdunits/client.go
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/client.go
@@ -19,6 +19,7 @@ type systemdConnection interface {
GetManagerProperty(string) (string, error)
ListUnitsContext(ctx context.Context) ([]dbus.UnitStatus, error)
ListUnitsByPatternsContext(ctx context.Context, states []string, patterns []string) ([]dbus.UnitStatus, error)
+ ListUnitFilesByPatternsContext(ctx context.Context, states []string, patterns []string) ([]dbus.UnitFile, error)
}
type systemdDBusClient struct{}
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/collect.go b/src/go/collectors/go.d.plugin/modules/systemdunits/collect.go
index eb596605fc..0d61c9998e 100644
--- a/src/go/collectors/go.d.plugin/modules/systemdunits/collect.go
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/collect.go
@@ -6,45 +6,9 @@
package systemdunits
import (
- "context"
"fmt"
"regexp"
"strconv"
- "strings"
-
- "github.com/coreos/go-systemd/v22/dbus"
-)
-
-const (
- // https://www.freedesktop.org/software/systemd/man/systemd.html
- unitStateActive = "active"
- unitStateInactive = "inactive"
- unitStateActivating = "activating"
- unitStateDeactivating = "deactivating"
- unitStateFailed = "failed"
-
- // https://www.freedesktop.org/software/systemd/man/systemd.html
- unitTypeService = "service"
- unitTypeSocket = "socket"
- unitTypeTarget = "target"
- unitTypePath = "path"
- unitTypeDevice = "device"
- unitTypeMount = "mount"
- unitTypeAutomount = "automount"
- unitTypeSwap = "swap"
- unitTypeTimer = "timer"
- unitTypeScope = "scope"
- unitTypeSlice = "slice"
-)
-
-var (
- unitStates = []string{
- unitStateActive,
- unitStateActivating,
- unitStateFailed,
- unitStateInactive,
- unitStateDeactivating,
- }
)
func (s *SystemdUnits) collect() (map[string]int64, error) {
@@ -62,47 +26,23 @@ func (s *SystemdUnits) collect() (map[string]int64, error) {
s.systemdVersion = ver
}
- var units []dbus.UnitStatus
- if s.systemdVersion >= 230 {
- // https://github.com/systemd/systemd/pull/3142
- units, err = s.getLoadedUnitsByPatterns(conn)
- } else {
- units, err = s.getLoadedUnits(conn)
- }
- if err != nil {
+ mx := make(map[string]int64)
+
+ if err := s.collectUnits(mx, conn); err != nil {
s.closeConnection()
return nil, err
}
- if len(units) == 0 {
- return nil, nil
+ if s.CollectUnitFiles && len(s.IncludeUnitFiles) > 0 {
+ if err := s.collectUnitFiles(mx, conn); err != nil {
+ s.closeConnection()
+ return mx, err
+ }
}
- mx := make(map[string]int64)
- s.collectUnitsStates(mx, units)
-
return mx, nil
}
-func (s *SystemdUnits) collectUnitsStates(mx map[string]int64, units []dbus.UnitStatus) {
- for _, unit := range units {
- name, typ := extractUnitNameType(cleanUnitName(unit.Name))
- if name == "" || typ == "" {
- continue
- }
-
- if !s.units[unit.Name] {
- s.units[unit.Name] = true
- s.addUnitToCharts(name, typ)
- }
-
- for _, s := range unitStates {
- mx[fmt.Sprintf("unit_%s_%s_state_%s", name, typ, s)] = 0
- }
- mx[fmt.Sprintf("unit_%s_%s_state_%s", name, typ, unit.ActiveState)] = 1
- }
-}
-
func (s *SystemdUnits) getConnection() (systemdConnection, error) {
if s.conn == nil {
conn, err := s.client.connect()
@@ -146,66 +86,3 @@ func (s *SystemdUnits) getSystemdVersion(conn systemdConnection) (int, error) {
return ver, nil
}
-
-func (s *SystemdUnits) getLoadedUnits(conn systemdConnection) ([]dbus.UnitStatus, error) {
- ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
- defer cancel()
-
- s.Debugf("calling function 'ListUnits'")
- units, err := conn.ListUnitsContext(ctx)
- if err != nil {
- return nil, fmt.Errorf("error on ListUnits: %v", err)
- }
-
- loaded := units[:0]
- for _, unit := range units {
- if unit.LoadState == "loaded" && s.sr.MatchString(unit.Name) {
- loaded = append(loaded, unit)
- }
- }
- s.Debugf("got total/loaded %d/%d units", len(units), len(loaded))
-
- return loaded, nil
-}
-
-func (s *SystemdUnits) getLoadedUnitsByPatterns(conn systemdConnection) ([]dbus.UnitStatus, error) {
- ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
- defer cancel()
-
- s.Debugf("calling function 'ListUnitsByPatterns'")
-
- units, err := conn.ListUnitsByPatternsContext(ctx, unitStates, s.Include)
- if err != nil {
- return nil, fmt.Errorf("error on ListUnitsByPatterns: %v", err)
- }
-
- loaded := units[:0]
- for _, unit := range units {
- if unit.LoadState == "loaded" {
- loaded = append(loaded, unit)
- }
- }
- s.Debugf("got total/loaded %d/%d units", len(units), len(loaded))
-
- return loaded, nil
-}
-
-func extractUnitNameType(name string) (string, string) {
- idx := strings.LastIndexByte(name, '.')
- if idx <= 0 {
- return "", ""
- }
- return name[:idx], name[idx+1:]
-}
-
-func cleanUnitName(name string) string {
- // dev-disk-by\x2duuid-DE44\x2dCEE0.device => dev-disk-by-uuid-DE44-CEE0.device
- if strings.IndexByte(name, '\\') == -1 {
- return name
- }
- v, err := strconv.Unquote("\"" + name + "\"")
- if err != nil {
- return name
- }
- return v
-}
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/collect_unit_files.go b/src/go/collectors/go.d.plugin/modules/systemdunits/collect_unit_files.go
new file mode 100644
index 0000000000..c1b12cf119
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/collect_unit_files.go
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+//go:build linux
+// +build linux
+
+package systemdunits
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/coreos/go-systemd/v22/dbus"
+)
+
+// https://github.com/systemd/systemd/blob/3d320785c4bbba74459096b07e85a79c4f0cdffb/src/shared/install.c#L3785
+// see "is-enabled" in https://www.man7.org/linux/man-pages/man1/systemctl.1.html
+var unitFileStates = []string{
+ "enabled",
+ "enabled-runtime",
+ "linked",
+ "linked-runtime",
+ "alias",
+ "masked",
+ "masked-runtime",
+ "static",
+ "disabled",
+ "indirect",
+ "generated",
+ "transient",
+ "bad",
+}
+
+func (s *SystemdUnits) collectUnitFiles(mx map[string]int64, conn systemdConnection) error {
+ if s.systemdVersion < 230 {
+ return nil
+ }
+
+ if now := time.Now(); now.After(s.lastListUnitFilesTime.Add(s.CollectUnitFilesEvery.Duration())) {
+ unitFiles, err := s.getUnitFilesByPatterns(conn)
+ if err != nil {
+ return err
+ }
+ s.lastListUnitFilesTime = now
+ s.cachedUnitFiles = unitFiles
+ }
+
+ seen := make(map[string]bool)
+
+ for _, unitFile := range s.cachedUnitFiles {
+ seen[unitFile.Path] = true
+
+ if !s.seenUnitFiles[unitFile.Path] {
+ s.seenUnitFiles[unitFile.Path] = true
+ s.addUnitFileCharts(unitFile.Path)
+ }
+
+ px := fmt.Sprintf("unit_file_%s_state_", unitFile.Path)
+ for _, st := range unitFileStates {
+ mx[px+st] = 0
+ }
+ mx[px+strings.ToLower(unitFile.Type)] = 1
+ }
+
+ for k := range s.seenUnitFiles {
+ if !seen[k] {
+ delete(s.seenUnitFiles, k)
+ s.removeUnitFileCharts(k)
+ }
+ }
+
+ return nil
+}
+
+func (s *SystemdUnits) getUnitFilesByPatterns(conn systemdConnection) ([]dbus.UnitFile, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
+ defer cancel()
+
+ s.Debugf("calling function 'ListUnitFilesByPatterns'")
+
+ unitFiles, err := conn.ListUnitFilesByPatternsContext(ctx, nil, []string{"*.service"})
+ if err != nil {
+ return nil, fmt.Errorf("error on ListUnitFilesByPatterns: %v", err)
+ }
+
+ for i := range unitFiles {
+ unitFiles[i].Path = cleanUnitName(unitFiles[i].Path)
+ }
+
+ s.Debugf("got %d unit files", len(unitFiles))
+
+ return unitFiles, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/collect_units.go b/src/go/collectors/go.d.plugin/modules/systemdunits/collect_units.go
new file mode 100644
index 0000000000..4cfc89d604
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/collect_units.go
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+//go:build linux
+// +build linux
+
+package systemdunits
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/coreos/go-systemd/v22/dbus"
+)
+
+const (
+ // https://www.freedesktop.org/software/systemd/man/systemd.html
+ unitStateActive = "active"
+ unitStateInactive = "inactive"
+ unitStateActivating = "activating"
+ unitStateDeactivating = "deactivating"
+ unitStateFailed = "failed"
+)
+
+var unitStates = []string{
+ unitStateActive,
+ unitStateActivating,
+ unitStateFailed,
+ unitStateInactive,
+ unitStateDeactivating,
+}
+
+func (s *SystemdUnits) collectUnits(mx map[string]int64, conn systemdConnection) error {
+ var units []dbus.UnitStatus
+ var err error
+
+ if s.systemdVersion >= 230 {
+ // https://github.com/systemd/systemd/pull/3142
+ units, err = s.getLoadedUnitsByPatterns(conn)
+ } else {
+ units, err = s.getLoadedUnits(conn)
+ }
+ if err != nil {
+ return err
+ }
+
+ seen := make(map[string]bool)
+
+ for _, unit := range units {
+ name, typ, ok := extractUnitNameType(unit.Name)
+ if !ok {
+ continue
+ }
+
+ seen[unit.Name] = true
+
+ if !s.seenUnits[unit.Name] {
+ s.seenUnits[unit.Name] = true
+ s.addUnitCharts(name, typ)
+ }
+
+ for _, s := range unitStates {
+ mx[fmt.Sprintf("unit_%s_%s_state_%s", name, typ, s)] = 0
+ }
+ mx[fmt.Sprintf("unit_%s_%s_state_%s", name, typ, unit.ActiveState)] = 1
+ }
+
+ for k := range s.seenUnits {
+ if !seen[k] {
+ delete(s.seenUnits, k)
+ if name, typ, ok := extractUnitNameType(k); ok {
+ s.removeUnitCharts(name, typ)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (s *SystemdUnits) getLoadedUnits(conn systemdConnection) ([]dbus.UnitStatus, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
+ defer cancel()
+
+ s.Debugf("calling function 'ListUnits'")
+ units, err := conn.ListUnitsContext(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("error on ListUnits: %v", err)
+ }
+
+ for i := range units {
+ units[i].Name = cleanUnitName(units[i].Name)
+ }
+
+ loaded := units[:0]
+ for _, unit := range units {
+ if unit.LoadState == "loaded" && s.unitSr.MatchString(unit.Name) {
+ loaded = append(loaded, unit)
+ }
+ }
+
+ s.Debugf("got total/loaded %d/%d units", len(units), len(loaded))
+
+ return loaded, nil
+}
+
+func (s *SystemdUnits) getLoadedUnitsByPatterns(conn systemdConnection) ([]dbus.UnitStatus, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration())
+ defer cancel()
+
+ s.Debugf("calling function 'ListUnitsByPatterns'")
+
+ units, err := conn.ListUnitsByPatternsContext(ctx, unitStates, s.Include)
+ if err != nil {
+ return nil, fmt.Errorf("error on ListUnitsByPatterns: %v", err)
+ }
+
+ for i := range units {
+ units[i].Name = cleanUnitName(units[i].Name)
+ }
+
+ loaded := units[:0]
+ for _, unit := range units {
+ if unit.LoadState == "loaded" {
+ loaded = append(loaded, unit)
+ }
+ }
+ s.Debugf("got total/loaded %d/%d units", len(units), len(loaded))
+
+ return loaded, nil
+}
+
+func extractUnitNameType(name string) (string, string, bool) {
+ idx := strings.LastIndexByte(name, '.')
+ if idx <= 0 {
+ return "", "", false
+ }
+ return name[:idx], name[idx+1:], true
+}
+
+func cleanUnitName(name string) string {
+ // dev-disk-by\x2duuid-DE44\x2dCEE0.device => dev-disk-by-uuid-DE44-CEE0.device
+ if strings.IndexByte(name, '\\') == -1 {
+ return name
+ }
+ v, err := strconv.Unquote("\"" + name + "\"")
+ if err != nil {
+ return name
+ }
+ return v
+}
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/config_schema.json b/src/go/collectors/go.d.plugin/modules/systemdunits/config_schema.json
index 8dc57a3d86..c13cabc6ec 100644
--- a/src/go/collectors/go.d.plugin/modules/systemdunits/config_schema.json
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/config_schema.json
@@ -34,6 +34,36 @@
"default": [
"*.service"
]
+ },
+ "collect_unit_files": {
+ "title": "Collect unit files",
+ "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.",
+ "type": "boolean",
+ "default": false
+ },
+ "collect_unit_files_every": {
+ "title": "Unit files polling interval",
+ "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.",
+ "type": "number",
+ "minimum": 1,
+ "default": 300
+ },
+ "include_unit_files": {
+ "title": "Include unit files",
+ "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).",
+ "type": [
+ "array",
+ "null"
+ ],
+ "uniqueItems": true,
+ "minItems": 1,
+ "items": {
+ "title": "Unit file name pattern",
+ "type": "string"
+ },
+ "default": [
+ "*.service"
+ ]
}
},
"required": [
@@ -48,11 +78,35 @@
"uiOptions": {
"fullPage": true
},
+ "ui:flavour": "tabs",
+ "ui:options": {
+ "tabs": [
+ {
+ "title": "Base",
+ "fields": [
+ "update_every",
+ "timeout",
+ "include"
+ ]
+ },
+ {
+ "title": "Unit Files",
+ "fields": [
+ "collect_unit_files",
+ "collect_unit_files_every",
+ "include_unit_files"
+ ]
+ }
+ ]
+ },
"timeout": {
"ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
},
"include": {
"ui:listFlavour": "list"
+ },
+ "include_unit_files": {
+ "ui:listFlavour": "list"
}
}
}
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/init.go b/src/go/collectors/go.d.plugin/modules/systemdunits/init.go
index e59290ace4..ea3d21d379 100644
--- a/src/go/collectors/go.d.plugin/modules/systemdunits/init.go
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/init.go
@@ -19,7 +19,7 @@ func (s *SystemdUnits) validateConfig() error {
return nil
}
-func (s *SystemdUnits) initSelector() (matcher.Matcher, error) {
+func (s *SystemdUnits) initUnitSelector() (matcher.Matcher, error) {
if len(s.Include) == 0 {
return matcher.TRUE(), nil
}
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/metadata.yaml b/src/go/collectors/go.d.plugin/modules/systemdunits/metadata.yaml
index 21755bb698..6ca804fb30 100644
--- a/src/go/collectors/go.d.plugin/modules/systemdunits/metadata.yaml
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/metadata.yaml
@@ -21,7 +21,7 @@ modules:
overview:
data_collection:
metrics_description: |
- This collector monitors Systemd units state.
+ This collector monitors the state of Systemd units and unit files.
method_description: ""
supported_platforms:
include: []
@@ -57,8 +57,12 @@ modules:
description: Recheck interval in seconds. Zero means no recheck will be scheduled.
default_value: 0
required: false
+ - name: timeout
+ description: System bus requests timeout.
+ default_value: 1
+ required: false
- name: include
- description: Systemd units filter.
+ description: Systemd units selector.
default_value: "*.service"
required: false
detailed_description: |
@@ -73,10 +77,30 @@ modules:
- pattern1
- pattern2
```
- - name: timeout
- description: System bus requests timeout.
- default_value: 1
+ - name: collect_unit_files
+ description: If set to true, collect the state of installed unit files. Enabling this may increase system overhead.
+ default_value: "false"
required: false
+ - name: collect_unit_files_every
+ 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.
+ default_value: 300
+ required: false
+ - name: include_unit_files
+ description: Systemd unit files selector.
+ default_value: "*.service"
+ required: false
+ detailed_description: |
+ Systemd unit files matching the selector will be monitored.
+
+ - Logic: (pattern1 OR pattern2)
+ - Pattern syntax: [shell file name pattern](https://golang.org/pkg/path/filepath/#Match)
+ - Syntax:
+
+ ```yaml
+ includes:
+ - pattern1
+ - pattern2
+ ```
examples:
folding:
title: Config
@@ -288,3 +312,29 @@ modules:
- name: activating
- name: deactivating
- name: failed
+ - name: unit file
+ description: These metrics refer to the systemd unit file.
+ labels:
+ - name: unit_file_name
+ description: systemd unit file name
+ - name: unit_file_type
+ description: systemd unit file type
+ metrics:
+ - name: systemd.unit_file_state
+ description: Unit File State
+ unit: state
+ chart_type: line
+ dimensions:
+ - name: enabled
+ - name: enabled-runtime
+ - name: linked
+ - name: linked-runtime
+ - name: alias
+ - name: masked
+ - name: masked-runtime
+ - name: static
+ - name: disabled
+ - name: indirect
+ - name: generated
+ - name: transient
+ - name: bad
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits.go b/src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits.go
index 345b2525a6..a6e82cbcbe 100644
--- a/src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits.go
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits.go
@@ -13,6 +13,8 @@ import (
"github.com/netdata/netdata/go/go.d.plugin/agent/module"
"github.com/netdata/netdata/go/go.d.plugin/pkg/matcher"
"github.com/netdata/netdata/go/go.d.plugin/pkg/web"
+
+ "github.com/coreos/go-systemd/v22/dbus"
)
//go:embed "config_schema.json"
@@ -31,22 +33,26 @@ func init() {
func New() *SystemdUnits {
return &SystemdUnits{
Config: Config{
- Timeout: web.Duration(time.Second * 2),
- Include: []string{
- "*.service",
- },
+ Timeout: web.Duration(time.Second * 2),
+ Include: []string{"*.service"},
+ CollectUnitFiles: false,
+ IncludeUnitFiles: []string{"*.service"},
+ CollectUnitFilesEvery: web.Duration(time.Minute * 5),
},
-
- charts: &module.Charts{},
- client: newSystemdDBusClient(),
- units: make(map[string]bool),
+ charts: &module.Charts{},
+ client: newSystemdDBusClient(),
+ seenUnits: make(map[string]bool),
+ seenUnitFiles: make(map[string]bool),
}
}
type Config struct {
- UpdateEvery int `yaml:"update_every" json:"update_every"`
- Timeout web.Duration `yaml:"timeout" json:"timeout"`
- Include []string `yaml:"include" json:"include"`
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
+ Include []string `yaml:"include" json:"include"`
+ CollectUnitFiles bool `yaml:"collect_unit_files" json:"collect_unit_files"`
+ IncludeUnitFiles []string `yaml:"include_unit_files" json:"include_unit_files"`
+ CollectUnitFilesEvery web.Duration `yaml:"collect_unit_files_every" json:"collect_unit_files_every"`
}
type SystemdUnits struct {
@@ -57,8 +63,13 @@ type SystemdUnits struct {
conn systemdConnection
systemdVersion int
- units map[string]bool
- sr matcher.Matcher
+
+ seenUnits map[string]bool
+ unitSr matcher.Matcher
+
+ lastListUnitFilesTime time.Time
+ cachedUnitFiles []dbus.UnitFile
+ seenUnitFiles map[string]bool
charts *module.Charts
}
@@ -68,21 +79,22 @@ func (s *SystemdUnits) Configuration() any {
}
func (s *SystemdUnits) Init() error {
- err := s.validateConfig()
- if err != nil {
+ if err := s.validateConfig(); err != nil {
s.Errorf("config validation: %v", err)
return err
}
- sr, err := s.initSelector()
+ sr, err := s.initUnitSelector()
if err != nil {
- s.Errorf("init selector: %v", err)
+ s.Errorf("init unit selector: %v", err)
return err
}
- s.sr = sr
+ s.unitSr = sr
- s.Debugf("unit names patterns: %v", s.Include)
s.Debugf("timeout: %s", s.Timeout)
+ s.Debugf("units: patterns '%v'", s.Include)
+ s.Debugf("unit files: enabled '%v', every '%s', patterns: %v",
+ s.CollectUnitFiles, s.CollectUnitFilesEvery, s.IncludeUnitFiles)
return nil
}
@@ -93,9 +105,11 @@ func (s *SystemdUnits) Check() error {
s.Error(err)
return err
}
+
if len(mx) == 0 {
return errors.New("no metrics collected")
}
+
return nil
}
diff --git a/src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits_test.go b/src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits_test.go
index 3a1a594247..d87ae66782 100644
--- a/src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits_test.go
+++ b/src/go/collectors/go.d.plugin/modules/systemdunits/systemdunits_test.go
@@ -11,6 +11,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "slices"
"testing"
"github.com/netdata/netdata/go/go.d.plugin/agent/module"
@@ -168,7 +169,7 @@ func TestSystemdUnits_Collect(t *testing.T) {
prepare func() *SystemdUnits
wantCollected map[string]int64
}{
- "success on systemd v230+ on collecting all unit type": {
+ "success v230+ on collecting all unit type": {
prepare: func() *SystemdUnits {
systemd := New()
systemd.Include = []string{"*"}
@@ -383,7 +384,7 @@ func TestSystemdUnits_Collect(t *testing.T) {
"unit_var-lib-nfs-rpc_pipefs_mount_state_inactive": 1,
},
},
- "success on systemd v230- on collecting all unit types": {
+ "success v230- on collecting all unit types": {
prepare: func() *SystemdUnits {
systemd := New()
systemd.Include = []string{"*"}
@@ -598,7 +599,7 @@ func TestSystemdUnits_Collect(t *testing.T) {
"unit_var-lib-nfs-rpc_pipefs_mount_state_inactive": 1,
},
},
- "success on systemd v230+ on collecting only 'service' unit type": {
+ "success v230+ on collecting only 'service' units": {
prepare: func() *SystemdUnits {
systemd := New()
systemd.Include = []string{"*.service"}
@@ -628,7 +629,7 @@ func TestSystemdUnits_Collect(t *testing.T) {
"unit_user@1000_service_state_inactive": 0,