feat(inputs.systemd_units): Introduce show subcommand for additional data (#14539)
This commit is contained in:
parent
a2a067a699
commit
ff4f0e41bd
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue