diff --git a/plugins/inputs/systemd_units/README.md b/plugins/inputs/systemd_units/README.md index 710a0e03a..16f9b90f3 100644 --- a/plugins/inputs/systemd_units/README.md +++ b/plugins/inputs/systemd_units/README.md @@ -1,18 +1,46 @@ # systemd Units Input Plugin -The systemd_units plugin gathers systemd unit status on Linux. It relies on -`systemctl list-units [PATTERN] --all --plain --type=service` to collect data on +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. -The results are tagged with the unit name and provide enumerated fields for -loaded, active and running fields, indicating the unit health. - 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. + ## Global configuration options In addition to the plugin-specific configuration settings, plugins support @@ -29,22 +57,30 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. [[inputs.systemd_units]] ## Set timeout for systemctl execution # timeout = "1s" - # + + ## 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 ": # 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 = "" ## pattern = "telegraf* influxdb*" ## pattern = "a*" + # pattern = "" ``` ## Metrics +These metrics are available in both modes: + - systemd_units: - tags: - name (string, unit name) @@ -56,6 +92,22 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. - active_code (int, see below) - sub_code (int, see below) +The following additional metrics are available whe using `subcommand = "show"`: + +- systemd_units: + - tags: + - uf_state (string, unit file state) + - uf_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) + ### Load enumeration of [unit_load_state_table][1] @@ -161,8 +213,18 @@ were removed, tables are hex aligned to keep some space for future values ## Example Output +### Output in `list-units` 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 systemd_units,host=host1.example.com,name=networking.service,load=loaded,active=failed,sub=failed load_code=0i,active_code=3i,sub_code=12i 1533730725000000000 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 + +```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=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 +``` diff --git a/plugins/inputs/systemd_units/sample.conf b/plugins/inputs/systemd_units/sample.conf index 421be8cff..4f54766bd 100644 --- a/plugins/inputs/systemd_units/sample.conf +++ b/plugins/inputs/systemd_units/sample.conf @@ -2,15 +2,21 @@ [[inputs.systemd_units]] ## Set timeout for systemctl execution # timeout = "1s" - # + + ## 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 ": # 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 = "" ## pattern = "telegraf* influxdb*" ## pattern = "a*" + # pattern = "" diff --git a/plugins/inputs/systemd_units/subcommand_list.go b/plugins/inputs/systemd_units/subcommand_list.go new file mode 100644 index 000000000..004671175 --- /dev/null +++ b/plugins/inputs/systemd_units/subcommand_list.go @@ -0,0 +1,86 @@ +//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 new file mode 100644 index 000000000..36607452d --- /dev/null +++ b/plugins/inputs/systemd_units/subcommand_list_test.go @@ -0,0 +1,116 @@ +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 new file mode 100644 index 000000000..7b8dc91cd --- /dev/null +++ b/plugins/inputs/systemd_units/subcommand_show.go @@ -0,0 +1,147 @@ +//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, fmt.Sprintf("--property=%s", 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, fmt.Sprintf("--property=%s", 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 new file mode 100644 index 000000000..c26155323 --- /dev/null +++ b/plugins/inputs/systemd_units/subcommand_show_test.go @@ -0,0 +1,229 @@ +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 5ecd26d7b..2f501369e 100644 --- a/plugins/inputs/systemd_units/systemd_units.go +++ b/plugins/inputs/systemd_units/systemd_units.go @@ -2,7 +2,6 @@ package systemd_units import ( - "bufio" "bytes" _ "embed" "fmt" @@ -19,21 +18,13 @@ import ( //go:embed sample.conf var sampleConfig string -// SystemdUnits is a telegraf plugin to gather systemd unit status -type SystemdUnits struct { - Timeout config.Duration - UnitType string `toml:"unittype"` - Pattern string `toml:"pattern"` - systemctl systemctl -} - -type systemctl func(timeout config.Duration, unitType string, pattern string) (*bytes.Buffer, error) - 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, @@ -128,109 +119,82 @@ var subMap = map[string]int{ "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 +} + +type getParametersFunc func(*SystemdUnits) *[]string +type parseResultFunc func(telegraf.Accumulator, *bytes.Buffer) + +type subCommandInfo struct { + getParameters getParametersFunc + parseResult parseResultFunc +} + var ( - defaultTimeout = config.Duration(time.Second) - defaultUnitType = "service" - defaultPattern = "" + defaultSubCommand = "list-units" + defaultTimeout = config.Duration(time.Second) + defaultUnitType = "service" + defaultPattern = "" ) func (*SystemdUnits) SampleConfig() string { return sampleConfig } -// Gather parses systemctl outputs and adds counters to the Accumulator -func (s *SystemdUnits) Gather(acc telegraf.Accumulator) error { - out, err := s.systemctl(s.Timeout, s.UnitType, s.Pattern) - if err != nil { - return err +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) } - scanner := bufio.NewScanner(out) - for scanner.Scan() { - line := scanner.Text() - - data := strings.Fields(line) - if len(data) < 4 { - acc.AddError(fmt.Errorf("Error parsing line (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("Error parsing field 'load', value not in map: %s", load)) - continue - } - if activeCode, ok = activeMap[active]; !ok { - acc.AddError(fmt.Errorf("Error parsing field 'active', value not in map: %s", active)) - continue - } - if subCode, ok = subMap[sub]; !ok { - acc.AddError(fmt.Errorf("Error parsing field 'sub', 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) - } + // 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 setSystemctl(timeout config.Duration, unitType string, pattern string) (*bytes.Buffer, error) { +func (s *SystemdUnits) Gather(acc telegraf.Accumulator) error { // is systemctl available ? systemctlPath, err := exec.LookPath("systemctl") if err != nil { - return nil, err + return err } - // build parameters for systemctl call - params := []string{"list-units"} - // create patterns parameters if provided in config - if pattern != "" { - psplit := strings.SplitN(pattern, " ", -1) - params = append(params, psplit...) - } - params = append(params, - "--all", "--plain", - // add type as configured in config - "--type="+unitType, - "--no-legend", - ) - cmd := exec.Command(systemctlPath, params...) + + cmd := exec.Command(systemctlPath, *s.commandParams...) var out bytes.Buffer cmd.Stdout = &out - err = internal.RunTimeout(cmd, time.Duration(timeout)) + err = internal.RunTimeout(cmd, time.Duration(s.Timeout)) if err != nil { - return &out, fmt.Errorf("error running systemctl %q: %w", strings.Join(params, " "), err) + return fmt.Errorf("error running systemctl %q: %w", strings.Join(*s.commandParams, " "), err) } - return &out, nil + + s.resultParser(acc, &out) + + return nil } func init() { inputs.Add("systemd_units", func() telegraf.Input { return &SystemdUnits{ - systemctl: setSystemctl, - Timeout: defaultTimeout, - UnitType: defaultUnitType, - Pattern: defaultPattern, + Timeout: defaultTimeout, + UnitType: defaultUnitType, + Pattern: defaultPattern, + SubCommand: defaultSubCommand, } }) } diff --git a/plugins/inputs/systemd_units/systemd_units_test.go b/plugins/inputs/systemd_units/systemd_units_test.go index 05070c6ff..a41714a18 100644 --- a/plugins/inputs/systemd_units/systemd_units_test.go +++ b/plugins/inputs/systemd_units/systemd_units_test.go @@ -2,99 +2,83 @@ package systemd_units import ( "bytes" - "fmt" "reflect" + "sort" + "strings" "testing" - "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/testutil" ) -func TestSystemdUnits(t *testing.T) { - tests := []struct { - name string - line string - tags map[string]string - fields map[string]interface{} - status int - err error - }{ - { - 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("Error parsing field 'load', value not in map: %s", "unknown"), - }, - { - name: "example too few fields", - line: "example.service loaded fai", - err: fmt.Errorf("Error parsing line (expected at least 4 fields): %s", "example.service loaded fai"), - }, - } +// 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 +} + +func runParserTests(t *testing.T, tests []TestDef, dut *subCommandInfo) { for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - systemdUnits := &SystemdUnits{ - systemctl: func(timeout config.Duration, unitType string, pattern string) (*bytes.Buffer, error) { - return bytes.NewBufferString(tt.line), nil - }, - } + t.Run(tt.Name, func(t *testing.T) { acc := new(testutil.Accumulator) - err := acc.GatherError(systemdUnits.Gather) - if !reflect.DeepEqual(tt.err, err) { - t.Errorf("%s: expected error '%#v' got '%#v'", tt.name, tt.err, err) + + 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 + } else { + t.Error("property Line and Lines set in test definition") + } + + dut.parseResult(acc, bytes.NewBufferString(line)) + err := acc.FirstError() + + if !reflect.DeepEqual(tt.Err, err) { + t.Errorf("%s: expected error '%#v' got '%#v'", tt.Name, tt.Err, err) } 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) + 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.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) + if !reflect.DeepEqual(m.Fields, tt.Fields) { + t.Errorf("%s: expected fields\n%#v got\n%#v\n", tt.Name, tt.Fields, m.Fields) } } }) } } + +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) + } + } + // 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) + } +}