feat(inputs.systemd_units): Introduce show subcommand for additional data (#14539)

This commit is contained in:
FlashSystems 2024-01-17 14:56:12 +01:00 committed by GitHub
parent a2a067a699
commit ff4f0e41bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 769 additions and 175 deletions

View File

@ -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 <!-- @/docs/includes/plugin_config.md -->
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
```

View File

@ -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 = ""

View File

@ -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 &params
}
func initSubcommandListUnits() *subCommandInfo {
return &subCommandInfo{
getParameters: getListUnitsParameters,
parseResult: parseListUnits,
}
}

View File

@ -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)
}

View File

@ -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 &params
}
func initSubcommandShow() *subCommandInfo {
return &subCommandInfo{
getParameters: getShowParameters,
parseResult: parseShow,
}
}

View File

@ -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)
}

View File

@ -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,
}
})
}

View File

@ -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)
}
}