From e3ce01abf029c23fd16152d7290c92b1aa301ac8 Mon Sep 17 00:00:00 2001 From: Sven Rebhan <36194019+srebhan@users.noreply.github.com> Date: Wed, 11 Dec 2024 22:32:52 +0100 Subject: [PATCH] feat(inputs.systemd_units): Add active_enter_timestamp_us field (#16287) --- go.mod | 2 +- plugins/inputs/systemd_units/README.md | 1 + .../systemd_units/systemd_units_linux.go | 46 +++-- .../systemd_units/systemd_units_test.go | 158 +++++++++--------- 4 files changed, 114 insertions(+), 93 deletions(-) diff --git a/go.mod b/go.mod index 9f8ae22fe..fa91210be 100644 --- a/go.mod +++ b/go.mod @@ -89,7 +89,6 @@ require ( github.com/go-sql-driver/mysql v1.8.1 github.com/go-stomp/stomp v2.1.4+incompatible github.com/gobwas/glob v0.2.3 - github.com/godbus/dbus/v5 v5.1.0 github.com/gofrs/uuid/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang/geo v0.0.0-20190916061304-5b978397cfec @@ -348,6 +347,7 @@ require ( github.com/goburrow/serial v0.1.1-0.20211022031912-bfb69110f8dd // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect diff --git a/plugins/inputs/systemd_units/README.md b/plugins/inputs/systemd_units/README.md index 5364f43ef..679d5819e 100644 --- a/plugins/inputs/systemd_units/README.md +++ b/plugins/inputs/systemd_units/README.md @@ -94,6 +94,7 @@ The following *additional* metrics are available with `details = true`: - swap_current (uint, current swap usage) - swap_peak (uint, peak swap usage) - mem_avail (uint, available memory for this unit) + - active_enter_timestamp_us (uint, timestamp in us when entered the state) ### Load diff --git a/plugins/inputs/systemd_units/systemd_units_linux.go b/plugins/inputs/systemd_units/systemd_units_linux.go index 443095ee2..2500b6cce 100644 --- a/plugins/inputs/systemd_units/systemd_units_linux.go +++ b/plugins/inputs/systemd_units/systemd_units_linux.go @@ -123,17 +123,18 @@ type client interface { ListUnitFilesByPatternsContext(ctx context.Context, states, pattern []string) ([]dbus.UnitFile, error) ListUnitsByNamesContext(ctx context.Context, units []string) ([]dbus.UnitStatus, error) GetUnitTypePropertiesContext(ctx context.Context, unit, unitType string) (map[string]interface{}, error) - GetUnitPropertyContext(ctx context.Context, unit, propertyName string) (*dbus.Property, error) + GetUnitPropertiesContext(ctx context.Context, unit string) (map[string]interface{}, error) ListUnitsContext(ctx context.Context) ([]dbus.UnitStatus, error) } type archParams struct { - client client - pattern []string - filter filter.Filter - unitTypeDBus string - scope string - user string + client client + pattern []string + filter filter.Filter + unitTypeDBus string + scope string + user string + warnUnitProps map[string]bool } func (s *SystemdUnits) Init() error { @@ -176,6 +177,8 @@ func (s *SystemdUnits) Init() error { return fmt.Errorf("invalid 'scope' %q", s.Scope) } + s.warnUnitProps = make(map[string]bool) + return nil } @@ -374,26 +377,35 @@ func (s *SystemdUnits) Gather(acc telegraf.Accumulator) error { } // Get required unit file properties - var unitFileState string - if v, err := s.client.GetUnitPropertyContext(ctx, state.Name, "UnitFileState"); err == nil { - unitFileState = strings.Trim(v.Value.String(), `'"`) - } - var unitFilePreset string - if v, err := s.client.GetUnitPropertyContext(ctx, state.Name, "UnitFilePreset"); err == nil { - unitFilePreset = strings.Trim(v.Value.String(), `'"`) + unitProperties, err := s.client.GetUnitPropertiesContext(ctx, state.Name) + if err != nil && !s.warnUnitProps[state.Name] { + s.Log.Warnf("Cannot read unit properties for %q: %v", state.Name, err) + s.warnUnitProps[state.Name] = true } - tags["state"] = unitFileState - tags["preset"] = unitFilePreset + // Set tags + if v, found := unitProperties["UnitFileState"]; found { + tags["state"] = v.(string) + } + if v, found := unitProperties["UnitFilePreset"]; found { + tags["preset"] = v.(string) + } + + // Set fields + if v, found := unitProperties["ActiveEnterTimestamp"]; found { + fields["active_enter_timestamp_us"] = v + } fields["status_errno"] = properties["StatusErrno"] fields["restarts"] = properties["NRestarts"] fields["pid"] = properties["MainPID"] + fields["mem_current"] = properties["MemoryCurrent"] fields["mem_peak"] = properties["MemoryPeak"] + fields["mem_avail"] = properties["MemoryAvailable"] + fields["swap_current"] = properties["MemorySwapCurrent"] fields["swap_peak"] = properties["MemorySwapPeak"] - fields["mem_avail"] = properties["MemoryAvailable"] // Sanitize unset memory fields for k, value := range fields { diff --git a/plugins/inputs/systemd_units/systemd_units_test.go b/plugins/inputs/systemd_units/systemd_units_test.go index 7add99775..3c2c71111 100644 --- a/plugins/inputs/systemd_units/systemd_units_test.go +++ b/plugins/inputs/systemd_units/systemd_units_test.go @@ -4,7 +4,6 @@ package systemd_units import ( "context" - "errors" "fmt" "math" "os/user" @@ -13,7 +12,6 @@ import ( "time" sdbus "github.com/coreos/go-systemd/v22/dbus" - "github.com/godbus/dbus/v5" "github.com/stretchr/testify/require" "github.com/influxdata/telegraf" @@ -25,12 +23,13 @@ import ( ) type properties struct { - uf *sdbus.UnitFile - utype string - state *sdbus.UnitStatus - ufPreset string - ufState string - properties map[string]interface{} + uf *sdbus.UnitFile + utype string + state *sdbus.UnitStatus + ufPreset string + ufState string + ufActiveEnter uint64 + properties map[string]interface{} } func TestDefaultPattern(t *testing.T) { @@ -284,6 +283,7 @@ func TestListFiles(t *testing.T) { } func TestShow(t *testing.T) { + enter := time.Now().UnixMicro() tests := []struct { name string properties map[string]properties @@ -301,8 +301,9 @@ func TestShow(t *testing.T) { ActiveState: "active", SubState: "running", }, - ufPreset: "disabled", - ufState: "enabled", + ufPreset: "disabled", + ufState: "enabled", + ufActiveEnter: uint64(enter), properties: map[string]interface{}{ "Id": "example.service", "StatusErrno": 0, @@ -328,17 +329,18 @@ func TestShow(t *testing.T) { "preset": "disabled", }, map[string]interface{}{ - "load_code": 0, - "active_code": 0, - "sub_code": 0, - "status_errno": 0, - "restarts": 1, - "mem_current": uint64(1000), - "mem_peak": uint64(2000), - "swap_current": uint64(3000), - "swap_peak": uint64(4000), - "mem_avail": uint64(5000), - "pid": 9999, + "load_code": 0, + "active_code": 0, + "sub_code": 0, + "status_errno": 0, + "restarts": 1, + "mem_current": uint64(1000), + "mem_peak": uint64(2000), + "swap_current": uint64(3000), + "swap_peak": uint64(4000), + "mem_avail": uint64(5000), + "pid": 9999, + "active_enter_timestamp_us": uint64(enter), }, time.Unix(0, 0), ), @@ -355,8 +357,9 @@ func TestShow(t *testing.T) { ActiveState: "active", SubState: "exited", }, - ufPreset: "disabled", - ufState: "enabled", + ufPreset: "disabled", + ufState: "enabled", + ufActiveEnter: 0, properties: map[string]interface{}{ "Id": "example.service", "StatusErrno": 0, @@ -376,16 +379,17 @@ func TestShow(t *testing.T) { "preset": "disabled", }, map[string]interface{}{ - "load_code": 0, - "active_code": 0, - "sub_code": 4, - "status_errno": 0, - "restarts": 0, - "mem_current": uint64(0), - "mem_peak": uint64(0), - "swap_current": uint64(0), - "swap_peak": uint64(0), - "mem_avail": uint64(0), + "load_code": 0, + "active_code": 0, + "sub_code": 4, + "status_errno": 0, + "restarts": 0, + "mem_current": uint64(0), + "mem_peak": uint64(0), + "swap_current": uint64(0), + "swap_peak": uint64(0), + "mem_avail": uint64(0), + "active_enter_timestamp_us": uint64(0), }, time.Unix(0, 0), ), @@ -402,8 +406,9 @@ func TestShow(t *testing.T) { ActiveState: "failed", SubState: "failed", }, - ufPreset: "disabled", - ufState: "enabled", + ufPreset: "disabled", + ufState: "enabled", + ufActiveEnter: uint64(enter), properties: map[string]interface{}{ "Id": "example.service", "StatusErrno": 10, @@ -428,16 +433,17 @@ func TestShow(t *testing.T) { "preset": "disabled", }, map[string]interface{}{ - "load_code": 0, - "active_code": 3, - "sub_code": 12, - "status_errno": 10, - "restarts": 1, - "mem_current": uint64(1000), - "mem_peak": uint64(2000), - "swap_current": uint64(3000), - "swap_peak": uint64(4000), - "mem_avail": uint64(5000), + "load_code": 0, + "active_code": 3, + "sub_code": 12, + "status_errno": 10, + "restarts": 1, + "mem_current": uint64(1000), + "mem_peak": uint64(2000), + "swap_current": uint64(3000), + "swap_peak": uint64(4000), + "mem_avail": uint64(5000), + "active_enter_timestamp_us": uint64(enter), }, time.Unix(0, 0), ), @@ -454,8 +460,9 @@ func TestShow(t *testing.T) { ActiveState: "inactive", SubState: "dead", }, - ufPreset: "disabled", - ufState: "enabled", + ufPreset: "disabled", + ufState: "enabled", + ufActiveEnter: uint64(0), properties: map[string]interface{}{ "Id": "example.service", }, @@ -473,14 +480,15 @@ func TestShow(t *testing.T) { "preset": "disabled", }, map[string]interface{}{ - "load_code": 2, - "active_code": 2, - "sub_code": 1, - "mem_current": uint64(0), - "mem_peak": uint64(0), - "swap_current": uint64(0), - "swap_peak": uint64(0), - "mem_avail": uint64(0), + "load_code": 2, + "active_code": 2, + "sub_code": 1, + "mem_current": uint64(0), + "mem_peak": uint64(0), + "swap_current": uint64(0), + "swap_peak": uint64(0), + "mem_avail": uint64(0), + "active_enter_timestamp_us": uint64(0), }, time.Unix(0, 0), ), @@ -517,8 +525,9 @@ func TestShow(t *testing.T) { ActiveState: "inactive", SubState: "dead", }, - ufPreset: "disabled", - ufState: "disabled", + ufPreset: "disabled", + ufState: "disabled", + ufActiveEnter: uint64(0), properties: map[string]interface{}{ "Id": "example.service", "StatusErrno": 0, @@ -543,16 +552,17 @@ func TestShow(t *testing.T) { "preset": "disabled", }, map[string]interface{}{ - "load_code": 0, - "active_code": int64(2), - "sub_code": 1, - "status_errno": 0, - "restarts": 0, - "mem_current": uint64(0), - "mem_peak": uint64(0), - "swap_current": uint64(0), - "swap_peak": uint64(0), - "mem_avail": uint64(0), + "load_code": 0, + "active_code": int64(2), + "sub_code": 1, + "status_errno": 0, + "restarts": 0, + "mem_current": uint64(0), + "mem_peak": uint64(0), + "swap_current": uint64(0), + "swap_peak": uint64(0), + "mem_avail": uint64(0), + "active_enter_timestamp_us": uint64(0), }, time.Unix(0, 0), ), @@ -974,19 +984,17 @@ func (c *fakeClient) GetUnitTypePropertiesContext(_ context.Context, unit, unitT return u.properties, nil } -func (c *fakeClient) GetUnitPropertyContext(_ context.Context, unit, propertyName string) (*sdbus.Property, error) { +func (c *fakeClient) GetUnitPropertiesContext(_ context.Context, unit string) (map[string]interface{}, error) { u, found := c.units[unit] if !found { return nil, nil } - switch propertyName { - case "UnitFileState": - return &sdbus.Property{Name: propertyName, Value: dbus.MakeVariant(u.ufState)}, nil - case "UnitFilePreset": - return &sdbus.Property{Name: propertyName, Value: dbus.MakeVariant(u.ufPreset)}, nil - } - return nil, errors.New("unknown property") + return map[string]interface{}{ + "UnitFileState": u.ufState, + "UnitFilePreset": u.ufPreset, + "ActiveEnterTimestamp": u.ufActiveEnter, + }, nil } func (c *fakeClient) ListUnitsContext(_ context.Context) ([]sdbus.UnitStatus, error) {