feat(inputs.systemd_units): Allow to query unloaded/disabled units (#14814)

Co-authored-by: Joshua Powers <powersj@fastmail.com>
This commit is contained in:
Sven Rebhan 2024-03-05 16:34:36 +01:00 committed by GitHub
parent 33ec0a9dec
commit ebea0b289a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1381 additions and 901 deletions

2
go.mod
View File

@ -323,7 +323,7 @@ require (
github.com/goburrow/serial v0.1.1-0.20211022031912-bfb69110f8dd // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/godbus/dbus/v5 v5.1.0
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect

View File

@ -1,45 +1,9 @@
# systemd Units Input Plugin
# Systemd-Units Input Plugin
The systemd_units plugin gathers systemd unit status on Linux. It uses the
`systemctl list-units` or the `systemctl show` command to collect data on
service status.
This plugin gathers the status of systemd-units on Linux, using systemd's DBus
interface.
This plugin is related to the [win_services module](../win_services/README.md),
which fulfills the same purpose on windows.
This plugin supports two modes of operation:
## Using `systemctl list-units`
This is the default mode. It uses the output of
`systemctl list-units [PATTERN] --all --plain --type=service` to collect data on
service status.
This mode will not supply as much information as `systemctl show`, but will be
compatible with almost every version of systemd.
The results are tagged with the unit name and provide enumerated fields for
loaded, active and running fields, indicating the unit's health.
In addition to services, this plugin can gather other unit types as well,
see `systemctl list-units --all --type help` for possible options.
## Using `systemctl show`
This mode can be enabled by setting the configuration option `subcommand` to
`show`. The plugin will use
`systemctl show [PATTERN] --all --type=service --property=...` to collect data
on service status.
This mode will yield more data on the service status. See the metrics chapter
for a list of properties.
The results are tagged with the unit name, unit status, preset status and
provide enumerated fields for loaded, active and running fields, as well as the
restart count and memory usage of the unit.
In addition to services, this plugin can gather other unit types as well,
see `systemctl show --all --type help` for possible options.
Please note: At least systemd v230 is required!
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
@ -53,30 +17,42 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## Configuration
```toml @sample.conf
# Gather systemd units state
# Gather information about systemd-unit states
# This plugin ONLY supports Linux
[[inputs.systemd_units]]
## Set timeout for systemctl execution
# timeout = "1s"
## Pattern of units to collect
## A space-separated list of unit-patterns including wildcards determining
## the units to collect.
## ex: pattern = "telegraf* influxdb* user@*"
# pattern = "*"
## Select the systemctl subcommand to use to gather information.
## Using `list-units` is the option with the broadest compatibility.
## Using `show` will get more information but may fail to list all units on
## some systems.
# subcommand = "list-units"
## Filter for a specific unit type, default is "service", other possible
## values are "socket", "target", "device", "mount", "automount", "swap",
## "timer", "path", "slice" and "scope ":
## Filter for a specific unit type
## Available settings are: service, socket, target, device, mount,
## automount, swap, timer, path, slice and scope
# unittype = "service"
## Filter for a specific pattern, default is "" (i.e. all), other possible
## values are valid pattern for systemctl, e.g. "a*" for all units with
## names starting with "a"
## pattern = "telegraf* influxdb*"
## pattern = "a*"
# pattern = ""
## Collect detailed information for the units
# details = false
## Timeout for state-collection
# timeout = "1s"
```
This plugin supports two modes of operation:
### Non-detailed mode
This is the default mode, collecting data on the unit's status only without
further details on the unit.
### Detailed mode
This mode can be enabled by setting the configuration option `details` to
`true`. In this mode the plugin collects all information of the non-detailed
mode but provides additional unit information such as memory usage,
restart-counts, PID, etc. See the [metrics section](#metrics) below for a list
of all properties collected.
## Metrics
These metrics are available in both modes:
@ -92,25 +68,25 @@ These metrics are available in both modes:
- active_code (int, see below)
- sub_code (int, see below)
The following additional metrics are available whe using `subcommand = "show"`:
The following *additional* metrics are available with `details = true`:
- systemd_units:
- tags:
- uf_state (string, unit file state)
- uf_preset (string, unit file preset state)
- state (string, unit file state)
- preset (string, unit file preset state)
- fields:
- status_errno (int, last error)
- restarts (int, number of restarts)
- mem_current (int, current memory usage)
- mem_peak (int, peak memory usage)
- swap_current (int, current swap usage)
- swap_peak (int, peak swap usage)
- mem_avail (int, available memory for this unit)
- pid (int, pid of the main process)
- mem_current (uint, current memory usage)
- mem_peak (uint, peak memory usage)
- swap_current (uint, current swap usage)
- swap_peak (uint, peak swap usage)
- mem_avail (uint, available memory for this unit)
### Load
enumeration of [unit_load_state_table][1]
Enumeration of [unit_load_state_table][1]
| Value | Meaning | Description |
| ----- | ------- | ----------- |
@ -126,7 +102,7 @@ enumeration of [unit_load_state_table][1]
### Active
enumeration of [unit_active_state_table][2]
Enumeration of [unit_active_state_table][2]
| Value | Meaning | Description |
| ----- | ------- | ----------- |
@ -213,7 +189,7 @@ were removed, tables are hex aligned to keep some space for future values
## Example Output
### Output in `list-units` mode
### Output in non-detailed mode
```text
systemd_units,host=host1.example.com,name=dbus.service,load=loaded,active=active,sub=running load_code=0i,active_code=0i,sub_code=0i 1533730725000000000
@ -221,10 +197,10 @@ systemd_units,host=host1.example.com,name=networking.service,load=loaded,active=
systemd_units,host=host1.example.com,name=ssh.service,load=loaded,active=active,sub=running load_code=0i,active_code=0i,sub_code=0i 1533730725000000000
```
### Output in `show` mode
### Output in detailed mode
```text
systemd_units,active=active,host=host1.example.com,load=loaded,name=dbus.service,sub=running,uf_preset=disabled,uf_state=static active_code=0i,load_code=0i,mem_avail=6470856704i,mem_current=2691072i,mem_peak=3895296i,pid=481i,restarts=0i,status_errno=0i,sub_code=0i,swap_current=794624i,swap_peak=884736i 1533730725000000000
systemd_units,active=active,host=host1.example.com,load=loaded,name=dbus.service,sub=running,preset=disabled,state=static active_code=0i,load_code=0i,mem_avail=6470856704i,mem_current=2691072i,mem_peak=3895296i,pid=481i,restarts=0i,status_errno=0i,sub_code=0i,swap_current=794624i,swap_peak=884736i 1533730725000000000
systemd_units,active=inactive,host=host1.example.com,load=not-found,name=networking.service,sub=dead active_code=2i,load_code=2i,pid=0i,restarts=0i,status_errno=0i,sub_code=1i 1533730725000000000
systemd_units,active=active,host=host1.example.com,load=loaded,name=pcscd.service,sub=running,uf_preset=disabled,uf_state=indirect active_code=0i,load_code=0i,mem_avail=6370541568i,mem_current=512000i,mem_peak=4399104i,pid=1673i,restarts=0i,status_errno=0i,sub_code=0i,swap_current=3149824i,swap_peak=3149824i 1533730725000000000
systemd_units,active=active,host=host1.example.com,load=loaded,name=pcscd.service,sub=running,preset=disabled,state=indirect active_code=0i,load_code=0i,mem_avail=6370541568i,mem_current=512000i,mem_peak=4399104i,pid=1673i,restarts=0i,status_errno=0i,sub_code=0i,swap_current=3149824i,swap_peak=3149824i 1533730725000000000
```

View File

@ -1,22 +1,19 @@
# Gather systemd units state
# Gather information about systemd-unit states
# This plugin ONLY supports Linux
[[inputs.systemd_units]]
## Set timeout for systemctl execution
# timeout = "1s"
## Pattern of units to collect
## A space-separated list of unit-patterns including wildcards determining
## the units to collect.
## ex: pattern = "telegraf* influxdb* user@*"
# pattern = "*"
## Select the systemctl subcommand to use to gather information.
## Using `list-units` is the option with the broadest compatibility.
## Using `show` will get more information but may fail to list all units on
## some systems.
# subcommand = "list-units"
## Filter for a specific unit type, default is "service", other possible
## values are "socket", "target", "device", "mount", "automount", "swap",
## "timer", "path", "slice" and "scope ":
## Filter for a specific unit type
## Available settings are: service, socket, target, device, mount,
## automount, swap, timer, path, slice and scope
# unittype = "service"
## Filter for a specific pattern, default is "" (i.e. all), other possible
## values are valid pattern for systemctl, e.g. "a*" for all units with
## names starting with "a"
## pattern = "telegraf* influxdb*"
## pattern = "a*"
# pattern = ""
## Collect detailed information for the units
# details = false
## Timeout for state-collection
# timeout = "1s"

View File

@ -1,86 +0,0 @@
//go:generate ../../../tools/readme_config_includer/generator
package systemd_units
import (
"bufio"
"bytes"
"fmt"
"strings"
"github.com/influxdata/telegraf"
)
// Gather parses systemctl outputs and adds counters to the Accumulator
func parseListUnits(acc telegraf.Accumulator, buffer *bytes.Buffer) {
scanner := bufio.NewScanner(buffer)
for scanner.Scan() {
line := scanner.Text()
data := strings.Fields(line)
if len(data) < 4 {
acc.AddError(fmt.Errorf("parsing line failed (expected at least 4 fields): %s", line))
continue
}
name := data[0]
load := data[1]
active := data[2]
sub := data[3]
tags := map[string]string{
"name": name,
"load": load,
"active": active,
"sub": sub,
}
var (
loadCode int
activeCode int
subCode int
ok bool
)
if loadCode, ok = loadMap[load]; !ok {
acc.AddError(fmt.Errorf("parsing field 'load' failed, value not in map: %s", load))
continue
}
if activeCode, ok = activeMap[active]; !ok {
acc.AddError(fmt.Errorf("parsing field field 'active' failed, value not in map: %s", active))
continue
}
if subCode, ok = subMap[sub]; !ok {
acc.AddError(fmt.Errorf("parsing field field 'sub' failed, value not in map: %s", sub))
continue
}
fields := map[string]interface{}{
"load_code": loadCode,
"active_code": activeCode,
"sub_code": subCode,
}
acc.AddFields(measurement, fields, tags)
}
}
func getListUnitsParameters(s *SystemdUnits) *[]string {
// build parameters for systemctl call
params := []string{"list-units"}
// create patterns parameters if provided in config
if s.Pattern != "" {
psplit := strings.SplitN(s.Pattern, " ", -1)
params = append(params, psplit...)
}
params = append(params,
"--all",
"--plain",
"--type="+s.UnitType,
"--no-legend",
)
return &params
}
func initSubcommandListUnits() *subCommandInfo {
return &subCommandInfo{
getParameters: getListUnitsParameters,
parseResult: parseListUnits,
}
}

View File

@ -1,116 +0,0 @@
package systemd_units
import (
"fmt"
"testing"
)
func TestSubcommandList(t *testing.T) {
tests := []TestDef{
{
Name: "example loaded active running",
Line: "example.service loaded active running example service description",
Tags: map[string]string{
"name": "example.service",
"load": "loaded",
"active": "active",
"sub": "running",
},
Fields: map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 0,
},
},
{
Name: "example loaded active exited",
Line: "example.service loaded active exited example service description",
Tags: map[string]string{
"name": "example.service",
"load": "loaded",
"active": "active",
"sub": "exited",
},
Fields: map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 4,
},
},
{
Name: "example loaded failed failed",
Line: "example.service loaded failed failed example service description",
Tags: map[string]string{"name": "example.service", "load": "loaded", "active": "failed", "sub": "failed"},
Fields: map[string]interface{}{
"load_code": 0,
"active_code": 3,
"sub_code": 12,
},
},
{
Name: "example not-found inactive dead",
Line: "example.service not-found inactive dead example service description",
Tags: map[string]string{
"name": "example.service",
"load": "not-found",
"active": "inactive",
"sub": "dead",
},
Fields: map[string]interface{}{
"load_code": 2,
"active_code": 2,
"sub_code": 1,
},
},
{
Name: "example unknown unknown unknown",
Line: "example.service unknown unknown unknown example service description",
Err: fmt.Errorf("parsing field 'load' failed, value not in map: %s", "unknown"),
},
{
Name: "example too few fields",
Line: "example.service loaded fai",
Err: fmt.Errorf("parsing line failed (expected at least 4 fields): %s", "example.service loaded fai"),
},
}
dut := initSubcommandListUnits()
runParserTests(t, tests, dut)
}
func TestCommandlineList(t *testing.T) {
// Test using the default pattern (no pattern)
paramsTemplate := []string{
"list-units",
"--all",
"--plain",
"--no-legend",
"--type=service",
}
dut := initSubcommandListUnits()
systemdUnits := SystemdUnits{
UnitType: "service",
}
runCommandLineTest(t, paramsTemplate, dut, &systemdUnits)
// Test using a more complex pattern
paramsTemplate = []string{
"list-units",
"unita.service",
"*.timer",
"--all",
"--plain",
"--no-legend",
"--type=service",
}
systemdUnits = SystemdUnits{
UnitType: "service",
Pattern: "unita.service *.timer",
}
runCommandLineTest(t, paramsTemplate, dut, &systemdUnits)
}

View File

@ -1,147 +0,0 @@
//go:generate ../../../tools/readme_config_includer/generator
package systemd_units
import (
"bufio"
"bytes"
"fmt"
"strconv"
"strings"
"github.com/influxdata/telegraf"
)
type valueDef struct {
valueName string
valueMap *map[string]int
}
// The following two maps configure the mapping of systemd properties to
// tags or values. The properties are automatically requested from
// `systemctl show`.
// If a vlaue has no valueMap, `atoi` is called on the value to convert it to
// an integer.
var tagMap = map[string]string{
"Id": "name",
"LoadState": "load",
"ActiveState": "active",
"SubState": "sub",
"UnitFileState": "uf_state",
"UnitFilePreset": "uf_preset",
}
var valueMap = map[string]valueDef{
"LoadState": {valueName: "load_code", valueMap: &loadMap},
"ActiveState": {valueName: "active_code", valueMap: &activeMap},
"SubState": {valueName: "sub_code", valueMap: &subMap},
"StatusErrno": {valueName: "status_errno", valueMap: nil},
"NRestarts": {valueName: "restarts", valueMap: nil},
"MemoryCurrent": {valueName: "mem_current", valueMap: nil},
"MemoryPeak": {valueName: "mem_peak", valueMap: nil},
"MemorySwapCurrent": {valueName: "swap_current", valueMap: nil},
"MemorySwapPeak": {valueName: "swap_peak", valueMap: nil},
"MemoryAvailable": {valueName: "mem_avail", valueMap: nil},
"MainPID": {valueName: "pid", valueMap: nil},
}
// Gather parses systemctl outputs and adds counters to the Accumulator
func parseShow(acc telegraf.Accumulator, buffer *bytes.Buffer) {
scanner := bufio.NewScanner(buffer)
tags := make(map[string]string)
fields := make(map[string]interface{})
for scanner.Scan() {
line := scanner.Text()
// An empty line signals the start of the next unit
if len(line) == 0 {
// We need at least a "name" field. This prevents values from the
// global information block (enabled by the --all switch) to be
// shown as a unit.
if _, ok := tags["name"]; ok {
acc.AddFields(measurement, fields, tags)
}
tags = make(map[string]string)
fields = make(map[string]interface{})
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
acc.AddError(fmt.Errorf("error parsing line (expected key=value): %s", line))
continue
}
// Map the tags
if tagName, isTag := tagMap[key]; isTag {
tags[tagName] = value
}
// Map the values
if valueDef, isValue := valueMap[key]; isValue {
// If a value map is set use it. If not, just try to convert the
// value into an integer.
if valueDef.valueMap != nil {
code, ok := (*valueDef.valueMap)[value]
if !ok {
acc.AddError(fmt.Errorf("error parsing field '%s', value '%s' not in map", key, value))
continue
}
fields[valueDef.valueName] = code
} else {
if value != "[not set]" {
intVal, err := strconv.Atoi(value)
if err != nil {
acc.AddError(fmt.Errorf("error '%w' parsing field '%s'. Not an integer value", err, key))
continue
}
fields[valueDef.valueName] = intVal
}
}
}
}
// Add the last unit because the output does not contain a newline for this
if _, ok := tags["name"]; ok {
acc.AddFields(measurement, fields, tags)
}
}
func getShowParameters(s *SystemdUnits) *[]string {
// build parameters for systemctl call
params := []string{"show"}
// create patterns parameters if provided in config
if s.Pattern == "" {
params = append(params, "*")
} else {
psplit := strings.SplitN(s.Pattern, " ", -1)
params = append(params, psplit...)
}
params = append(params, "--all", "--type="+s.UnitType)
// add the fields we're interested in to the command line
for property := range tagMap {
params = append(params, "--property="+property)
}
for property := range valueMap {
// If a property exists within the tagMap it was already added. Do not add it again to
// keep the command line short.
if _, exists := tagMap[property]; !exists {
params = append(params, "--property="+property)
}
}
return &params
}
func initSubcommandShow() *subCommandInfo {
return &subCommandInfo{
getParameters: getShowParameters,
parseResult: parseShow,
}
}

View File

@ -1,229 +0,0 @@
package systemd_units
import (
"fmt"
"testing"
)
func TestSubcommandShow(t *testing.T) {
tests := []TestDef{
{
Name: "example loaded active running",
Lines: []string{
"Id=example.service",
"LoadState=loaded",
"ActiveState=active",
"SubState=running",
"UnitFileState=enabled",
"UnitFilePreset=disabled",
"StatusErrno=0",
"NRestarts=1",
"MemoryCurrent=1000",
"MemoryPeak=2000",
"MemorySwapCurrent=3000",
"MemorySwapPeak=4000",
"MemoryAvailable=5000",
"MainPID=9999",
},
Tags: map[string]string{
"name": "example.service",
"load": "loaded",
"active": "active",
"sub": "running",
"uf_state": "enabled",
"uf_preset": "disabled",
},
Fields: map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 0,
"status_errno": 0,
"restarts": 1,
"mem_current": 1000,
"mem_peak": 2000,
"swap_current": 3000,
"swap_peak": 4000,
"mem_avail": 5000,
"pid": 9999,
},
},
{
Name: "example loaded active exited",
Lines: []string{
"Id=example.service",
"LoadState=loaded",
"ActiveState=active",
"SubState=exited",
"UnitFileState=enabled",
"UnitFilePreset=disabled",
"StatusErrno=0",
"NRestarts=0",
},
Tags: map[string]string{
"name": "example.service",
"load": "loaded",
"active": "active",
"sub": "exited",
"uf_state": "enabled",
"uf_preset": "disabled",
},
Fields: map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 4,
"status_errno": 0,
"restarts": 0,
},
},
{
Name: "example loaded failed failed",
Lines: []string{
"Id=example.service",
"LoadState=loaded",
"ActiveState=failed",
"SubState=failed",
"UnitFileState=enabled",
"UnitFilePreset=disabled",
"StatusErrno=10",
"NRestarts=1",
"MemoryCurrent=1000",
"MemoryPeak=2000",
"MemorySwapCurrent=3000",
"MemorySwapPeak=4000",
"MemoryAvailable=5000",
},
Tags: map[string]string{
"name": "example.service",
"load": "loaded",
"active": "failed",
"sub": "failed",
"uf_state": "enabled",
"uf_preset": "disabled",
},
Fields: map[string]interface{}{
"load_code": 0,
"active_code": 3,
"sub_code": 12,
"status_errno": 10,
"restarts": 1,
"mem_current": 1000,
"mem_peak": 2000,
"swap_current": 3000,
"swap_peak": 4000,
"mem_avail": 5000,
},
},
{
Name: "example not-found inactive dead",
Lines: []string{
"Id=example.service",
"LoadState=not-found",
"ActiveState=inactive",
"SubState=dead",
"UnitFileState=enabled",
"UnitFilePreset=disabled",
"StatusErrno=[not set]",
"NRestarts=[not set]",
"MemoryCurrent=[not set]",
"MemoryPeak=[not set]",
"MemorySwapCurrent=[not set]",
"MemorySwapPeak=[not set]",
"MemoryAvailable=[not set]",
"MainPID=[not set]",
},
Tags: map[string]string{
"name": "example.service",
"load": "not-found",
"active": "inactive",
"sub": "dead",
"uf_state": "enabled",
"uf_preset": "disabled",
},
Fields: map[string]interface{}{
"load_code": 2,
"active_code": 2,
"sub_code": 1,
},
},
{
Name: "example unknown unknown unknown",
Lines: []string{
"Id=example.service",
"LoadState=unknown",
"ActiveState=unknown",
"SubState=unknown",
"UnitFileState=unknown",
"UnitFilePreset=unknown",
},
Err: fmt.Errorf("error parsing field '%s', value '%s' not in map", "LoadState", "unknown"),
},
{
Name: "example no key value pair",
Lines: []string{
"Id=example.service",
"LoadState",
"ActiveState=active",
},
Err: fmt.Errorf("error parsing line (expected key=value): %s", "LoadState"),
Tags: map[string]string{
"name": "example.service",
"active": "active",
},
Fields: map[string]interface{}{
"active_code": 0,
},
},
}
dut := initSubcommandShow()
runParserTests(t, tests, dut)
}
func TestCommandlineShow(t *testing.T) {
propertiesTemplate := []string{
"--all",
"--type=service",
"--property=Id",
"--property=LoadState",
"--property=ActiveState",
"--property=SubState",
"--property=StatusErrno",
"--property=UnitFileState",
"--property=UnitFilePreset",
"--property=NRestarts",
"--property=MemoryCurrent",
"--property=MemoryPeak",
"--property=MemorySwapCurrent",
"--property=MemorySwapPeak",
"--property=MemoryAvailable",
"--property=MainPID",
}
// Test with the default patern
paramsTemplate := append([]string{
"show",
"*",
}, propertiesTemplate...)
dut := initSubcommandShow()
systemdUnits := SystemdUnits{
UnitType: "service",
}
runCommandLineTest(t, paramsTemplate, dut, &systemdUnits)
// Test using a more komplex pattern
paramsTemplate = append([]string{
"show",
"unita.service",
"*.timer",
}, propertiesTemplate...)
systemdUnits = SystemdUnits{
UnitType: "service",
Pattern: "unita.service *.timer",
}
runCommandLineTest(t, paramsTemplate, dut, &systemdUnits)
}

