feat(inputs.systemd_units): Allow to query unloaded/disabled units (#14814)
Co-authored-by: Joshua Powers <powersj@fastmail.com>
This commit is contained in:
parent
33ec0a9dec
commit
ebea0b289a
2
go.mod
2
go.mod
|
|
@ -323,7 +323,7 @@ require (
|
||||||
github.com/goburrow/serial v0.1.1-0.20211022031912-bfb69110f8dd // indirect
|
github.com/goburrow/serial v0.1.1-0.20211022031912-bfb69110f8dd // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // 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/gofrs/uuid v4.4.0+incompatible // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -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
|
This plugin gathers the status of systemd-units on Linux, using systemd's DBus
|
||||||
`systemctl list-units` or the `systemctl show` command to collect data on
|
interface.
|
||||||
service status.
|
|
||||||
|
|
||||||
This plugin is related to the [win_services module](../win_services/README.md),
|
Please note: At least systemd v230 is required!
|
||||||
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 -->
|
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||||
|
|
||||||
|
|
@ -53,30 +17,42 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
```toml @sample.conf
|
```toml @sample.conf
|
||||||
# Gather systemd units state
|
# Gather information about systemd-unit states
|
||||||
|
# This plugin ONLY supports Linux
|
||||||
[[inputs.systemd_units]]
|
[[inputs.systemd_units]]
|
||||||
## Set timeout for systemctl execution
|
## Pattern of units to collect
|
||||||
# timeout = "1s"
|
## 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.
|
## Filter for a specific unit type
|
||||||
## Using `list-units` is the option with the broadest compatibility.
|
## Available settings are: service, socket, target, device, mount,
|
||||||
## Using `show` will get more information but may fail to list all units on
|
## automount, swap, timer, path, slice and scope
|
||||||
## 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"
|
# unittype = "service"
|
||||||
|
|
||||||
## Filter for a specific pattern, default is "" (i.e. all), other possible
|
## Collect detailed information for the units
|
||||||
## values are valid pattern for systemctl, e.g. "a*" for all units with
|
# details = false
|
||||||
## names starting with "a"
|
|
||||||
## pattern = "telegraf* influxdb*"
|
## Timeout for state-collection
|
||||||
## pattern = "a*"
|
# timeout = "1s"
|
||||||
# pattern = ""
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
## Metrics
|
||||||
|
|
||||||
These metrics are available in both modes:
|
These metrics are available in both modes:
|
||||||
|
|
@ -92,25 +68,25 @@ These metrics are available in both modes:
|
||||||
- active_code (int, see below)
|
- active_code (int, see below)
|
||||||
- sub_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:
|
- systemd_units:
|
||||||
- tags:
|
- tags:
|
||||||
- uf_state (string, unit file state)
|
- state (string, unit file state)
|
||||||
- uf_preset (string, unit file preset state)
|
- preset (string, unit file preset state)
|
||||||
- fields:
|
- fields:
|
||||||
- status_errno (int, last error)
|
- status_errno (int, last error)
|
||||||
- restarts (int, number of restarts)
|
- 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)
|
- 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
|
### Load
|
||||||
|
|
||||||
enumeration of [unit_load_state_table][1]
|
Enumeration of [unit_load_state_table][1]
|
||||||
|
|
||||||
| Value | Meaning | Description |
|
| Value | Meaning | Description |
|
||||||
| ----- | ------- | ----------- |
|
| ----- | ------- | ----------- |
|
||||||
|
|
@ -126,7 +102,7 @@ enumeration of [unit_load_state_table][1]
|
||||||
|
|
||||||
### Active
|
### Active
|
||||||
|
|
||||||
enumeration of [unit_active_state_table][2]
|
Enumeration of [unit_active_state_table][2]
|
||||||
|
|
||||||
| Value | Meaning | Description |
|
| Value | Meaning | Description |
|
||||||
| ----- | ------- | ----------- |
|
| ----- | ------- | ----------- |
|
||||||
|
|
@ -213,7 +189,7 @@ were removed, tables are hex aligned to keep some space for future values
|
||||||
|
|
||||||
## Example Output
|
## Example Output
|
||||||
|
|
||||||
### Output in `list-units` mode
|
### Output in non-detailed mode
|
||||||
|
|
||||||
```text
|
```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=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
|
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
|
```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=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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,19 @@
|
||||||
# Gather systemd units state
|
# Gather information about systemd-unit states
|
||||||
|
# This plugin ONLY supports Linux
|
||||||
[[inputs.systemd_units]]
|
[[inputs.systemd_units]]
|
||||||
## Set timeout for systemctl execution
|
## Pattern of units to collect
|
||||||
# timeout = "1s"
|
## 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.
|
## Filter for a specific unit type
|
||||||
## Using `list-units` is the option with the broadest compatibility.
|
## Available settings are: service, socket, target, device, mount,
|
||||||
## Using `show` will get more information but may fail to list all units on
|
## automount, swap, timer, path, slice and scope
|
||||||
## 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"
|
# unittype = "service"
|
||||||
|
|
||||||
## Filter for a specific pattern, default is "" (i.e. all), other possible
|
## Collect detailed information for the units
|
||||||
## values are valid pattern for systemctl, e.g. "a*" for all units with
|
# details = false
|
||||||
## names starting with "a"
|
|
||||||
## pattern = "telegraf* influxdb*"
|
## Timeout for state-collection
|
||||||
## pattern = "a*"
|
# timeout = "1s"
|
||||||
# pattern = ""
|
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
//go:generate ../../../tools/readme_config_includer/generator
|
|
||||||
package systemd_units
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/influxdata/telegraf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Gather parses systemctl outputs and adds counters to the Accumulator
|
|
||||||
func parseListUnits(acc telegraf.Accumulator, buffer *bytes.Buffer) {
|
|
||||||
scanner := bufio.NewScanner(buffer)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
|
|
||||||
data := strings.Fields(line)
|
|
||||||
if len(data) < 4 {
|
|
||||||
acc.AddError(fmt.Errorf("parsing line failed (expected at least 4 fields): %s", line))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := data[0]
|
|
||||||
load := data[1]
|
|
||||||
active := data[2]
|
|
||||||
sub := data[3]
|
|
||||||
tags := map[string]string{
|
|
||||||
"name": name,
|
|
||||||
"load": load,
|
|
||||||
"active": active,
|
|
||||||
"sub": sub,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
loadCode int
|
|
||||||
activeCode int
|
|
||||||
subCode int
|
|
||||||
ok bool
|
|
||||||
)
|
|
||||||
if loadCode, ok = loadMap[load]; !ok {
|
|
||||||
acc.AddError(fmt.Errorf("parsing field 'load' failed, value not in map: %s", load))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if activeCode, ok = activeMap[active]; !ok {
|
|
||||||
acc.AddError(fmt.Errorf("parsing field field 'active' failed, value not in map: %s", active))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if subCode, ok = subMap[sub]; !ok {
|
|
||||||
acc.AddError(fmt.Errorf("parsing field field 'sub' failed, value not in map: %s", sub))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fields := map[string]interface{}{
|
|
||||||
"load_code": loadCode,
|
|
||||||
"active_code": activeCode,
|
|
||||||
"sub_code": subCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
acc.AddFields(measurement, fields, tags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getListUnitsParameters(s *SystemdUnits) *[]string {
|
|
||||||
// build parameters for systemctl call
|
|
||||||
params := []string{"list-units"}
|
|
||||||
// create patterns parameters if provided in config
|
|
||||||
if s.Pattern != "" {
|
|
||||||
psplit := strings.SplitN(s.Pattern, " ", -1)
|
|
||||||
params = append(params, psplit...)
|
|
||||||
}
|
|
||||||
params = append(params,
|
|
||||||
"--all",
|
|
||||||
"--plain",
|
|
||||||
"--type="+s.UnitType,
|
|
||||||
"--no-legend",
|
|
||||||
)
|
|
||||||
|
|
||||||
return ¶ms
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSubcommandListUnits() *subCommandInfo {
|
|
||||||
return &subCommandInfo{
|
|
||||||
getParameters: getListUnitsParameters,
|
|
||||||
parseResult: parseListUnits,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
//go:generate ../../../tools/readme_config_includer/generator
|
|
||||||
package systemd_units
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/influxdata/telegraf"
|
|
||||||
)
|
|
||||||
|
|
||||||
type valueDef struct {
|
|
||||||
valueName string
|
|
||||||
valueMap *map[string]int
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following two maps configure the mapping of systemd properties to
|
|
||||||
// tags or values. The properties are automatically requested from
|
|
||||||
// `systemctl show`.
|
|
||||||
// If a vlaue has no valueMap, `atoi` is called on the value to convert it to
|
|
||||||
// an integer.
|
|
||||||
var tagMap = map[string]string{
|
|
||||||
"Id": "name",
|
|
||||||
"LoadState": "load",
|
|
||||||
"ActiveState": "active",
|
|
||||||
"SubState": "sub",
|
|
||||||
"UnitFileState": "uf_state",
|
|
||||||
"UnitFilePreset": "uf_preset",
|
|
||||||
}
|
|
||||||
|
|
||||||
var valueMap = map[string]valueDef{
|
|
||||||
"LoadState": {valueName: "load_code", valueMap: &loadMap},
|
|
||||||
"ActiveState": {valueName: "active_code", valueMap: &activeMap},
|
|
||||||
"SubState": {valueName: "sub_code", valueMap: &subMap},
|
|
||||||
"StatusErrno": {valueName: "status_errno", valueMap: nil},
|
|
||||||
"NRestarts": {valueName: "restarts", valueMap: nil},
|
|
||||||
"MemoryCurrent": {valueName: "mem_current", valueMap: nil},
|
|
||||||
"MemoryPeak": {valueName: "mem_peak", valueMap: nil},
|
|
||||||
"MemorySwapCurrent": {valueName: "swap_current", valueMap: nil},
|
|
||||||
"MemorySwapPeak": {valueName: "swap_peak", valueMap: nil},
|
|
||||||
"MemoryAvailable": {valueName: "mem_avail", valueMap: nil},
|
|
||||||
"MainPID": {valueName: "pid", valueMap: nil},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gather parses systemctl outputs and adds counters to the Accumulator
|
|
||||||
func parseShow(acc telegraf.Accumulator, buffer *bytes.Buffer) {
|
|
||||||
scanner := bufio.NewScanner(buffer)
|
|
||||||
|
|
||||||
tags := make(map[string]string)
|
|
||||||
fields := make(map[string]interface{})
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
|
|
||||||
// An empty line signals the start of the next unit
|
|
||||||
if len(line) == 0 {
|
|
||||||
// We need at least a "name" field. This prevents values from the
|
|
||||||
// global information block (enabled by the --all switch) to be
|
|
||||||
// shown as a unit.
|
|
||||||
if _, ok := tags["name"]; ok {
|
|
||||||
acc.AddFields(measurement, fields, tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
tags = make(map[string]string)
|
|
||||||
fields = make(map[string]interface{})
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key, value, ok := strings.Cut(line, "=")
|
|
||||||
if !ok {
|
|
||||||
acc.AddError(fmt.Errorf("error parsing line (expected key=value): %s", line))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map the tags
|
|
||||||
if tagName, isTag := tagMap[key]; isTag {
|
|
||||||
tags[tagName] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map the values
|
|
||||||
if valueDef, isValue := valueMap[key]; isValue {
|
|
||||||
// If a value map is set use it. If not, just try to convert the
|
|
||||||
// value into an integer.
|
|
||||||
if valueDef.valueMap != nil {
|
|
||||||
code, ok := (*valueDef.valueMap)[value]
|
|
||||||
if !ok {
|
|
||||||
acc.AddError(fmt.Errorf("error parsing field '%s', value '%s' not in map", key, value))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fields[valueDef.valueName] = code
|
|
||||||
} else {
|
|
||||||
if value != "[not set]" {
|
|
||||||
intVal, err := strconv.Atoi(value)
|
|
||||||
if err != nil {
|
|
||||||
acc.AddError(fmt.Errorf("error '%w' parsing field '%s'. Not an integer value", err, key))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fields[valueDef.valueName] = intVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the last unit because the output does not contain a newline for this
|
|
||||||
if _, ok := tags["name"]; ok {
|
|
||||||
acc.AddFields(measurement, fields, tags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getShowParameters(s *SystemdUnits) *[]string {
|
|
||||||
// build parameters for systemctl call
|
|
||||||
params := []string{"show"}
|
|
||||||
// create patterns parameters if provided in config
|
|
||||||
if s.Pattern == "" {
|
|
||||||
params = append(params, "*")
|
|
||||||
} else {
|
|
||||||
psplit := strings.SplitN(s.Pattern, " ", -1)
|
|
||||||
params = append(params, psplit...)
|
|
||||||
}
|
|
||||||
|
|
||||||
params = append(params, "--all", "--type="+s.UnitType)
|
|
||||||
|
|
||||||
// add the fields we're interested in to the command line
|
|
||||||
for property := range tagMap {
|
|
||||||
params = append(params, "--property="+property)
|
|
||||||
}
|
|
||||||
for property := range valueMap {
|
|
||||||
// If a property exists within the tagMap it was already added. Do not add it again to
|
|
||||||
// keep the command line short.
|
|
||||||
if _, exists := tagMap[property]; !exists {
|
|
||||||
params = append(params, "--property="+property)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ¶ms
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSubcommandShow() *subCommandInfo {
|
|
||||||
return &subCommandInfo{
|
|
||||||
getParameters: getShowParameters,
|
|
||||||
parseResult: parseShow,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -2,199 +2,33 @@
|
||||||
package systemd_units
|
package systemd_units
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/influxdata/telegraf"
|
"github.com/influxdata/telegraf"
|
||||||
"github.com/influxdata/telegraf/config"
|
"github.com/influxdata/telegraf/config"
|
||||||
"github.com/influxdata/telegraf/internal"
|
|
||||||
"github.com/influxdata/telegraf/plugins/inputs"
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed sample.conf
|
//go:embed sample.conf
|
||||||
var sampleConfig string
|
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
|
// SystemdUnits is a telegraf plugin to gather systemd unit status
|
||||||
type SystemdUnits struct {
|
type SystemdUnits struct {
|
||||||
Timeout config.Duration `toml:"timeout"`
|
Pattern string `toml:"pattern"`
|
||||||
SubCommand string `toml:"subcommand"`
|
UnitType string `toml:"unittype"`
|
||||||
UnitType string `toml:"unittype"`
|
Details bool `toml:"details"`
|
||||||
Pattern string `toml:"pattern"`
|
Timeout config.Duration `toml:"timeout"`
|
||||||
resultParser parseResultFunc
|
Log telegraf.Logger `toml:"-"`
|
||||||
commandParams *[]string
|
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 {
|
func (*SystemdUnits) SampleConfig() string {
|
||||||
return sampleConfig
|
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() {
|
func init() {
|
||||||
inputs.Add("systemd_units", func() telegraf.Input {
|
inputs.Add("systemd_units", func() telegraf.Input {
|
||||||
return &SystemdUnits{
|
return &SystemdUnits{Timeout: config.Duration(time.Second)}
|
||||||
Timeout: defaultTimeout,
|
|
||||||
UnitType: defaultUnitType,
|
|
||||||
Pattern: defaultPattern,
|
|
||||||
SubCommand: defaultSubCommand,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
//go:build !linux
|
//go:build !linux
|
||||||
|
|
||||||
package systemd_units
|
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
Loading…
Reference in New Issue