1
0
Fork 0
telegraf/plugins/inputs/systemd_units/systemd_units_test.go

1050 lines
26 KiB
Go
Raw Normal View History

//go:build linux
package systemd_units
import (
"context"
"fmt"
"math"
"os/user"
"strings"
"testing"
"time"
sdbus "github.com/coreos/go-systemd/v22/dbus"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/filter"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/testutil"
)
type properties struct {
uf *sdbus.UnitFile
utype string
state *sdbus.UnitStatus
ufPreset string
ufState string
ufActiveEnter uint64
properties map[string]interface{}
}
func TestDefaultPattern(t *testing.T) {
plugin := &SystemdUnits{}
require.NoError(t, plugin.Init())
require.Equal(t, "*", plugin.Pattern)
}
func TestDefaultScope(t *testing.T) {
u, err := user.Current()
if err != nil {
return
}
tests := []struct {
name string
scope string
expectedScope string
expectedUser string
}{
{
name: "default scope",
scope: "",
expectedScope: "system",
expectedUser: "",
},
{
name: "system scope",
scope: "system",
expectedScope: "system",
expectedUser: "",
},
{
name: "user scope",
scope: "user",
expectedScope: "user",
expectedUser: u.Username,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plugin := &SystemdUnits{
Scope: tt.scope,
}
require.NoError(t, plugin.Init())
require.Equal(t, tt.expectedScope, plugin.scope)
require.Equal(t, tt.expectedUser, plugin.user)
})
}
}
func TestListFiles(t *testing.T) {
tests := []struct {
name string
properties map[string]properties
line string
expected []telegraf.Metric
expectedErr string
}{
{
name: "example loaded active running",
line: "example.service loaded active running example service description",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "loaded",
ActiveState: "active",
SubState: "running",
},
},
},
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "loaded",
"active": "active",
"sub": "running",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 0,
},
time.Unix(0, 0),
),
},
},
{
name: "example loaded active exited",
line: "example.service loaded active exited example service description",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "loaded",
ActiveState: "active",
SubState: "exited",
},
},
},
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "loaded",
"active": "active",
"sub": "exited",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 4,
},
time.Unix(0, 0),
),
},
},
{
name: "example loaded failed failed",
line: "example.service loaded failed failed example service description",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "loaded",
ActiveState: "failed",
SubState: "failed",
},
},
},
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "loaded",
"active": "failed",
"sub": "failed",
},
map[string]interface{}{
"load_code": 0,
"active_code": 3,
"sub_code": 12,
},
time.Unix(0, 0),
),
},
},
{
name: "example not-found inactive dead",
line: "example.service not-found inactive dead example service description",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "not-found",
ActiveState: "inactive",
SubState: "dead",
},
},
},
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "not-found",
"active": "inactive",
"sub": "dead",
},
map[string]interface{}{
"load_code": 2,
"active_code": 2,
"sub_code": 1,
},
time.Unix(0, 0),
),
},
},
{
name: "example unknown unknown unknown",
line: "example.service unknown unknown unknown example service description",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "unknown",
ActiveState: "unknown",
SubState: "unknown",
},
},
},
expectedErr: "parsing field 'load' failed, value not in map",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Run a meta-test for finding regressions compared to metrics
// emitted by previous versions
old, err := oldParseListUnits(tt.line)
if tt.expectedErr == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
}
testutil.RequireMetricsEqual(t, old, tt.expected, testutil.IgnoreTime())
// Setup plugin. Do NOT call Start() as this would connect to
// the real systemd daemon.
plugin := &SystemdUnits{
Pattern: "examp*",
Timeout: config.Duration(time.Second),
Log: testutil.Logger{},
}
require.NoError(t, plugin.Init())
// Create a fake client to inject data
client := &fakeClient{
units: tt.properties,
connected: true,
}
client.fixPropertyTypes()
plugin.client = client
defer plugin.Stop()
// Run gather
var acc testutil.Accumulator
err = acc.GatherError(plugin.Gather)
if tt.expectedErr != "" {
require.ErrorContains(t, err, tt.expectedErr)
return
}
require.NoError(t, err)
// Do the comparison
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, tt.expected, actual, testutil.IgnoreTime())
})
}
}
func TestShow(t *testing.T) {
enter := time.Now().UnixMicro()
tests := []struct {
name string
properties map[string]properties
expected []telegraf.Metric
expectedErr string
}{
{
name: "example loaded active running",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "loaded",
ActiveState: "active",
SubState: "running",
},
ufPreset: "disabled",
ufState: "enabled",
ufActiveEnter: uint64(enter),
properties: map[string]interface{}{
"Id": "example.service",
"StatusErrno": 0,
"NRestarts": 1,
"MemoryCurrent": 1000,
"MemoryPeak": 2000,
"MemorySwapCurrent": 3000,
"MemorySwapPeak": 4000,
"MemoryAvailable": 5000,
"MainPID": 9999,
},
},
},
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "loaded",
"active": "active",
"sub": "running",
"state": "enabled",
"preset": "disabled",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 0,
"status_errno": 0,
"restarts": 1,
"mem_current": uint64(1000),
"mem_peak": uint64(2000),
"swap_current": uint64(3000),
"swap_peak": uint64(4000),
"mem_avail": uint64(5000),
"pid": 9999,
"active_enter_timestamp_us": uint64(enter),
},
time.Unix(0, 0),
),
},
},
{
name: "example loaded active exited",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "loaded",
ActiveState: "active",
SubState: "exited",
},
ufPreset: "disabled",
ufState: "enabled",
ufActiveEnter: 0,
properties: map[string]interface{}{
"Id": "example.service",
"StatusErrno": 0,
"NRestarts": 0,
},
},
},
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "loaded",
"active": "active",
"sub": "exited",
"state": "enabled",
"preset": "disabled",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 4,
"status_errno": 0,
"restarts": 0,
"mem_current": uint64(0),
"mem_peak": uint64(0),
"swap_current": uint64(0),
"swap_peak": uint64(0),
"mem_avail": uint64(0),
"active_enter_timestamp_us": uint64(0),
},
time.Unix(0, 0),
),
},
},
{
name: "example loaded failed failed",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "loaded",
ActiveState: "failed",
SubState: "failed",
},
ufPreset: "disabled",
ufState: "enabled",
ufActiveEnter: uint64(enter),
properties: map[string]interface{}{
"Id": "example.service",
"StatusErrno": 10,
"NRestarts": 1,
"MemoryCurrent": 1000,
"MemoryPeak": 2000,
"MemorySwapCurrent": 3000,
"MemorySwapPeak": 4000,
"MemoryAvailable": 5000,
},
},
},
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "loaded",
"active": "failed",
"sub": "failed",
"state": "enabled",
"preset": "disabled",
},
map[string]interface{}{
"load_code": 0,
"active_code": 3,
"sub_code": 12,
"status_errno": 10,
"restarts": 1,
"mem_current": uint64(1000),
"mem_peak": uint64(2000),
"swap_current": uint64(3000),
"swap_peak": uint64(4000),
"mem_avail": uint64(5000),
"active_enter_timestamp_us": uint64(enter),
},
time.Unix(0, 0),
),
},
},
{
name: "example not-found inactive dead",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "not-found",
ActiveState: "inactive",
SubState: "dead",
},
ufPreset: "disabled",
ufState: "enabled",
ufActiveEnter: uint64(0),
properties: map[string]interface{}{
"Id": "example.service",
},
},
},
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "not-found",
"active": "inactive",
"sub": "dead",
"state": "enabled",
"preset": "disabled",
},
map[string]interface{}{
"load_code": 2,
"active_code": 2,
"sub_code": 1,
"mem_current": uint64(0),
"mem_peak": uint64(0),
"swap_current": uint64(0),
"swap_peak": uint64(0),
"mem_avail": uint64(0),
"active_enter_timestamp_us": uint64(0),
},
time.Unix(0, 0),
),
},
},
{
name: "example unknown unknown unknown",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "unknown",
ActiveState: "unknown",
SubState: "unknown",
},
ufPreset: "unknown",
ufState: "unknown",
properties: map[string]interface{}{
"Id": "example.service",
},
},
},
expectedErr: "parsing field 'load' failed, value not in map",
},
{
name: "example loaded but inactive with unset fields",
properties: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "loaded",
ActiveState: "inactive",
SubState: "dead",
},
ufPreset: "disabled",
ufState: "disabled",
ufActiveEnter: uint64(0),
properties: map[string]interface{}{
"Id": "example.service",
"StatusErrno": 0,
"NRestarts": 0,
"MemoryCurrent": uint64(math.MaxUint64),
"MemoryPeak": uint64(math.MaxUint64),
"MemorySwapCurrent": uint64(math.MaxUint64),
"MemorySwapPeak": uint64(math.MaxUint64),
"MemoryAvailable": uint64(math.MaxUint64),
},
},
},
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "loaded",
"active": "inactive",
"sub": "dead",
"state": "disabled",
"preset": "disabled",
},
map[string]interface{}{
"load_code": 0,
"active_code": int64(2),
"sub_code": 1,
"status_errno": 0,
"restarts": 0,
"mem_current": uint64(0),
"mem_peak": uint64(0),
"swap_current": uint64(0),
"swap_peak": uint64(0),
"mem_avail": uint64(0),
"active_enter_timestamp_us": uint64(0),
},
time.Unix(0, 0),
),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup plugin. Do NOT call Start() as this would connect to
// the real systemd daemon.
plugin := &SystemdUnits{
Pattern: "examp*",
Details: true,
Timeout: config.Duration(time.Second),
Log: testutil.Logger{},
}
require.NoError(t, plugin.Init())
// Create a fake client to inject data
client := &fakeClient{
units: tt.properties,
connected: true,
}
client.fixPropertyTypes()
plugin.client = client
defer plugin.Stop()
// Run gather
var acc testutil.Accumulator
err := acc.GatherError(plugin.Gather)
if tt.expectedErr != "" {
require.ErrorContains(t, err, tt.expectedErr)
return
}
require.NoError(t, err)
// Do the comparison
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, tt.expected, actual, testutil.IgnoreTime())
})
}
}
func TestMultiInstance(t *testing.T) {
tests := []struct {
name string
pattern string
expected []telegraf.Metric
}{
{
name: "multiple without concrete instance",
pattern: "examp* user@*",
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "example.service",
"load": "loaded",
"active": "active",
"sub": "running",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 0,
},
time.Unix(0, 0),
),
metric.New(
"systemd_units",
map[string]string{
"name": "user@1000.service",
"load": "loaded",
"active": "active",
"sub": "running",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 0,
},
time.Unix(0, 0),
),
metric.New(
"systemd_units",
map[string]string{
"name": "user@1001.service",
"load": "loaded",
"active": "active",
"sub": "exited",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 4,
},
time.Unix(0, 0),
),
},
},
{
name: "multiple without instance prefix",
pattern: "user@1*",
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "user@1000.service",
"load": "loaded",
"active": "active",
"sub": "running",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 0,
},
time.Unix(0, 0),
),
metric.New(
"systemd_units",
map[string]string{
"name": "user@1001.service",
"load": "loaded",
"active": "active",
"sub": "exited",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 4,
},
time.Unix(0, 0),
),
},
},
{
name: "multiple with concrete instance",
pattern: "user@1001.service",
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "user@1001.service",
"load": "loaded",
"active": "active",
"sub": "exited",
},
map[string]interface{}{
"load_code": 0,
"active_code": 0,
"sub_code": 4,
},
time.Unix(0, 0),
),
},
},
{
name: "static but loaded instance",
pattern: "shadow*",
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "shadow.service",
"load": "loaded",
"active": "inactive",
"sub": "dead",
},
map[string]interface{}{
"load_code": 0,
"active_code": 2,
"sub_code": 1,
},
time.Unix(0, 0),
),
},
},
{
name: "static but not loaded instance",
pattern: "cups*",
expected: []telegraf.Metric{
metric.New(
"systemd_units",
map[string]string{
"name": "cups-lpd@.service",
"load": "stub",
"active": "inactive",
"sub": "dead",
},
map[string]interface{}{
"load_code": 1,
"active_code": 2,
"sub_code": 1,
},
time.Unix(0, 0),
),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup plugin. Do NOT call Start() as this would connect to
// the real systemd daemon.
plugin := &SystemdUnits{
Pattern: tt.pattern,
CollectDisabled: true,
Timeout: config.Duration(time.Second),
Log: testutil.Logger{},
}
require.NoError(t, plugin.Init())
// Create a fake client to inject data
client := &fakeClient{
units: map[string]properties{
"example.service": {
utype: "Service",
state: &sdbus.UnitStatus{
Name: "example.service",
LoadState: "loaded",
ActiveState: "active",
SubState: "running",
},
},
"user-runtime-dir@1000.service": {
uf: &sdbus.UnitFile{
Path: "user-runtime-dir@.service",
Type: "static",
},
utype: "Service",
state: &sdbus.UnitStatus{
Name: "user-runtime-dir@1000.service",
LoadState: "loaded",
ActiveState: "active",
SubState: "exited",
},
},
"user@1000.service": {
uf: &sdbus.UnitFile{
Path: "user@.service",
Type: "static",
},
utype: "Service",
state: &sdbus.UnitStatus{
Name: "user@1000.service",
LoadState: "loaded",
ActiveState: "active",
SubState: "running",
},
},
"user@1001.service": {
uf: &sdbus.UnitFile{
Path: "user@.service",
Type: "static",
},
utype: "Service",
state: &sdbus.UnitStatus{
Name: "user@1001.service",
LoadState: "loaded",
ActiveState: "active",
SubState: "exited",
},
},
"shadow.service": {
uf: &sdbus.UnitFile{
Path: "shadow.service",
Type: "static",
},
utype: "Service",
state: &sdbus.UnitStatus{
Name: "shadow.service",
LoadState: "loaded",
ActiveState: "inactive",
SubState: "dead",
},
},
"cups-lpd@.service": {
uf: &sdbus.UnitFile{
Path: "cups-lpd@.service",
Type: "static",
},
utype: "Service",
},
},
connected: true,
}
client.fixPropertyTypes()
plugin.client = client
defer plugin.Stop()
// Run gather
var acc testutil.Accumulator
require.NoError(t, acc.GatherError(plugin.Gather))
// Do the comparison
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, tt.expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
})
}
}
func BenchmarkAllUnitsIntegration(b *testing.B) {
plugin := &SystemdUnits{
CollectDisabled: true,
Timeout: config.Duration(3 * time.Second),
}
require.NoError(b, plugin.Init())
acc := &testutil.Accumulator{Discard: true}
require.NoError(b, plugin.Start(acc))
require.NoError(b, acc.GatherError(plugin.Gather))
require.NotZero(b, acc.NMetrics())
b.Logf("produced %d metrics", acc.NMetrics())
for n := 0; n < b.N; n++ {
//nolint:errcheck // skip check in benchmarking
_ = plugin.Gather(acc)
}
}
func BenchmarkAllLoadedUnitsIntegration(b *testing.B) {
plugin := &SystemdUnits{
Timeout: config.Duration(3 * time.Second),
}
require.NoError(b, plugin.Init())
acc := &testutil.Accumulator{Discard: true}
require.NoError(b, plugin.Start(acc))
require.NoError(b, acc.GatherError(plugin.Gather))
require.NotZero(b, acc.NMetrics())
b.Logf("produced %d metrics", acc.NMetrics())
for n := 0; n < b.N; n++ {
//nolint:errcheck // skip check in benchmarking
_ = plugin.Gather(acc)
}
}
// Fake client implementation
type fakeClient struct {
units map[string]properties
connected bool
}
func (c *fakeClient) fixPropertyTypes() {
for unit, u := range c.units {
for k, value := range u.properties {
if strings.HasPrefix(k, "Memory") {
//nolint:errcheck // will cause issues later in tests
u.properties[k], _ = internal.ToUint64(value)
}
}
c.units[unit] = u
}
}
func (c *fakeClient) Connected() bool {
return c.connected
}
func (c *fakeClient) Close() {
c.connected = false
}
func (c *fakeClient) ListUnitFilesByPatternsContext(_ context.Context, _, pattern []string) ([]sdbus.UnitFile, error) {
f := filter.MustCompile(pattern)
var files []sdbus.UnitFile
seen := make(map[string]bool)
for name, props := range c.units {
var uf sdbus.UnitFile
if props.uf != nil && f.Match(props.uf.Path) {
uf = sdbus.UnitFile{
Path: "/usr/lib/systemd/system/" + props.uf.Path,
Type: props.uf.Type,
}
} else if props.uf == nil && f.Match(name) {
uf = sdbus.UnitFile{
Path: "/usr/lib/systemd/system/" + name,
Type: "enabled",
}
} else {
continue
}
if !seen[uf.Path] {
files = append(files, uf)
}
seen[uf.Path] = true
}
return files, nil
}
func (c *fakeClient) ListUnitsByNamesContext(_ context.Context, units []string) ([]sdbus.UnitStatus, error) {
var states []sdbus.UnitStatus
for name, u := range c.units {
for _, requestedName := range units {
if name == requestedName && u.state != nil {
states = append(states, *u.state)
break
}
}
}
return states, nil
}
func (c *fakeClient) GetUnitTypePropertiesContext(_ context.Context, unit, unitType string) (map[string]interface{}, error) {
u, found := c.units[unit]
if !found {
return nil, nil
}
if u.utype != unitType {
return nil, fmt.Errorf("unknown interface 'org.freedesktop.systemd1.%s", unitType)
}
return u.properties, nil
}
func (c *fakeClient) GetUnitPropertiesContext(_ context.Context, unit string) (map[string]interface{}, error) {
u, found := c.units[unit]
if !found {
return nil, nil
}
return map[string]interface{}{
"UnitFileState": u.ufState,
"UnitFilePreset": u.ufPreset,
"ActiveEnterTimestamp": u.ufActiveEnter,
}, nil
}
func (c *fakeClient) ListUnitsContext(_ context.Context) ([]sdbus.UnitStatus, error) {
units := make([]sdbus.UnitStatus, 0, len(c.units))
for _, u := range c.units {
if u.state != nil {
units = append(units, *u.state)
}
}
return units, nil
}
// Slightly adapted version of 'parseListUnits()' function of 'subcommand_list.go'
func oldParseListUnits(line string) ([]telegraf.Metric, error) {
data := strings.Fields(line)
if len(data) < 4 {
return nil, fmt.Errorf("parsing line failed (expected at least 4 fields): %s", line)
}
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 {
return nil, fmt.Errorf("parsing field 'load' failed, value not in map: %s", load)
}
if activeCode, ok = activeMap[active]; !ok {
return nil, fmt.Errorf("parsing field field 'active' failed, value not in map: %s", active)
}
if subCode, ok = subMap[sub]; !ok {
return nil, fmt.Errorf("parsing field field 'sub' failed, value not in map: %s", sub)
}
fields := map[string]interface{}{
"load_code": loadCode,
"active_code": activeCode,
"sub_code": subCode,
}
return []telegraf.Metric{metric.New("systemd_units", tags, fields, time.Now())}, nil
}