View File

@ -2,199 +2,33 @@
package systemd_units
import (
"bytes"
_ "embed"
"fmt"
"os/exec"
"strings"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
const measurement = "systemd_units"
// Below are mappings of systemd state tables as defined in
// https://github.com/systemd/systemd/blob/c87700a1335f489be31cd3549927da68b5638819/src/basic/unit-def.c
// Duplicate strings are removed from this list.
// This map is used by `subcommand_show` and `subcommand_list`. Changes must be
// compatible with both subcommands.
var loadMap = map[string]int{
"loaded": 0,
"stub": 1,
"not-found": 2,
"bad-setting": 3,
"error": 4,
"merged": 5,
"masked": 6,
}
var activeMap = map[string]int{
"active": 0,
"reloading": 1,
"inactive": 2,
"failed": 3,
"activating": 4,
"deactivating": 5,
}
var subMap = map[string]int{
// service_state_table, offset 0x0000
"running": 0x0000,
"dead": 0x0001,
"start-pre": 0x0002,
"start": 0x0003,
"exited": 0x0004,
"reload": 0x0005,
"stop": 0x0006,
"stop-watchdog": 0x0007,
"stop-sigterm": 0x0008,
"stop-sigkill": 0x0009,
"stop-post": 0x000a,
"final-sigterm": 0x000b,
"failed": 0x000c,
"auto-restart": 0x000d,
"condition": 0x000e,
"cleaning": 0x000f,
// automount_state_table, offset 0x0010
// continuation of service_state_table
"waiting": 0x0010,
"reload-signal": 0x0011,
"reload-notify": 0x0012,
"final-watchdog": 0x0013,
"dead-before-auto-restart": 0x0014,
"failed-before-auto-restart": 0x0015,
"dead-resources-pinned": 0x0016,
"auto-restart-queued": 0x0017,
// device_state_table, offset 0x0020
"tentative": 0x0020,
"plugged": 0x0021,
// mount_state_table, offset 0x0030
"mounting": 0x0030,
"mounting-done": 0x0031,
"mounted": 0x0032,
"remounting": 0x0033,
"unmounting": 0x0034,
"remounting-sigterm": 0x0035,
"remounting-sigkill": 0x0036,
"unmounting-sigterm": 0x0037,
"unmounting-sigkill": 0x0038,
// path_state_table, offset 0x0040
// scope_state_table, offset 0x0050
"abandoned": 0x0050,
// slice_state_table, offset 0x0060
"active": 0x0060,
// socket_state_table, offset 0x0070
"start-chown": 0x0070,
"start-post": 0x0071,
"listening": 0x0072,
"stop-pre": 0x0073,
"stop-pre-sigterm": 0x0074,
"stop-pre-sigkill": 0x0075,
"final-sigkill": 0x0076,
// swap_state_table, offset 0x0080
"activating": 0x0080,
"activating-done": 0x0081,
"deactivating": 0x0082,
"deactivating-sigterm": 0x0083,
"deactivating-sigkill": 0x0084,
// target_state_table, offset 0x0090
// timer_state_table, offset 0x00a0
"elapsed": 0x00a0,
}
// SystemdUnits is a telegraf plugin to gather systemd unit status
type SystemdUnits struct {
Timeout config.Duration `toml:"timeout"`
SubCommand string `toml:"subcommand"`
UnitType string `toml:"unittype"`
Pattern string `toml:"pattern"`
resultParser parseResultFunc
commandParams *[]string
Pattern string `toml:"pattern"`
UnitType string `toml:"unittype"`
Details bool `toml:"details"`
Timeout config.Duration `toml:"timeout"`
Log telegraf.Logger `toml:"-"`
archParams
}
type getParametersFunc func(*SystemdUnits) *[]string
type parseResultFunc func(telegraf.Accumulator, *bytes.Buffer)
type subCommandInfo struct {
getParameters getParametersFunc
parseResult parseResultFunc
}
var (
defaultSubCommand = "list-units"
defaultTimeout = config.Duration(time.Second)
defaultUnitType = "service"
defaultPattern = ""
)
func (*SystemdUnits) SampleConfig() string {
return sampleConfig
}
func (s *SystemdUnits) Init() error {
var subCommandInfo *subCommandInfo
switch s.SubCommand {
case "show":
subCommandInfo = initSubcommandShow()
case "list-units":
subCommandInfo = initSubcommandListUnits()
default:
return fmt.Errorf("invalid value for 'subcommand': %s", s.SubCommand)
}
// Save the parsing function for later and pre-compute the
// command line because it will not change.
s.resultParser = subCommandInfo.parseResult
s.commandParams = subCommandInfo.getParameters(s)
return nil
}
func (s *SystemdUnits) Gather(acc telegraf.Accumulator) error {
// is systemctl available ?
systemctlPath, err := exec.LookPath("systemctl")
if err != nil {
return err
}
cmd := exec.Command(systemctlPath, *s.commandParams...)
var out bytes.Buffer
cmd.Stdout = &out
err = internal.RunTimeout(cmd, time.Duration(s.Timeout))
if err != nil {
return fmt.Errorf("error running systemctl %q: %w", strings.Join(*s.commandParams, " "), err)
}
s.resultParser(acc, &out)
return nil
}
func init() {
inputs.Add("systemd_units", func() telegraf.Input {
return &SystemdUnits{
Timeout: defaultTimeout,
UnitType: defaultUnitType,
Pattern: defaultPattern,
SubCommand: defaultSubCommand,
}
return &SystemdUnits{Timeout: config.Duration(time.Second)}
})
}

View File

@ -0,0 +1,364 @@
//go:build linux
package systemd_units
import (
"context"
"fmt"
"math"
"path"
"strings"
"time"
"github.com/coreos/go-systemd/v22/dbus"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/filter"
)
// Below are mappings of systemd state tables as defined in
// https://github.com/systemd/systemd/blob/c87700a1335f489be31cd3549927da68b5638819/src/basic/unit-def.c
// Duplicate strings are removed from this list.
// This map is used by `subcommand_show` and `subcommand_list`. Changes must be
// compatible with both subcommands.
var loadMap = map[string]int{
"loaded": 0,
"stub": 1,
"not-found": 2,
"bad-setting": 3,
"error": 4,
"merged": 5,
"masked": 6,
}
var activeMap = map[string]int{
"active": 0,
"reloading": 1,
"inactive": 2,
"failed": 3,
"activating": 4,
"deactivating": 5,
}
var subMap = map[string]int{
// service_state_table, offset 0x0000
"running": 0x0000,
"dead": 0x0001,
"start-pre": 0x0002,
"start": 0x0003,
"exited": 0x0004,
"reload": 0x0005,
"stop": 0x0006,
"stop-watchdog": 0x0007,
"stop-sigterm": 0x0008,
"stop-sigkill": 0x0009,
"stop-post": 0x000a,
"final-sigterm": 0x000b,
"failed": 0x000c,
"auto-restart": 0x000d,
"condition": 0x000e,
"cleaning": 0x000f,
// automount_state_table, offset 0x0010
// continuation of service_state_table
"waiting": 0x0010,
"reload-signal": 0x0011,
"reload-notify": 0x0012,
"final-watchdog": 0x0013,
"dead-before-auto-restart": 0x0014,
"failed-before-auto-restart": 0x0015,
"dead-resources-pinned": 0x0016,
"auto-restart-queued": 0x0017,
// device_state_table, offset 0x0020
"tentative": 0x0020,
"plugged": 0x0021,
// mount_state_table, offset 0x0030
"mounting": 0x0030,
"mounting-done": 0x0031,
"mounted": 0x0032,
"remounting": 0x0033,
"unmounting": 0x0034,
"remounting-sigterm": 0x0035,
"remounting-sigkill": 0x0036,
"unmounting-sigterm": 0x0037,
"unmounting-sigkill": 0x0038,
// path_state_table, offset 0x0040
// scope_state_table, offset 0x0050
"abandoned": 0x0050,
// slice_state_table, offset 0x0060
"active": 0x0060,
// socket_state_table, offset 0x0070
"start-chown": 0x0070,
"start-post": 0x0071,
"listening": 0x0072,
"stop-pre": 0x0073,
"stop-pre-sigterm": 0x0074,
"stop-pre-sigkill": 0x0075,
"final-sigkill": 0x0076,
// swap_state_table, offset 0x0080
"activating": 0x0080,
"activating-done": 0x0081,
"deactivating": 0x0082,
"deactivating-sigterm": 0x0083,
"deactivating-sigkill": 0x0084,
// target_state_table, offset 0x0090
// timer_state_table, offset 0x00a0
"elapsed": 0x00a0,
}
type unitInfo struct {
name string
state dbus.UnitStatus
properties map[string]interface{}
unitFileState string
unitFilePreset string
}
type client interface {
Connected() bool
Close()
ListUnitFilesByPatternsContext(ctx context.Context, states, pattern []string) ([]dbus.UnitFile, error)
ListUnitsByNamesContext(ctx context.Context, units []string) ([]dbus.UnitStatus, error)
GetUnitTypePropertiesContext(ctx context.Context, unit, unitType string) (map[string]interface{}, error)
GetUnitPropertyContext(ctx context.Context, unit, propertyName string) (*dbus.Property, error)
ListUnitsContext(ctx context.Context) ([]dbus.UnitStatus, error)
}
type archParams struct {
client client
pattern []string
filter filter.Filter
}
func (s *SystemdUnits) Init() error {
// Set default pattern
if s.Pattern == "" {
s.Pattern = "*"
}
// Check unit-type and convert the first letter to uppercase as this is
// what dbus expects.
switch s.UnitType {
case "":
s.UnitType = "service"
case "service", "socket", "target", "device", "mount", "automount", "swap",
"timer", "path", "slice", "scope":
default:
return fmt.Errorf("invalid 'unittype' %q", s.UnitType)
}
s.UnitType = strings.ToUpper(s.UnitType[0:1]) + strings.ToLower(s.UnitType[1:])
s.pattern = strings.Split(s.Pattern, " ")
f, err := filter.Compile(s.pattern)
if err != nil {
return fmt.Errorf("compiling filter failed: %w", err)
}
s.filter = f
return nil
}
func (s *SystemdUnits) Start(telegraf.Accumulator) error {
ctx := context.Background()
client, err := dbus.NewSystemConnectionContext(ctx)
if err != nil {
return err
}
s.client = client
return nil
}
func (s *SystemdUnits) Stop() {
if s.client != nil && s.client.Connected() {
s.client.Close()
}
}
func (s *SystemdUnits) Gather(acc telegraf.Accumulator) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.Timeout))
defer cancel()
// List all loaded units to handle multi-instance units correctly
loaded, err := s.client.ListUnitsContext(ctx)
if err != nil {
return fmt.Errorf("listing loaded units failed: %w", err)
}
// List all unit files matching the pattern to also get disabled units
list := []string{"enabled", "disabled", "static"}
files, err := s.client.ListUnitFilesByPatternsContext(ctx, list, s.pattern)
if err != nil {
return fmt.Errorf("listing unit files failed: %w", err)
}
// Collect all matching units, the loaded ones and the disabled ones
states := make([]dbus.UnitStatus, 0, len(files))
// Match all loaded units first
seen := make(map[string]bool)
for _, u := range loaded {
if !s.filter.Match(u.Name) {
continue
}
states = append(states, u)
// Remember multi-instance units to remove duplicates from files
instance := u.Name
if strings.Contains(u.Name, "@") {
prefix, _, _ := strings.Cut(u.Name, "@")
suffix := path.Ext(u.Name)
instance = prefix + "@" + suffix
}
seen[instance] = true
}
// Now split the unit-files into disabled ones and static ones, ignore
// enabled units as those are already contained in the "loaded" list.
disabled := make([]string, 0, len(files))
static := make([]string, 0, len(files))
for _, f := range files {
name := path.Base(f.Path)
switch f.Type {
case "disabled":
if seen[name] {
continue
}
seen[name] = true
disabled = append(disabled, name)
case "static":
// Make sure we filter already loaded static multi-instance units
instance := name
if strings.Contains(name, "@") {
prefix, _, _ := strings.Cut(name, "@")
suffix := path.Ext(name)
instance = prefix + "@" + suffix
}
if seen[instance] || seen[name] {
continue
}
seen[instance] = true
static = append(static, name)
}
}
// Resolve the disabled and remaining static units
disabledStates, err := s.client.ListUnitsByNamesContext(ctx, disabled)
if err != nil {
return fmt.Errorf("listing unit states failed: %w", err)
}
states = append(states, disabledStates...)
// Add special information about unused static units
for _, name := range static {
if !strings.EqualFold(strings.TrimPrefix(path.Ext(name), "."), s.UnitType) {
continue
}
states = append(states, dbus.UnitStatus{
Name: name,
LoadState: "stub",
ActiveState: "inactive",
SubState: "dead",
})
}
// Merge the unit information into one struct
units := make([]unitInfo, 0, len(states))
for _, state := range states {
// Filter units of the wrong type
props, err := s.client.GetUnitTypePropertiesContext(ctx, state.Name, s.UnitType)
if err != nil {
continue
}
u := unitInfo{
name: state.Name,
state: state,
properties: props,
}
// Get required unit file properties
if v, err := s.client.GetUnitPropertyContext(ctx, state.Name, "UnitFileState"); err == nil {
u.unitFileState = strings.Trim(v.Value.String(), `'"`)
}
if v, err := s.client.GetUnitPropertyContext(ctx, state.Name, "UnitFilePreset"); err == nil {
u.unitFilePreset = strings.Trim(v.Value.String(), `'"`)
}
units = append(units, u)
}
// Create the metrics
for _, u := range units {
// Map the state names to numerical values
load, ok := loadMap[u.state.LoadState]
if !ok {
acc.AddError(fmt.Errorf("parsing field 'load' failed, value not in map: %s", u.state.LoadState))
continue
}
active, ok := activeMap[u.state.ActiveState]
if !ok {
acc.AddError(fmt.Errorf("parsing field field 'active' failed, value not in map: %s", u.state.ActiveState))
continue
}
subState, ok := subMap[u.state.SubState]
if !ok {
acc.AddError(fmt.Errorf("parsing field field 'sub' failed, value not in map: %s", u.state.SubState))
continue
}
// Create the metric
tags := map[string]string{
"name": u.name,
"load": u.state.LoadState,
"active": u.state.ActiveState,
"sub": u.state.SubState,
}
fields := map[string]interface{}{
"load_code": load,
"active_code": active,
"sub_code": subState,
}
if s.Details {
tags["state"] = u.unitFileState
tags["preset"] = u.unitFilePreset
fields["status_errno"] = u.properties["StatusErrno"]
fields["restarts"] = u.properties["NRestarts"]
fields["pid"] = u.properties["MainPID"]
fields["mem_current"] = u.properties["MemoryCurrent"]
fields["mem_peak"] = u.properties["MemoryPeak"]
fields["swap_current"] = u.properties["MemorySwapCurrent"]
fields["swap_peak"] = u.properties["MemorySwapPeak"]
fields["mem_avail"] = u.properties["MemoryAvailable"]
// Sanitize unset memory fields
for k, value := range fields {
switch {
case strings.HasPrefix(k, "mem_"), strings.HasPrefix(k, "swap_"):
v, ok := value.(uint64)
if ok && v == math.MaxUint64 || value == nil {
fields[k] = uint64(0)
}
}
}
}
acc.AddFields("systemd_units", fields, tags)
}
return nil
}

View File

@ -1,3 +1,20 @@
//go:build !linux
package systemd_units
import "github.com/influxdata/telegraf"
type archParams struct{}
func (s *SystemdUnits) Init() error {
s.Log.Info("Skipping plugin as it is not supported by this platform!")
// Required to remove linter-warning on unused struct member
_ = s.archParams
return nil
}
func (*SystemdUnits) Gather(_ telegraf.Accumulator) error {
return nil
}

File diff suppressed because it is too large Load Diff