diff --git a/go.mod b/go.mod index dbf742952..ff1b8e8f2 100644 --- a/go.mod +++ b/go.mod @@ -323,7 +323,7 @@ require ( github.com/goburrow/serial v0.1.1-0.20211022031912-bfb69110f8dd // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 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.0 // indirect diff --git a/plugins/inputs/systemd_units/README.md b/plugins/inputs/systemd_units/README.md index 16f9b90f3..241f6b6f3 100644 --- a/plugins/inputs/systemd_units/README.md +++ b/plugins/inputs/systemd_units/README.md @@ -1,45 +1,9 @@ -# systemd Units Input Plugin +# Systemd-Units Input Plugin -The systemd_units plugin gathers systemd unit status on Linux. It uses the -`systemctl list-units` or the `systemctl show` command to collect data on -service status. +This plugin gathers the status of systemd-units on Linux, using systemd's DBus +interface. -This plugin is related to the [win_services module](../win_services/README.md), -which fulfills the same purpose on windows. - -This plugin supports two modes of operation: - -## Using `systemctl list-units` - -This is the default mode. It uses the output of -`systemctl list-units [PATTERN] --all --plain --type=service` to collect data on -service status. - -This mode will not supply as much information as `systemctl show`, but will be -compatible with almost every version of systemd. - -The results are tagged with the unit name and provide enumerated fields for -loaded, active and running fields, indicating the unit's health. - -In addition to services, this plugin can gather other unit types as well, -see `systemctl list-units --all --type help` for possible options. - -## Using `systemctl show` - -This mode can be enabled by setting the configuration option `subcommand` to -`show`. The plugin will use -`systemctl show [PATTERN] --all --type=service --property=...` to collect data -on service status. - -This mode will yield more data on the service status. See the metrics chapter -for a list of properties. - -The results are tagged with the unit name, unit status, preset status and -provide enumerated fields for loaded, active and running fields, as well as the -restart count and memory usage of the unit. - -In addition to services, this plugin can gather other unit types as well, -see `systemctl show --all --type help` for possible options. +Please note: At least systemd v230 is required! ## Global configuration options @@ -53,30 +17,42 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. ## Configuration ```toml @sample.conf -# Gather systemd units state +# Gather information about systemd-unit states +# This plugin ONLY supports Linux [[inputs.systemd_units]] - ## Set timeout for systemctl execution - # timeout = "1s" + ## Pattern of units to collect + ## A space-separated list of unit-patterns including wildcards determining + ## the units to collect. + ## ex: pattern = "telegraf* influxdb* user@*" + # pattern = "*" - ## Select the systemctl subcommand to use to gather information. - ## Using `list-units` is the option with the broadest compatibility. - ## Using `show` will get more information but may fail to list all units on - ## some systems. - # subcommand = "list-units" - - ## Filter for a specific unit type, default is "service", other possible - ## values are "socket", "target", "device", "mount", "automount", "swap", - ## "timer", "path", "slice" and "scope ": + ## Filter for a specific unit type + ## Available settings are: service, socket, target, device, mount, + ## automount, swap, timer, path, slice and scope # unittype = "service" - ## Filter for a specific pattern, default is "" (i.e. all), other possible - ## values are valid pattern for systemctl, e.g. "a*" for all units with - ## names starting with "a" - ## pattern = "telegraf* influxdb*" - ## pattern = "a*" - # pattern = "" + ## Collect detailed information for the units + # details = false + + ## Timeout for state-collection + # timeout = "1s" ``` +This plugin supports two modes of operation: + +### Non-detailed mode + +This is the default mode, collecting data on the unit's status only without +further details on the unit. + +### Detailed mode + +This mode can be enabled by setting the configuration option `details` to +`true`. In this mode the plugin collects all information of the non-detailed +mode but provides additional unit information such as memory usage, +restart-counts, PID, etc. See the [metrics section](#metrics) below for a list +of all properties collected. + ## Metrics These metrics are available in both modes: @@ -92,25 +68,25 @@ These metrics are available in both modes: - active_code (int, see below) - sub_code (int, see below) -The following additional metrics are available whe using `subcommand = "show"`: +The following *additional* metrics are available with `details = true`: - systemd_units: - tags: - - uf_state (string, unit file state) - - uf_preset (string, unit file preset state) + - state (string, unit file state) + - preset (string, unit file preset state) - fields: - status_errno (int, last error) - restarts (int, number of restarts) - - mem_current (int, current memory usage) - - mem_peak (int, peak memory usage) - - swap_current (int, current swap usage) - - swap_peak (int, peak swap usage) - - mem_avail (int, available memory for this unit) - pid (int, pid of the main process) + - mem_current (uint, current memory usage) + - mem_peak (uint, peak memory usage) + - swap_current (uint, current swap usage) + - swap_peak (uint, peak swap usage) + - mem_avail (uint, available memory for this unit) ### Load -enumeration of [unit_load_state_table][1] +Enumeration of [unit_load_state_table][1] | Value | Meaning | Description | | ----- | ------- | ----------- | @@ -126,7 +102,7 @@ enumeration of [unit_load_state_table][1] ### Active -enumeration of [unit_active_state_table][2] +Enumeration of [unit_active_state_table][2] | Value | Meaning | Description | | ----- | ------- | ----------- | @@ -213,7 +189,7 @@ were removed, tables are hex aligned to keep some space for future values ## Example Output -### Output in `list-units` mode +### Output in non-detailed mode ```text systemd_units,host=host1.example.com,name=dbus.service,load=loaded,active=active,sub=running load_code=0i,active_code=0i,sub_code=0i 1533730725000000000 @@ -221,10 +197,10 @@ systemd_units,host=host1.example.com,name=networking.service,load=loaded,active= systemd_units,host=host1.example.com,name=ssh.service,load=loaded,active=active,sub=running load_code=0i,active_code=0i,sub_code=0i 1533730725000000000 ``` -### Output in `show` mode +### Output in detailed mode ```text -systemd_units,active=active,host=host1.example.com,load=loaded,name=dbus.service,sub=running,uf_preset=disabled,uf_state=static active_code=0i,load_code=0i,mem_avail=6470856704i,mem_current=2691072i,mem_peak=3895296i,pid=481i,restarts=0i,status_errno=0i,sub_code=0i,swap_current=794624i,swap_peak=884736i 1533730725000000000 +systemd_units,active=active,host=host1.example.com,load=loaded,name=dbus.service,sub=running,preset=disabled,state=static active_code=0i,load_code=0i,mem_avail=6470856704i,mem_current=2691072i,mem_peak=3895296i,pid=481i,restarts=0i,status_errno=0i,sub_code=0i,swap_current=794624i,swap_peak=884736i 1533730725000000000 systemd_units,active=inactive,host=host1.example.com,load=not-found,name=networking.service,sub=dead active_code=2i,load_code=2i,pid=0i,restarts=0i,status_errno=0i,sub_code=1i 1533730725000000000 -systemd_units,active=active,host=host1.example.com,load=loaded,name=pcscd.service,sub=running,uf_preset=disabled,uf_state=indirect active_code=0i,load_code=0i,mem_avail=6370541568i,mem_current=512000i,mem_peak=4399104i,pid=1673i,restarts=0i,status_errno=0i,sub_code=0i,swap_current=3149824i,swap_peak=3149824i 1533730725000000000 +systemd_units,active=active,host=host1.example.com,load=loaded,name=pcscd.service,sub=running,preset=disabled,state=indirect active_code=0i,load_code=0i,mem_avail=6370541568i,mem_current=512000i,mem_peak=4399104i,pid=1673i,restarts=0i,status_errno=0i,sub_code=0i,swap_current=3149824i,swap_peak=3149824i 1533730725000000000 ``` diff --git a/plugins/inputs/systemd_units/sample.conf b/plugins/inputs/systemd_units/sample.conf index 4f54766bd..7a8aac9b1 100644 --- a/plugins/inputs/systemd_units/sample.conf +++ b/plugins/inputs/systemd_units/sample.conf @@ -1,22 +1,19 @@ -# Gather systemd units state +# Gather information about systemd-unit states +# This plugin ONLY supports Linux [[inputs.systemd_units]] - ## Set timeout for systemctl execution - # timeout = "1s" + ## Pattern of units to collect + ## A space-separated list of unit-patterns including wildcards determining + ## the units to collect. + ## ex: pattern = "telegraf* influxdb* user@*" + # pattern = "*" - ## Select the systemctl subcommand to use to gather information. - ## Using `list-units` is the option with the broadest compatibility. - ## Using `show` will get more information but may fail to list all units on - ## some systems. - # subcommand = "list-units" - - ## Filter for a specific unit type, default is "service", other possible - ## values are "socket", "target", "device", "mount", "automount", "swap", - ## "timer", "path", "slice" and "scope ": + ## Filter for a specific unit type + ## Available settings are: service, socket, target, device, mount, + ## automount, swap, timer, path, slice and scope # unittype = "service" - ## Filter for a specific pattern, default is "" (i.e. all), other possible - ## values are valid pattern for systemctl, e.g. "a*" for all units with - ## names starting with "a" - ## pattern = "telegraf* influxdb*" - ## pattern = "a*" - # pattern = "" + ## Collect detailed information for the units + # details = false + + ## Timeout for state-collection + # timeout = "1s" diff --git a/plugins/inputs/systemd_units/subcommand_list.go b/plugins/inputs/systemd_units/subcommand_list.go deleted file mode 100644 index 004671175..000000000 --- a/plugins/inputs/systemd_units/subcommand_list.go +++ /dev/null @@ -1,86 +0,0 @@ -//go:generate ../../../tools/readme_config_includer/generator -package systemd_units - -import ( - "bufio" - "bytes" - "fmt" - "strings" - - "github.com/influxdata/telegraf" -) - -// Gather parses systemctl outputs and adds counters to the Accumulator -func parseListUnits(acc telegraf.Accumulator, buffer *bytes.Buffer) { - scanner := bufio.NewScanner(buffer) - for scanner.Scan() { - line := scanner.Text() - - data := strings.Fields(line) - if len(data) < 4 { - acc.AddError(fmt.Errorf("parsing line failed (expected at least 4 fields): %s", line)) - continue - } - name := data[0] - load := data[1] - active := data[2] - sub := data[3] - tags := map[string]string{ - "name": name, - "load": load, - "active": active, - "sub": sub, - } - - var ( - loadCode int - activeCode int - subCode int - ok bool - ) - if loadCode, ok = loadMap[load]; !ok { - acc.AddError(fmt.Errorf("parsing field 'load' failed, value not in map: %s", load)) - continue - } - if activeCode, ok = activeMap[active]; !ok { - acc.AddError(fmt.Errorf("parsing field field 'active' failed, value not in map: %s", active)) - continue - } - if subCode, ok = subMap[sub]; !ok { - acc.AddError(fmt.Errorf("parsing field field 'sub' failed, value not in map: %s", sub)) - continue - } - fields := map[string]interface{}{ - "load_code": loadCode, - "active_code": activeCode, - "sub_code": subCode, - } - - acc.AddFields(measurement, fields, tags) - } -} - -func getListUnitsParameters(s *SystemdUnits) *[]string { - // build parameters for systemctl call - params := []string{"list-units"} - // create patterns parameters if provided in config - if s.Pattern != "" { - psplit := strings.SplitN(s.Pattern, " ", -1) - params = append(params, psplit...) - } - params = append(params, - "--all", - "--plain", - "--type="+s.UnitType, - "--no-legend", - ) - - return ¶ms -} - -func initSubcommandListUnits() *subCommandInfo { - return &subCommandInfo{ - getParameters: getListUnitsParameters, - parseResult: parseListUnits, - } -} diff --git a/plugins/inputs/systemd_units/subcommand_list_test.go b/plugins/inputs/systemd_units/subcommand_list_test.go deleted file mode 100644 index 36607452d..000000000 --- a/plugins/inputs/systemd_units/subcommand_list_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package systemd_units - -import ( - "fmt" - "testing" -) - -func TestSubcommandList(t *testing.T) { - tests := []TestDef{ - { - Name: "example loaded active running", - Line: "example.service loaded active running example service description", - Tags: map[string]string{ - "name": "example.service", - "load": "loaded", - "active": "active", - "sub": "running", - }, - Fields: map[string]interface{}{ - "load_code": 0, - "active_code": 0, - "sub_code": 0, - }, - }, - { - Name: "example loaded active exited", - Line: "example.service loaded active exited example service description", - Tags: map[string]string{ - "name": "example.service", - "load": "loaded", - "active": "active", - "sub": "exited", - }, - Fields: map[string]interface{}{ - "load_code": 0, - "active_code": 0, - "sub_code": 4, - }, - }, - { - Name: "example loaded failed failed", - Line: "example.service loaded failed failed example service description", - Tags: map[string]string{"name": "example.service", "load": "loaded", "active": "failed", "sub": "failed"}, - Fields: map[string]interface{}{ - "load_code": 0, - "active_code": 3, - "sub_code": 12, - }, - }, - { - Name: "example not-found inactive dead", - Line: "example.service not-found inactive dead example service description", - Tags: map[string]string{ - "name": "example.service", - "load": "not-found", - "active": "inactive", - "sub": "dead", - }, - Fields: map[string]interface{}{ - "load_code": 2, - "active_code": 2, - "sub_code": 1, - }, - }, - { - Name: "example unknown unknown unknown", - Line: "example.service unknown unknown unknown example service description", - Err: fmt.Errorf("parsing field 'load' failed, value not in map: %s", "unknown"), - }, - { - Name: "example too few fields", - Line: "example.service loaded fai", - Err: fmt.Errorf("parsing line failed (expected at least 4 fields): %s", "example.service loaded fai"), - }, - } - - dut := initSubcommandListUnits() - - runParserTests(t, tests, dut) -} - -func TestCommandlineList(t *testing.T) { - // Test using the default pattern (no pattern) - paramsTemplate := []string{ - "list-units", - "--all", - "--plain", - "--no-legend", - "--type=service", - } - - dut := initSubcommandListUnits() - systemdUnits := SystemdUnits{ - UnitType: "service", - } - - runCommandLineTest(t, paramsTemplate, dut, &systemdUnits) - - // Test using a more complex pattern - paramsTemplate = []string{ - "list-units", - "unita.service", - "*.timer", - "--all", - "--plain", - "--no-legend", - "--type=service", - } - - systemdUnits = SystemdUnits{ - UnitType: "service", - Pattern: "unita.service *.timer", - } - - runCommandLineTest(t, paramsTemplate, dut, &systemdUnits) -} diff --git a/plugins/inputs/systemd_units/subcommand_show.go b/plugins/inputs/systemd_units/subcommand_show.go deleted file mode 100644 index 5f5042ec5..000000000 --- a/plugins/inputs/systemd_units/subcommand_show.go +++ /dev/null @@ -1,147 +0,0 @@ -//go:generate ../../../tools/readme_config_includer/generator -package systemd_units - -import ( - "bufio" - "bytes" - "fmt" - "strconv" - "strings" - - "github.com/influxdata/telegraf" -) - -type valueDef struct { - valueName string - valueMap *map[string]int -} - -// The following two maps configure the mapping of systemd properties to -// tags or values. The properties are automatically requested from -// `systemctl show`. -// If a vlaue has no valueMap, `atoi` is called on the value to convert it to -// an integer. -var tagMap = map[string]string{ - "Id": "name", - "LoadState": "load", - "ActiveState": "active", - "SubState": "sub", - "UnitFileState": "uf_state", - "UnitFilePreset": "uf_preset", -} - -var valueMap = map[string]valueDef{ - "LoadState": {valueName: "load_code", valueMap: &loadMap}, - "ActiveState": {valueName: "active_code", valueMap: &activeMap}, - "SubState": {valueName: "sub_code", valueMap: &subMap}, - "StatusErrno": {valueName: "status_errno", valueMap: nil}, - "NRestarts": {valueName: "restarts", valueMap: nil}, - "MemoryCurrent": {valueName: "mem_current", valueMap: nil}, - "MemoryPeak": {valueName: "mem_peak", valueMap: nil}, - "MemorySwapCurrent": {valueName: "swap_current", valueMap: nil}, - "MemorySwapPeak": {valueName: "swap_peak", valueMap: nil}, - "MemoryAvailable": {valueName: "mem_avail", valueMap: nil}, - "MainPID": {valueName: "pid", valueMap: nil}, -} - -// Gather parses systemctl outputs and adds counters to the Accumulator -func parseShow(acc telegraf.Accumulator, buffer *bytes.Buffer) { - scanner := bufio.NewScanner(buffer) - - tags := make(map[string]string) - fields := make(map[string]interface{}) - - for scanner.Scan() { - line := scanner.Text() - - // An empty line signals the start of the next unit - if len(line) == 0 { - // We need at least a "name" field. This prevents values from the - // global information block (enabled by the --all switch) to be - // shown as a unit. - if _, ok := tags["name"]; ok { - acc.AddFields(measurement, fields, tags) - } - - tags = make(map[string]string) - fields = make(map[string]interface{}) - - continue - } - - key, value, ok := strings.Cut(line, "=") - if !ok { - acc.AddError(fmt.Errorf("error parsing line (expected key=value): %s", line)) - continue - } - - // Map the tags - if tagName, isTag := tagMap[key]; isTag { - tags[tagName] = value - } - - // Map the values - if valueDef, isValue := valueMap[key]; isValue { - // If a value map is set use it. If not, just try to convert the - // value into an integer. - if valueDef.valueMap != nil { - code, ok := (*valueDef.valueMap)[value] - if !ok { - acc.AddError(fmt.Errorf("error parsing field '%s', value '%s' not in map", key, value)) - continue - } - - fields[valueDef.valueName] = code - } else { - if value != "[not set]" { - intVal, err := strconv.Atoi(value) - if err != nil { - acc.AddError(fmt.Errorf("error '%w' parsing field '%s'. Not an integer value", err, key)) - continue - } - fields[valueDef.valueName] = intVal - } - } - } - } - - // Add the last unit because the output does not contain a newline for this - if _, ok := tags["name"]; ok { - acc.AddFields(measurement, fields, tags) - } -} - -func getShowParameters(s *SystemdUnits) *[]string { - // build parameters for systemctl call - params := []string{"show"} - // create patterns parameters if provided in config - if s.Pattern == "" { - params = append(params, "*") - } else { - psplit := strings.SplitN(s.Pattern, " ", -1) - params = append(params, psplit...) - } - - params = append(params, "--all", "--type="+s.UnitType) - - // add the fields we're interested in to the command line - for property := range tagMap { - params = append(params, "--property="+property) - } - for property := range valueMap { - // If a property exists within the tagMap it was already added. Do not add it again to - // keep the command line short. - if _, exists := tagMap[property]; !exists { - params = append(params, "--property="+property) - } - } - - return ¶ms -} - -func initSubcommandShow() *subCommandInfo { - return &subCommandInfo{ - getParameters: getShowParameters, - parseResult: parseShow, - } -} diff --git a/plugins/inputs/systemd_units/subcommand_show_test.go b/plugins/inputs/systemd_units/subcommand_show_test.go deleted file mode 100644 index c26155323..000000000 --- a/plugins/inputs/systemd_units/subcommand_show_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package systemd_units - -import ( - "fmt" - "testing" -) - -func TestSubcommandShow(t *testing.T) { - tests := []TestDef{ - { - Name: "example loaded active running", - Lines: []string{ - "Id=example.service", - "LoadState=loaded", - "ActiveState=active", - "SubState=running", - "UnitFileState=enabled", - "UnitFilePreset=disabled", - "StatusErrno=0", - "NRestarts=1", - "MemoryCurrent=1000", - "MemoryPeak=2000", - "MemorySwapCurrent=3000", - "MemorySwapPeak=4000", - "MemoryAvailable=5000", - "MainPID=9999", - }, - Tags: map[string]string{ - "name": "example.service", - "load": "loaded", - "active": "active", - "sub": "running", - "uf_state": "enabled", - "uf_preset": "disabled", - }, - Fields: map[string]interface{}{ - "load_code": 0, - "active_code": 0, - "sub_code": 0, - "status_errno": 0, - "restarts": 1, - "mem_current": 1000, - "mem_peak": 2000, - "swap_current": 3000, - "swap_peak": 4000, - "mem_avail": 5000, - "pid": 9999, - }, - }, - { - Name: "example loaded active exited", - Lines: []string{ - "Id=example.service", - "LoadState=loaded", - "ActiveState=active", - "SubState=exited", - "UnitFileState=enabled", - "UnitFilePreset=disabled", - "StatusErrno=0", - "NRestarts=0", - }, - Tags: map[string]string{ - "name": "example.service", - "load": "loaded", - "active": "active", - "sub": "exited", - "uf_state": "enabled", - "uf_preset": "disabled", - }, - Fields: map[string]interface{}{ - "load_code": 0, - "active_code": 0, - "sub_code": 4, - "status_errno": 0, - "restarts": 0, - }, - }, - { - Name: "example loaded failed failed", - Lines: []string{ - "Id=example.service", - "LoadState=loaded", - "ActiveState=failed", - "SubState=failed", - "UnitFileState=enabled", - "UnitFilePreset=disabled", - "StatusErrno=10", - "NRestarts=1", - "MemoryCurrent=1000", - "MemoryPeak=2000", - "MemorySwapCurrent=3000", - "MemorySwapPeak=4000", - "MemoryAvailable=5000", - }, - Tags: map[string]string{ - "name": "example.service", - "load": "loaded", - "active": "failed", - "sub": "failed", - "uf_state": "enabled", - "uf_preset": "disabled", - }, - Fields: map[string]interface{}{ - "load_code": 0, - "active_code": 3, - "sub_code": 12, - "status_errno": 10, - "restarts": 1, - "mem_current": 1000, - "mem_peak": 2000, - "swap_current": 3000, - "swap_peak": 4000, - "mem_avail": 5000, - }, - }, - { - Name: "example not-found inactive dead", - Lines: []string{ - "Id=example.service", - "LoadState=not-found", - "ActiveState=inactive", - "SubState=dead", - "UnitFileState=enabled", - "UnitFilePreset=disabled", - "StatusErrno=[not set]", - "NRestarts=[not set]", - "MemoryCurrent=[not set]", - "MemoryPeak=[not set]", - "MemorySwapCurrent=[not set]", - "MemorySwapPeak=[not set]", - "MemoryAvailable=[not set]", - "MainPID=[not set]", - }, - Tags: map[string]string{ - "name": "example.service", - "load": "not-found", - "active": "inactive", - "sub": "dead", - "uf_state": "enabled", - "uf_preset": "disabled", - }, - Fields: map[string]interface{}{ - "load_code": 2, - "active_code": 2, - "sub_code": 1, - }, - }, - { - Name: "example unknown unknown unknown", - Lines: []string{ - "Id=example.service", - "LoadState=unknown", - "ActiveState=unknown", - "SubState=unknown", - "UnitFileState=unknown", - "UnitFilePreset=unknown", - }, - Err: fmt.Errorf("error parsing field '%s', value '%s' not in map", "LoadState", "unknown"), - }, - { - Name: "example no key value pair", - Lines: []string{ - "Id=example.service", - "LoadState", - "ActiveState=active", - }, - Err: fmt.Errorf("error parsing line (expected key=value): %s", "LoadState"), - Tags: map[string]string{ - "name": "example.service", - "active": "active", - }, - Fields: map[string]interface{}{ - "active_code": 0, - }, - }, - } - - dut := initSubcommandShow() - - runParserTests(t, tests, dut) -} - -func TestCommandlineShow(t *testing.T) { - propertiesTemplate := []string{ - "--all", - "--type=service", - "--property=Id", - "--property=LoadState", - "--property=ActiveState", - "--property=SubState", - "--property=StatusErrno", - "--property=UnitFileState", - "--property=UnitFilePreset", - "--property=NRestarts", - "--property=MemoryCurrent", - "--property=MemoryPeak", - "--property=MemorySwapCurrent", - "--property=MemorySwapPeak", - "--property=MemoryAvailable", - "--property=MainPID", - } - - // Test with the default patern - paramsTemplate := append([]string{ - "show", - "*", - }, propertiesTemplate...) - - dut := initSubcommandShow() - systemdUnits := SystemdUnits{ - UnitType: "service", - } - - runCommandLineTest(t, paramsTemplate, dut, &systemdUnits) - - // Test using a more komplex pattern - paramsTemplate = append([]string{ - "show", - "unita.service", - "*.timer", - }, propertiesTemplate...) - - systemdUnits = SystemdUnits{ - UnitType: "service", - Pattern: "unita.service *.timer", - } - - runCommandLineTest(t, paramsTemplate, dut, &systemdUnits) -} diff --git a/plugins/inputs/systemd_units/systemd_units.go b/plugins/inputs/systemd_units/systemd_units.go index 2f501369e..839e7e2e4 100644 --- a/plugins/inputs/systemd_units/systemd_units.go +++ b/plugins/inputs/systemd_units/systemd_units.go @@ -2,199 +2,33 @@ package systemd_units import ( - "bytes" _ "embed" - "fmt" - "os/exec" - "strings" "time" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" - "github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/plugins/inputs" ) //go:embed sample.conf var sampleConfig string -const measurement = "systemd_units" - -// Below are mappings of systemd state tables as defined in -// https://github.com/systemd/systemd/blob/c87700a1335f489be31cd3549927da68b5638819/src/basic/unit-def.c -// Duplicate strings are removed from this list. -// This map is used by `subcommand_show` and `subcommand_list`. Changes must be -// compatible with both subcommands. -var loadMap = map[string]int{ - "loaded": 0, - "stub": 1, - "not-found": 2, - "bad-setting": 3, - "error": 4, - "merged": 5, - "masked": 6, -} - -var activeMap = map[string]int{ - "active": 0, - "reloading": 1, - "inactive": 2, - "failed": 3, - "activating": 4, - "deactivating": 5, -} - -var subMap = map[string]int{ - // service_state_table, offset 0x0000 - "running": 0x0000, - "dead": 0x0001, - "start-pre": 0x0002, - "start": 0x0003, - "exited": 0x0004, - "reload": 0x0005, - "stop": 0x0006, - "stop-watchdog": 0x0007, - "stop-sigterm": 0x0008, - "stop-sigkill": 0x0009, - "stop-post": 0x000a, - "final-sigterm": 0x000b, - "failed": 0x000c, - "auto-restart": 0x000d, - "condition": 0x000e, - "cleaning": 0x000f, - - // automount_state_table, offset 0x0010 - // continuation of service_state_table - "waiting": 0x0010, - "reload-signal": 0x0011, - "reload-notify": 0x0012, - "final-watchdog": 0x0013, - "dead-before-auto-restart": 0x0014, - "failed-before-auto-restart": 0x0015, - "dead-resources-pinned": 0x0016, - "auto-restart-queued": 0x0017, - - // device_state_table, offset 0x0020 - "tentative": 0x0020, - "plugged": 0x0021, - - // mount_state_table, offset 0x0030 - "mounting": 0x0030, - "mounting-done": 0x0031, - "mounted": 0x0032, - "remounting": 0x0033, - "unmounting": 0x0034, - "remounting-sigterm": 0x0035, - "remounting-sigkill": 0x0036, - "unmounting-sigterm": 0x0037, - "unmounting-sigkill": 0x0038, - - // path_state_table, offset 0x0040 - - // scope_state_table, offset 0x0050 - "abandoned": 0x0050, - - // slice_state_table, offset 0x0060 - "active": 0x0060, - - // socket_state_table, offset 0x0070 - "start-chown": 0x0070, - "start-post": 0x0071, - "listening": 0x0072, - "stop-pre": 0x0073, - "stop-pre-sigterm": 0x0074, - "stop-pre-sigkill": 0x0075, - "final-sigkill": 0x0076, - - // swap_state_table, offset 0x0080 - "activating": 0x0080, - "activating-done": 0x0081, - "deactivating": 0x0082, - "deactivating-sigterm": 0x0083, - "deactivating-sigkill": 0x0084, - - // target_state_table, offset 0x0090 - - // timer_state_table, offset 0x00a0 - "elapsed": 0x00a0, -} - // SystemdUnits is a telegraf plugin to gather systemd unit status type SystemdUnits struct { - Timeout config.Duration `toml:"timeout"` - SubCommand string `toml:"subcommand"` - UnitType string `toml:"unittype"` - Pattern string `toml:"pattern"` - resultParser parseResultFunc - commandParams *[]string + Pattern string `toml:"pattern"` + UnitType string `toml:"unittype"` + Details bool `toml:"details"` + Timeout config.Duration `toml:"timeout"` + Log telegraf.Logger `toml:"-"` + archParams } -type getParametersFunc func(*SystemdUnits) *[]string -type parseResultFunc func(telegraf.Accumulator, *bytes.Buffer) - -type subCommandInfo struct { - getParameters getParametersFunc - parseResult parseResultFunc -} - -var ( - defaultSubCommand = "list-units" - defaultTimeout = config.Duration(time.Second) - defaultUnitType = "service" - defaultPattern = "" -) - func (*SystemdUnits) SampleConfig() string { return sampleConfig } -func (s *SystemdUnits) Init() error { - var subCommandInfo *subCommandInfo - - switch s.SubCommand { - case "show": - subCommandInfo = initSubcommandShow() - case "list-units": - subCommandInfo = initSubcommandListUnits() - default: - return fmt.Errorf("invalid value for 'subcommand': %s", s.SubCommand) - } - - // Save the parsing function for later and pre-compute the - // command line because it will not change. - s.resultParser = subCommandInfo.parseResult - s.commandParams = subCommandInfo.getParameters(s) - - return nil -} - -func (s *SystemdUnits) Gather(acc telegraf.Accumulator) error { - // is systemctl available ? - systemctlPath, err := exec.LookPath("systemctl") - if err != nil { - return err - } - - cmd := exec.Command(systemctlPath, *s.commandParams...) - var out bytes.Buffer - cmd.Stdout = &out - err = internal.RunTimeout(cmd, time.Duration(s.Timeout)) - if err != nil { - return fmt.Errorf("error running systemctl %q: %w", strings.Join(*s.commandParams, " "), err) - } - - s.resultParser(acc, &out) - - return nil -} - func init() { inputs.Add("systemd_units", func() telegraf.Input { - return &SystemdUnits{ - Timeout: defaultTimeout, - UnitType: defaultUnitType, - Pattern: defaultPattern, - SubCommand: defaultSubCommand, - } + return &SystemdUnits{Timeout: config.Duration(time.Second)} }) } diff --git a/plugins/inputs/systemd_units/systemd_units_linux.go b/plugins/inputs/systemd_units/systemd_units_linux.go new file mode 100644 index 000000000..16badae79 --- /dev/null +++ b/plugins/inputs/systemd_units/systemd_units_linux.go @@ -0,0 +1,364 @@ +//go:build linux + +package systemd_units + +import ( + "context" + "fmt" + "math" + "path" + "strings" + "time" + + "github.com/coreos/go-systemd/v22/dbus" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/filter" +) + +// Below are mappings of systemd state tables as defined in +// https://github.com/systemd/systemd/blob/c87700a1335f489be31cd3549927da68b5638819/src/basic/unit-def.c +// Duplicate strings are removed from this list. +// This map is used by `subcommand_show` and `subcommand_list`. Changes must be +// compatible with both subcommands. +var loadMap = map[string]int{ + "loaded": 0, + "stub": 1, + "not-found": 2, + "bad-setting": 3, + "error": 4, + "merged": 5, + "masked": 6, +} + +var activeMap = map[string]int{ + "active": 0, + "reloading": 1, + "inactive": 2, + "failed": 3, + "activating": 4, + "deactivating": 5, +} + +var subMap = map[string]int{ + // service_state_table, offset 0x0000 + "running": 0x0000, + "dead": 0x0001, + "start-pre": 0x0002, + "start": 0x0003, + "exited": 0x0004, + "reload": 0x0005, + "stop": 0x0006, + "stop-watchdog": 0x0007, + "stop-sigterm": 0x0008, + "stop-sigkill": 0x0009, + "stop-post": 0x000a, + "final-sigterm": 0x000b, + "failed": 0x000c, + "auto-restart": 0x000d, + "condition": 0x000e, + "cleaning": 0x000f, + + // automount_state_table, offset 0x0010 + // continuation of service_state_table + "waiting": 0x0010, + "reload-signal": 0x0011, + "reload-notify": 0x0012, + "final-watchdog": 0x0013, + "dead-before-auto-restart": 0x0014, + "failed-before-auto-restart": 0x0015, + "dead-resources-pinned": 0x0016, + "auto-restart-queued": 0x0017, + + // device_state_table, offset 0x0020 + "tentative": 0x0020, + "plugged": 0x0021, + + // mount_state_table, offset 0x0030 + "mounting": 0x0030, + "mounting-done": 0x0031, + "mounted": 0x0032, + "remounting": 0x0033, + "unmounting": 0x0034, + "remounting-sigterm": 0x0035, + "remounting-sigkill": 0x0036, + "unmounting-sigterm": 0x0037, + "unmounting-sigkill": 0x0038, + + // path_state_table, offset 0x0040 + + // scope_state_table, offset 0x0050 + "abandoned": 0x0050, + + // slice_state_table, offset 0x0060 + "active": 0x0060, + + // socket_state_table, offset 0x0070 + "start-chown": 0x0070, + "start-post": 0x0071, + "listening": 0x0072, + "stop-pre": 0x0073, + "stop-pre-sigterm": 0x0074, + "stop-pre-sigkill": 0x0075, + "final-sigkill": 0x0076, + + // swap_state_table, offset 0x0080 + "activating": 0x0080, + "activating-done": 0x0081, + "deactivating": 0x0082, + "deactivating-sigterm": 0x0083, + "deactivating-sigkill": 0x0084, + + // target_state_table, offset 0x0090 + + // timer_state_table, offset 0x00a0 + "elapsed": 0x00a0, +} + +type unitInfo struct { + name string + state dbus.UnitStatus + properties map[string]interface{} + unitFileState string + unitFilePreset string +} + +type client interface { + Connected() bool + Close() + + 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) + ListUnitsContext(ctx context.Context) ([]dbus.UnitStatus, error) +} + +type archParams struct { + client client + pattern []string + filter filter.Filter +} + +func (s *SystemdUnits) Init() error { + // Set default pattern + if s.Pattern == "" { + s.Pattern = "*" + } + + // Check unit-type and convert the first letter to uppercase as this is + // what dbus expects. + switch s.UnitType { + case "": + s.UnitType = "service" + case "service", "socket", "target", "device", "mount", "automount", "swap", + "timer", "path", "slice", "scope": + default: + return fmt.Errorf("invalid 'unittype' %q", s.UnitType) + } + s.UnitType = strings.ToUpper(s.UnitType[0:1]) + strings.ToLower(s.UnitType[1:]) + + s.pattern = strings.Split(s.Pattern, " ") + f, err := filter.Compile(s.pattern) + if err != nil { + return fmt.Errorf("compiling filter failed: %w", err) + } + s.filter = f + + return nil +} + +func (s *SystemdUnits) Start(telegraf.Accumulator) error { + ctx := context.Background() + client, err := dbus.NewSystemConnectionContext(ctx) + if err != nil { + return err + } + s.client = client + + return nil +} + +func (s *SystemdUnits) Stop() { + if s.client != nil && s.client.Connected() { + s.client.Close() + } +} + +func (s *SystemdUnits) Gather(acc telegraf.Accumulator) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.Timeout)) + defer cancel() + + // List all loaded units to handle multi-instance units correctly + loaded, err := s.client.ListUnitsContext(ctx) + if err != nil { + return fmt.Errorf("listing loaded units failed: %w", err) + } + + // List all unit files matching the pattern to also get disabled units + list := []string{"enabled", "disabled", "static"} + files, err := s.client.ListUnitFilesByPatternsContext(ctx, list, s.pattern) + if err != nil { + return fmt.Errorf("listing unit files failed: %w", err) + } + + // Collect all matching units, the loaded ones and the disabled ones + states := make([]dbus.UnitStatus, 0, len(files)) + + // Match all loaded units first + seen := make(map[string]bool) + for _, u := range loaded { + if !s.filter.Match(u.Name) { + continue + } + states = append(states, u) + + // Remember multi-instance units to remove duplicates from files + instance := u.Name + if strings.Contains(u.Name, "@") { + prefix, _, _ := strings.Cut(u.Name, "@") + suffix := path.Ext(u.Name) + instance = prefix + "@" + suffix + } + seen[instance] = true + } + + // Now split the unit-files into disabled ones and static ones, ignore + // enabled units as those are already contained in the "loaded" list. + disabled := make([]string, 0, len(files)) + static := make([]string, 0, len(files)) + for _, f := range files { + name := path.Base(f.Path) + + switch f.Type { + case "disabled": + if seen[name] { + continue + } + seen[name] = true + disabled = append(disabled, name) + case "static": + // Make sure we filter already loaded static multi-instance units + instance := name + if strings.Contains(name, "@") { + prefix, _, _ := strings.Cut(name, "@") + suffix := path.Ext(name) + instance = prefix + "@" + suffix + } + if seen[instance] || seen[name] { + continue + } + seen[instance] = true + static = append(static, name) + } + } + + // Resolve the disabled and remaining static units + disabledStates, err := s.client.ListUnitsByNamesContext(ctx, disabled) + if err != nil { + return fmt.Errorf("listing unit states failed: %w", err) + } + states = append(states, disabledStates...) + + // Add special information about unused static units + for _, name := range static { + if !strings.EqualFold(strings.TrimPrefix(path.Ext(name), "."), s.UnitType) { + continue + } + + states = append(states, dbus.UnitStatus{ + Name: name, + LoadState: "stub", + ActiveState: "inactive", + SubState: "dead", + }) + } + + // Merge the unit information into one struct + units := make([]unitInfo, 0, len(states)) + for _, state := range states { + // Filter units of the wrong type + props, err := s.client.GetUnitTypePropertiesContext(ctx, state.Name, s.UnitType) + if err != nil { + continue + } + + u := unitInfo{ + name: state.Name, + state: state, + properties: props, + } + + // Get required unit file properties + if v, err := s.client.GetUnitPropertyContext(ctx, state.Name, "UnitFileState"); err == nil { + u.unitFileState = strings.Trim(v.Value.String(), `'"`) + } + if v, err := s.client.GetUnitPropertyContext(ctx, state.Name, "UnitFilePreset"); err == nil { + u.unitFilePreset = strings.Trim(v.Value.String(), `'"`) + } + + units = append(units, u) + } + + // Create the metrics + for _, u := range units { + // Map the state names to numerical values + load, ok := loadMap[u.state.LoadState] + if !ok { + acc.AddError(fmt.Errorf("parsing field 'load' failed, value not in map: %s", u.state.LoadState)) + continue + } + active, ok := activeMap[u.state.ActiveState] + if !ok { + acc.AddError(fmt.Errorf("parsing field field 'active' failed, value not in map: %s", u.state.ActiveState)) + continue + } + subState, ok := subMap[u.state.SubState] + if !ok { + acc.AddError(fmt.Errorf("parsing field field 'sub' failed, value not in map: %s", u.state.SubState)) + continue + } + + // Create the metric + tags := map[string]string{ + "name": u.name, + "load": u.state.LoadState, + "active": u.state.ActiveState, + "sub": u.state.SubState, + } + + fields := map[string]interface{}{ + "load_code": load, + "active_code": active, + "sub_code": subState, + } + + if s.Details { + tags["state"] = u.unitFileState + tags["preset"] = u.unitFilePreset + + fields["status_errno"] = u.properties["StatusErrno"] + fields["restarts"] = u.properties["NRestarts"] + fields["pid"] = u.properties["MainPID"] + fields["mem_current"] = u.properties["MemoryCurrent"] + fields["mem_peak"] = u.properties["MemoryPeak"] + fields["swap_current"] = u.properties["MemorySwapCurrent"] + fields["swap_peak"] = u.properties["MemorySwapPeak"] + fields["mem_avail"] = u.properties["MemoryAvailable"] + + // Sanitize unset memory fields + for k, value := range fields { + switch { + case strings.HasPrefix(k, "mem_"), strings.HasPrefix(k, "swap_"): + v, ok := value.(uint64) + if ok && v == math.MaxUint64 || value == nil { + fields[k] = uint64(0) + } + } + } + } + acc.AddFields("systemd_units", fields, tags) + } + + return nil +} diff --git a/plugins/inputs/systemd_units/systemd_units_notlinux.go b/plugins/inputs/systemd_units/systemd_units_notlinux.go index 99e970e78..05136ec0d 100644 --- a/plugins/inputs/systemd_units/systemd_units_notlinux.go +++ b/plugins/inputs/systemd_units/systemd_units_notlinux.go @@ -1,3 +1,20 @@ //go:build !linux package systemd_units + +import "github.com/influxdata/telegraf" + +type archParams struct{} + +func (s *SystemdUnits) Init() error { + s.Log.Info("Skipping plugin as it is not supported by this platform!") + + // Required to remove linter-warning on unused struct member + _ = s.archParams + + return nil +} + +func (*SystemdUnits) Gather(_ telegraf.Accumulator) error { + return nil +} diff --git a/plugins/inputs/systemd_units/systemd_units_test.go b/plugins/inputs/systemd_units/systemd_units_test.go index a41714a18..f0e3d9bfe 100644 --- a/plugins/inputs/systemd_units/systemd_units_test.go +++ b/plugins/inputs/systemd_units/systemd_units_test.go @@ -1,84 +1,954 @@ +//go:build linux + package systemd_units import ( - "bytes" - "reflect" - "sort" + "context" + "errors" + "fmt" + "math" "strings" "testing" + "time" + sdbus "github.com/coreos/go-systemd/v22/dbus" + "github.com/godbus/dbus/v5" + "github.com/stretchr/testify/require" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/config" + "github.com/influxdata/telegraf/filter" + "github.com/influxdata/telegraf/internal" + "github.com/influxdata/telegraf/metric" "github.com/influxdata/telegraf/testutil" ) -// Global test definitions and structure. -// Tests are located within `subcommand_list_test.go` and -// `subcommand_show_test.go`. - -type TestDef struct { - Name string - Line string - Lines []string - Tags map[string]string - Fields map[string]interface{} - Status int - Err error +type properties struct { + uf *sdbus.UnitFile + utype string + state *sdbus.UnitStatus + ufPreset string + ufState string + properties map[string]interface{} } -func runParserTests(t *testing.T, tests []TestDef, dut *subCommandInfo) { +func TestDefaultPattern(t *testing.T) { + plugin := &SystemdUnits{} + require.NoError(t, plugin.Init()) + require.Equal(t, "*", plugin.Pattern) +} + +func TestListFiles(t *testing.T) { + tests := []struct { + name string + properties map[string]properties + line string + expected []telegraf.Metric + expectedErr string + }{ + { + name: "example loaded active running", + line: "example.service loaded active running example service description", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "loaded", + ActiveState: "active", + SubState: "running", + }, + }, + }, + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "loaded", + "active": "active", + "sub": "running", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 0, + "sub_code": 0, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "example loaded active exited", + line: "example.service loaded active exited example service description", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "loaded", + ActiveState: "active", + SubState: "exited", + }, + }, + }, + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "loaded", + "active": "active", + "sub": "exited", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 0, + "sub_code": 4, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "example loaded failed failed", + line: "example.service loaded failed failed example service description", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "loaded", + ActiveState: "failed", + SubState: "failed", + }, + }, + }, + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "loaded", + "active": "failed", + "sub": "failed", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 3, + "sub_code": 12, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "example not-found inactive dead", + line: "example.service not-found inactive dead example service description", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "not-found", + ActiveState: "inactive", + SubState: "dead", + }, + }, + }, + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "not-found", + "active": "inactive", + "sub": "dead", + }, + map[string]interface{}{ + "load_code": 2, + "active_code": 2, + "sub_code": 1, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "example unknown unknown unknown", + line: "example.service unknown unknown unknown example service description", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "unknown", + ActiveState: "unknown", + SubState: "unknown", + }, + }, + }, + expectedErr: "parsing field 'load' failed, value not in map", + }, + } + for _, tt := range tests { - t.Run(tt.Name, func(t *testing.T) { - acc := new(testutil.Accumulator) - - var line string - if len(tt.Lines) > 0 && len(tt.Line) == 0 { - line = strings.Join(tt.Lines, "\n") - } else if len(tt.Lines) == 0 && len(tt.Line) > 0 { - line = tt.Line + t.Run(tt.name, func(t *testing.T) { + // Run a meta-test for finding regressions compared to metrics + // emitted by previous versions + old, err := oldParseListUnits(tt.line) + if tt.expectedErr == "" { + require.NoError(t, err) } else { - t.Error("property Line and Lines set in test definition") + require.Error(t, err) } + testutil.RequireMetricsEqual(t, old, tt.expected, testutil.IgnoreTime()) - dut.parseResult(acc, bytes.NewBufferString(line)) - err := acc.FirstError() + // Setup plugin. Do NOT call Start() as this would connect to + // the real systemd daemon. + plugin := &SystemdUnits{ + Pattern: "examp*", + Timeout: config.Duration(time.Second), + } + require.NoError(t, plugin.Init()) - if !reflect.DeepEqual(tt.Err, err) { - t.Errorf("%s: expected error '%#v' got '%#v'", tt.Name, tt.Err, err) + // Create a fake client to inject data + client := &fakeClient{ + units: tt.properties, + connected: true, } - if len(acc.Metrics) > 0 { - m := acc.Metrics[0] - if !reflect.DeepEqual(m.Measurement, measurement) { - t.Errorf("%s: expected measurement '%#v' got '%#v'\n", tt.Name, measurement, m.Measurement) - } - if !reflect.DeepEqual(m.Tags, tt.Tags) { - t.Errorf("%s: expected tags\n%#v got\n%#v\n", tt.Name, tt.Tags, m.Tags) - } - if !reflect.DeepEqual(m.Fields, tt.Fields) { - t.Errorf("%s: expected fields\n%#v got\n%#v\n", tt.Name, tt.Fields, m.Fields) - } + client.fixPropertyTypes() + plugin.client = client + defer plugin.Stop() + + // Run gather + var acc testutil.Accumulator + err = acc.GatherError(plugin.Gather) + if tt.expectedErr != "" { + require.ErrorContains(t, err, tt.expectedErr) + return } + require.NoError(t, err) + + // Do the comparison + actual := acc.GetTelegrafMetrics() + testutil.RequireMetricsEqual(t, tt.expected, actual, testutil.IgnoreTime()) }) } } -func runCommandLineTest(t *testing.T, paramsTemplate []string, dut *subCommandInfo, systemdUnits *SystemdUnits) { - params := *dut.getParameters(systemdUnits) - - // Because we sort the params and the template array before comparison - // we have to compare the positional parameters first. - for i, v := range paramsTemplate { - if strings.HasPrefix(v, "--") { - break - } - - if v != params[i] { - t.Errorf("Positional parameter %d is '%s'. Expected '%s'.", i, params[i], v) - } +func TestShow(t *testing.T) { + tests := []struct { + name string + properties map[string]properties + expected []telegraf.Metric + expectedErr string + }{ + { + name: "example loaded active running", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "loaded", + ActiveState: "active", + SubState: "running", + }, + ufPreset: "disabled", + ufState: "enabled", + properties: map[string]interface{}{ + "Id": "example.service", + "StatusErrno": 0, + "NRestarts": 1, + "MemoryCurrent": 1000, + "MemoryPeak": 2000, + "MemorySwapCurrent": 3000, + "MemorySwapPeak": 4000, + "MemoryAvailable": 5000, + "MainPID": 9999, + }, + }, + }, + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "loaded", + "active": "active", + "sub": "running", + "state": "enabled", + "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, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "example loaded active exited", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "loaded", + ActiveState: "active", + SubState: "exited", + }, + ufPreset: "disabled", + ufState: "enabled", + properties: map[string]interface{}{ + "Id": "example.service", + "StatusErrno": 0, + "NRestarts": 0, + }, + }, + }, + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "loaded", + "active": "active", + "sub": "exited", + "state": "enabled", + "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), + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "example loaded failed failed", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "loaded", + ActiveState: "failed", + SubState: "failed", + }, + ufPreset: "disabled", + ufState: "enabled", + properties: map[string]interface{}{ + "Id": "example.service", + "StatusErrno": 10, + "NRestarts": 1, + "MemoryCurrent": 1000, + "MemoryPeak": 2000, + "MemorySwapCurrent": 3000, + "MemorySwapPeak": 4000, + "MemoryAvailable": 5000, + }, + }, + }, + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "loaded", + "active": "failed", + "sub": "failed", + "state": "enabled", + "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), + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "example not-found inactive dead", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "not-found", + ActiveState: "inactive", + SubState: "dead", + }, + ufPreset: "disabled", + ufState: "enabled", + properties: map[string]interface{}{ + "Id": "example.service", + }, + }, + }, + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "not-found", + "active": "inactive", + "sub": "dead", + "state": "enabled", + "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), + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "example unknown unknown unknown", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "unknown", + ActiveState: "unknown", + SubState: "unknown", + }, + ufPreset: "unknown", + ufState: "unknown", + properties: map[string]interface{}{ + "Id": "example.service", + }, + }, + }, + expectedErr: "parsing field 'load' failed, value not in map", + }, + { + name: "example loaded but inactive with unset fields", + properties: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "loaded", + ActiveState: "inactive", + SubState: "dead", + }, + ufPreset: "disabled", + ufState: "disabled", + properties: map[string]interface{}{ + "Id": "example.service", + "StatusErrno": 0, + "NRestarts": 0, + "MemoryCurrent": uint64(math.MaxUint64), + "MemoryPeak": uint64(math.MaxUint64), + "MemorySwapCurrent": uint64(math.MaxUint64), + "MemorySwapPeak": uint64(math.MaxUint64), + "MemoryAvailable": uint64(math.MaxUint64), + }, + }, + }, + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "loaded", + "active": "inactive", + "sub": "dead", + "state": "disabled", + "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), + }, + time.Unix(0, 0), + ), + }, + }, } - // Because the maps do not lead to a stable order of the "--property" - // arguments sort all the command line arguments and compare them. - sort.Strings(params) - sort.Strings(paramsTemplate) - if !reflect.DeepEqual(params, paramsTemplate) { - t.Errorf("Generated list of command line arguments '%#v' do not match expected list command line arguments '%#v'", params, paramsTemplate) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup plugin. Do NOT call Start() as this would connect to + // the real systemd daemon. + plugin := &SystemdUnits{ + Pattern: "examp*", + Details: true, + Timeout: config.Duration(time.Second), + } + require.NoError(t, plugin.Init()) + + // Create a fake client to inject data + client := &fakeClient{ + units: tt.properties, + connected: true, + } + client.fixPropertyTypes() + plugin.client = client + defer plugin.Stop() + + // Run gather + var acc testutil.Accumulator + err := acc.GatherError(plugin.Gather) + if tt.expectedErr != "" { + require.ErrorContains(t, err, tt.expectedErr) + return + } + require.NoError(t, err) + + // Do the comparison + actual := acc.GetTelegrafMetrics() + testutil.RequireMetricsEqual(t, tt.expected, actual, testutil.IgnoreTime()) + }) } } + +func TestMultiInstance(t *testing.T) { + tests := []struct { + name string + pattern string + expected []telegraf.Metric + }{ + { + name: "multiple without concrete instance", + pattern: "examp* user@*", + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "example.service", + "load": "loaded", + "active": "active", + "sub": "running", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 0, + "sub_code": 0, + }, + time.Unix(0, 0), + ), + metric.New( + "systemd_units", + map[string]string{ + "name": "user@1000.service", + "load": "loaded", + "active": "active", + "sub": "running", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 0, + "sub_code": 0, + }, + time.Unix(0, 0), + ), + metric.New( + "systemd_units", + map[string]string{ + "name": "user@1001.service", + "load": "loaded", + "active": "active", + "sub": "exited", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 0, + "sub_code": 4, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "multiple without instance prefix", + pattern: "user@1*", + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "user@1000.service", + "load": "loaded", + "active": "active", + "sub": "running", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 0, + "sub_code": 0, + }, + time.Unix(0, 0), + ), + metric.New( + "systemd_units", + map[string]string{ + "name": "user@1001.service", + "load": "loaded", + "active": "active", + "sub": "exited", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 0, + "sub_code": 4, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "multiple with concrete instance", + pattern: "user@1001.service", + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "user@1001.service", + "load": "loaded", + "active": "active", + "sub": "exited", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 0, + "sub_code": 4, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "static but loaded instance", + pattern: "shadow*", + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "shadow.service", + "load": "loaded", + "active": "inactive", + "sub": "dead", + }, + map[string]interface{}{ + "load_code": 0, + "active_code": 2, + "sub_code": 1, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "static but not loaded instance", + pattern: "cups*", + expected: []telegraf.Metric{ + metric.New( + "systemd_units", + map[string]string{ + "name": "cups-lpd@.service", + "load": "stub", + "active": "inactive", + "sub": "dead", + }, + map[string]interface{}{ + "load_code": 1, + "active_code": 2, + "sub_code": 1, + }, + time.Unix(0, 0), + ), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup plugin. Do NOT call Start() as this would connect to + // the real systemd daemon. + plugin := &SystemdUnits{ + Pattern: tt.pattern, + Timeout: config.Duration(time.Second), + } + require.NoError(t, plugin.Init()) + + // Create a fake client to inject data + client := &fakeClient{ + units: map[string]properties{ + "example.service": { + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "example.service", + LoadState: "loaded", + ActiveState: "active", + SubState: "running", + }, + }, + "user-runtime-dir@1000.service": { + uf: &sdbus.UnitFile{ + Path: "user-runtime-dir@.service", + Type: "static", + }, + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "user-runtime-dir@1000.service", + LoadState: "loaded", + ActiveState: "active", + SubState: "exited", + }, + }, + "user@1000.service": { + uf: &sdbus.UnitFile{ + Path: "user@.service", + Type: "static", + }, + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "user@1000.service", + LoadState: "loaded", + ActiveState: "active", + SubState: "running", + }, + }, + "user@1001.service": { + uf: &sdbus.UnitFile{ + Path: "user@.service", + Type: "static", + }, + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "user@1001.service", + LoadState: "loaded", + ActiveState: "active", + SubState: "exited", + }, + }, + "shadow.service": { + uf: &sdbus.UnitFile{ + Path: "shadow.service", + Type: "static", + }, + utype: "Service", + state: &sdbus.UnitStatus{ + Name: "shadow.service", + LoadState: "loaded", + ActiveState: "inactive", + SubState: "dead", + }, + }, + "cups-lpd@.service": { + uf: &sdbus.UnitFile{ + Path: "cups-lpd@.service", + Type: "static", + }, + utype: "Service", + }, + }, + connected: true, + } + client.fixPropertyTypes() + plugin.client = client + defer plugin.Stop() + + // Run gather + var acc testutil.Accumulator + require.NoError(t, acc.GatherError(plugin.Gather)) + + // Do the comparison + actual := acc.GetTelegrafMetrics() + testutil.RequireMetricsEqual(t, tt.expected, actual, testutil.IgnoreTime(), testutil.SortMetrics()) + }) + } +} + +// Fake client implementation +type fakeClient struct { + units map[string]properties + connected bool +} + +func (c *fakeClient) fixPropertyTypes() { + for unit, u := range c.units { + for k, value := range u.properties { + if strings.HasPrefix(k, "Memory") { + u.properties[k], _ = internal.ToUint64(value) + } + } + c.units[unit] = u + } +} + +func (c *fakeClient) Connected() bool { + return !c.connected +} + +func (c *fakeClient) Close() { + c.connected = false +} + +func (c *fakeClient) ListUnitFilesByPatternsContext(_ context.Context, _, pattern []string) ([]sdbus.UnitFile, error) { + f := filter.MustCompile(pattern) + + var files []sdbus.UnitFile + seen := make(map[string]bool) + for name, props := range c.units { + var uf sdbus.UnitFile + if props.uf != nil && f.Match(props.uf.Path) { + uf = sdbus.UnitFile{ + Path: "/usr/lib/systemd/system/" + props.uf.Path, + Type: props.uf.Type, + } + } else if props.uf == nil && f.Match(name) { + uf = sdbus.UnitFile{ + Path: "/usr/lib/systemd/system/" + name, + Type: "enabled", + } + } else { + continue + } + + if !seen[uf.Path] { + files = append(files, uf) + } + seen[uf.Path] = true + } + + return files, nil +} + +func (c *fakeClient) ListUnitsByNamesContext(_ context.Context, units []string) ([]sdbus.UnitStatus, error) { + var states []sdbus.UnitStatus + for name, u := range c.units { + for _, requestedName := range units { + if name == requestedName && u.state != nil { + states = append(states, *u.state) + break + } + } + } + + return states, nil +} + +func (c *fakeClient) GetUnitTypePropertiesContext(_ context.Context, unit, unitType string) (map[string]interface{}, error) { + u, found := c.units[unit] + if !found { + return nil, nil + } + if u.utype != unitType { + return nil, fmt.Errorf("Unknown interface 'org.freedesktop.systemd1.%s", unitType) + } + return u.properties, nil +} + +func (c *fakeClient) GetUnitPropertyContext(_ context.Context, unit, propertyName string) (*sdbus.Property, 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") +} + +func (c *fakeClient) ListUnitsContext(_ context.Context) ([]sdbus.UnitStatus, error) { + units := make([]sdbus.UnitStatus, 0, len(c.units)) + for _, u := range c.units { + if u.state != nil { + units = append(units, *u.state) + } + } + return units, nil +} + +// Slightly adapted version of 'parseListUnits()' function of 'subcommand_list.go' +func oldParseListUnits(line string) ([]telegraf.Metric, error) { + data := strings.Fields(line) + if len(data) < 4 { + return nil, fmt.Errorf("parsing line failed (expected at least 4 fields): %s", line) + } + name := data[0] + load := data[1] + active := data[2] + sub := data[3] + tags := map[string]string{ + "name": name, + "load": load, + "active": active, + "sub": sub, + } + + var ( + loadCode int + activeCode int + subCode int + ok bool + ) + if loadCode, ok = loadMap[load]; !ok { + return nil, fmt.Errorf("parsing field 'load' failed, value not in map: %s", load) + } + if activeCode, ok = activeMap[active]; !ok { + return nil, fmt.Errorf("parsing field field 'active' failed, value not in map: %s", active) + } + if subCode, ok = subMap[sub]; !ok { + return nil, fmt.Errorf("parsing field field 'sub' failed, value not in map: %s", sub) + } + fields := map[string]interface{}{ + "load_code": loadCode, + "active_code": activeCode, + "sub_code": subCode, + } + + return []telegraf.Metric{metric.New("systemd_units", tags, fields, time.Now())}, nil +}