diff --git a/collector/systemd_linux.go b/collector/systemd_linux.go index ee2ded8d..2ee18356 100644 --- a/collector/systemd_linux.go +++ b/collector/systemd_linux.go @@ -74,6 +74,7 @@ type systemdCollector struct { socketCurrentConnectionsDesc *prometheus.Desc socketRefusedConnectionsDesc *prometheus.Desc systemdVersionDesc *prometheus.Desc + virtualizationDesc *prometheus.Desc // Use regexps for more flexibility than device_filter.go allows systemdUnitIncludePattern *regexp.Regexp systemdUnitExcludePattern *regexp.Regexp @@ -82,6 +83,10 @@ type systemdCollector struct { var unitStatesName = []string{"active", "activating", "deactivating", "inactive", "failed"} +var getManagerPropertyFunc = func(conn *dbus.Conn, name string) (string, error) { + return conn.GetManagerProperty(name) +} + func init() { registerCollector("systemd", defaultDisabled, NewSystemdCollector) } @@ -132,6 +137,9 @@ func NewSystemdCollector(logger *slog.Logger) (Collector, error) { systemdVersionDesc := prometheus.NewDesc( prometheus.BuildFQName(namespace, subsystem, "version"), "Detected systemd version", []string{"version"}, nil) + virtualizationDesc := prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "virtualization"), + "Detected virtualization technology", []string{"type"}, nil) if *oldSystemdUnitExclude != "" { if !systemdUnitExcludeSet { @@ -167,6 +175,7 @@ func NewSystemdCollector(logger *slog.Logger) (Collector, error) { socketCurrentConnectionsDesc: socketCurrentConnectionsDesc, socketRefusedConnectionsDesc: socketRefusedConnectionsDesc, systemdVersionDesc: systemdVersionDesc, + virtualizationDesc: virtualizationDesc, systemdUnitIncludePattern: systemdUnitIncludePattern, systemdUnitExcludePattern: systemdUnitExcludePattern, logger: logger, @@ -194,6 +203,14 @@ func (c *systemdCollector) Update(ch chan<- prometheus.Metric) error { systemdVersionFull, ) + systemdVirtualization := c.getSystemdVirtualization(conn) + ch <- prometheus.MustNewConstMetric( + c.virtualizationDesc, + prometheus.GaugeValue, + 1.0, + systemdVirtualization, + ) + allUnits, err := c.getAllUnits(conn) if err != nil { return fmt.Errorf("couldn't get units: %w", err) @@ -421,7 +438,7 @@ func (c *systemdCollector) collectSummaryMetrics(ch chan<- prometheus.Metric, su } func (c *systemdCollector) collectSystemState(conn *dbus.Conn, ch chan<- prometheus.Metric) error { - systemState, err := conn.GetManagerProperty("SystemState") + systemState, err := getManagerPropertyFunc(conn, "SystemState") if err != nil { return fmt.Errorf("couldn't get system state: %w", err) } @@ -490,7 +507,7 @@ func filterUnits(units []unit, includePattern, excludePattern *regexp.Regexp, lo } func (c *systemdCollector) getSystemdVersion(conn *dbus.Conn) (float64, string) { - version, err := conn.GetManagerProperty("Version") + version, err := getManagerPropertyFunc(conn, "Version") if err != nil { c.logger.Debug("Unable to get systemd version property, defaulting to 0") return 0, "" @@ -505,3 +522,19 @@ func (c *systemdCollector) getSystemdVersion(conn *dbus.Conn) (float64, string) } return v, version } + +func (c *systemdCollector) getSystemdVirtualization(conn *dbus.Conn) string { + virt, err := getManagerPropertyFunc(conn, "Virtualization") + if err != nil { + c.logger.Debug("Could not get Virtualization property", "err", err) + return "unknown" + } + + virtStr := strings.Trim(virt, `"`) + if virtStr == "" { + // If no virtualization type is returned, assume it's bare metal. + return "none" + } + + return virtStr +} diff --git a/collector/systemd_linux_test.go b/collector/systemd_linux_test.go index 1c290377..92aa848e 100644 --- a/collector/systemd_linux_test.go +++ b/collector/systemd_linux_test.go @@ -17,6 +17,7 @@ package collector import ( + "errors" "io" "log/slog" "regexp" @@ -137,3 +138,61 @@ func testSummaryHelper(t *testing.T, state string, actual float64, expected floa t.Errorf("Summary mode didn't count %s jobs correctly. Actual: %f, expected: %f", state, actual, expected) } } + +func Test_systemdCollector_getSystemdVirtualization(t *testing.T) { + type fields struct { + logger *slog.Logger + } + type args struct { + conn *dbus.Conn + } + tests := []struct { + name string + fields fields + args args + mock func(conn *dbus.Conn, name string) (string, error) + want string + }{ + { + name: "Error", + fields: fields{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}, + args: args{conn: &dbus.Conn{}}, + mock: func(conn *dbus.Conn, name string) (string, error) { + return "", errors.New("test error") + }, + want: "unknown", + }, + { + name: "Empty", + fields: fields{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}, + args: args{conn: &dbus.Conn{}}, + mock: func(conn *dbus.Conn, name string) (string, error) { + return `""`, nil + }, + want: "none", + }, + { + name: "Valid", + fields: fields{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}, + args: args{conn: &dbus.Conn{}}, + mock: func(conn *dbus.Conn, name string) (string, error) { + return `"kvm"`, nil + }, + want: "kvm", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + origFunc := getManagerPropertyFunc + defer func() { getManagerPropertyFunc = origFunc }() + getManagerPropertyFunc = tt.mock + + c := &systemdCollector{ + logger: tt.fields.logger, + } + if got := c.getSystemdVirtualization(tt.args.conn); got != tt.want { + t.Errorf("systemdCollector.getSystemdVirtualization() = %v, want %v", got, tt.want) + } + }) + } +}