Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .chloggen/systemd-receiver.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: new_component

# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
component: receiver/systemd

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Report active state of systemd units.

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [33532]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]
25 changes: 25 additions & 0 deletions receiver/systemdreceiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
| Status | |
| ------------- |-----------|
| Stability | [development]: metrics |
| Unsupported Platforms | darwin, windows |
| Distributions | [] |
| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Areceiver%2Fsystemd%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Areceiver%2Fsystemd) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Areceiver%2Fsystemd%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Areceiver%2Fsystemd) |
| Code coverage | [![codecov](https://codecov.io/github/open-telemetry/opentelemetry-collector-contrib/graph/main/badge.svg?component=receiver_systemd)](https://app.codecov.io/gh/open-telemetry/opentelemetry-collector-contrib/tree/main/?components%5B0%5D=receiver_systemd&displayType=list) |
Expand All @@ -13,3 +14,27 @@
[development]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#development
<!-- end autogenerated section -->

The systemd receiver gathers metrics for locally running systemd units.

This scraper generates a metric with a label for service state with a value of `1` if the unit is in that state. For
example, the following metrics will be generated if the `nginx` service is currently active:

```
systemd.unit.state{systemd.unit.name="nginx", systemd.unit.active_state="active"} = 1
systemd.unit.state{systemd.unit.name="nginx", systemd.unit.active_state="reloading"} = 0
systemd.unit.state{systemd.unit.name="nginx", systemd.unit.active_state="inactive"} = 0
systemd.unit.state{systemd.unit.name="nginx", systemd.unit.active_state="failed"} = 0
systemd.unit.state{systemd.unit.name="nginx", systemd.unit.active_state="activating"} = 0
systemd.unit.state{systemd.unit.name="nginx", systemd.unit.active_state="deactivating"} = 0
systemd.unit.state{systemd.unit.name="nginx", systemd.unit.active_state="maintenance"} = 0
systemd.unit.state{systemd.unit.name="nginx", systemd.unit.active_state="refreshing"} = 0
```

## Configuration

| Field | Default | Description |
|---------| -----------------| ---------------------------------------------------------------------|
| `scope` | `system` | The service manager to gather units from, either `system` or `user`. |
| `units` | `["*.service"]` | The units to scrape, as a list of [patterns]. |

[patterns]: https://www.freedesktop.org/software/systemd/man/latest/systemctl.html#Parameter%20Syntax
37 changes: 36 additions & 1 deletion receiver/systemdreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,39 @@

package systemdreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver"

type Config struct{}
import (
"errors"

"go.opentelemetry.io/collector/scraper/scraperhelper"
"go.uber.org/multierr"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver/internal/metadata"
)

var (
errInvalidScope = errors.New(`"scope" must be one of "systemd" or "user"`)
errNoUnits = errors.New("no units configured")
)

type Config struct {
scraperhelper.ControllerConfig `mapstructure:",squash"`
metadata.MetricsBuilderConfig `mapstructure:",squash"`

Scope string `mapstructure:"scope"`
Units []string `mapstructure:"units"`
}

func (c Config) Validate() error {
var err error

// Ensure we have a valid scope.
if c.Scope != "system" && c.Scope != "user" {
err = multierr.Append(err, errInvalidScope)
}

if len(c.Units) == 0 {
err = multierr.Append(err, errNoUnits)
}

return err
}
54 changes: 54 additions & 0 deletions receiver/systemdreceiver/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package systemdreceiver

import (
"testing"

"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/scraper/scraperhelper"
"go.uber.org/multierr"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver/internal/metadata"
)

func TestValidate(t *testing.T) {
testCases := []struct {
desc string
cfg *Config
expectedErr error
}{
{
desc: "invalid scope",
cfg: &Config{
Scope: "unknown",
Units: []string{"*.service"},
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(),
},
expectedErr: multierr.Combine(errInvalidScope),
},
{
desc: "no units",
cfg: &Config{
Scope: "system",
Units: []string{},
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(),
},
expectedErr: multierr.Combine(errNoUnits),
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
actualErr := tc.cfg.Validate()
if tc.expectedErr != nil {
require.EqualError(t, actualErr, tc.expectedErr.Error())
} else {
require.NoError(t, actualErr)
}
})
}
}
33 changes: 33 additions & 0 deletions receiver/systemdreceiver/documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[comment]: <> (Code generated by mdatagen. DO NOT EDIT.)

# systemd

## Default Metrics

The following metrics are emitted by default. Each of them can be disabled by applying the following configuration:

```yaml
metrics:
<metric_name>:
enabled: false
```

### systemd.unit.state

1 if the check resulted in active_state matching the current state, otherwise 0.

| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic |
| ---- | ----------- | ---------- | ----------------------- | --------- |
| 1 | Sum | Int | Cumulative | false |

#### Attributes

| Name | Description | Values | Requirement Level |
| ---- | ----------- | ------ | -------- |
| systemd.unit.active_state | The active state of the unit (https://www.freedesktop.org/software/systemd/man/latest/systemd.html#Units) | Str: ``active``, ``reloading``, ``inactive``, ``failed``, ``activating``, ``deactivating``, ``maintenance``, ``refreshing`` | Recommended |

## Resource Attributes

| Name | Description | Values | Enabled |
| ---- | ----------- | ------ | ------- |
| systemd.unit.name | Name of the systemd unit | Any Str | true |
44 changes: 36 additions & 8 deletions receiver/systemdreceiver/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@ package systemdreceiver // import "github.com/open-telemetry/opentelemetry-colle

import (
"context"
"errors"
"runtime"
"time"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/consumer"
"go.opentelemetry.io/collector/receiver"
"go.opentelemetry.io/collector/scraper"
"go.opentelemetry.io/collector/scraper/scraperhelper"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver/internal/metadata"
)

var (
errConfigNotValid = errors.New("config not valid for the systemd receiver")
errNonLinux = errors.New("systemd receiver is only supported on Linux")
)

// NewFactory creates a factory for systemd receiver.
func NewFactory() receiver.Factory {
return receiver.NewFactory(
Expand All @@ -22,14 +32,32 @@ func NewFactory() receiver.Factory {
}

func createDefaultConfig() component.Config {
return &Config{}
cfg := scraperhelper.NewDefaultControllerConfig()
cfg.CollectionInterval = 60 * time.Second

return &Config{
ControllerConfig: cfg,
MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(),
Scope: "system",
Units: []string{"*.service"},
}
}

func createMetricsReceiver(
_ context.Context,
_ receiver.Settings,
_ component.Config,
_ consumer.Metrics,
) (receiver.Metrics, error) {
return systemdReceiver{}, nil
func createMetricsReceiver(_ context.Context, params receiver.Settings, rConf component.Config, consumer consumer.Metrics) (receiver.Metrics, error) {
if runtime.GOOS != "linux" {
return nil, errNonLinux
}

cfg, ok := rConf.(*Config)
if !ok {
return nil, errConfigNotValid
}

systemdScraper := newScraper(cfg, params)
s, err := scraper.NewMetrics(systemdScraper.scrape, scraper.WithStart(systemdScraper.start), scraper.WithShutdown(systemdScraper.shutdown))
if err != nil {
return nil, err
}

return scraperhelper.NewMetricsController(&cfg.ControllerConfig, params, consumer, scraperhelper.AddScraper(metadata.Type, s))
}
43 changes: 43 additions & 0 deletions receiver/systemdreceiver/factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package systemdreceiver

import (
"runtime"
"testing"

"github.com/stretchr/testify/assert"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/receiver/receivertest"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver/internal/metadata"
)

func TestCreateDefaultConfig(t *testing.T) {
factory := NewFactory()
cfg := factory.CreateDefaultConfig()
assert.IsType(t, &Config{}, cfg)
assert.NotNil(t, cfg, "failed to create default config")
assert.NoError(t, componenttest.CheckConfigStruct(cfg))
}

func TestCreateMetrics(t *testing.T) {
factory := NewFactory()

scraper, err := factory.CreateMetrics(
t.Context(),
receivertest.NewNopSettings(metadata.Type),
factory.CreateDefaultConfig(),
nil,
)

if runtime.GOOS == "linux" {
assert.NoError(t, err)
assert.NotNil(t, scraper)
} else {
assert.Error(t, err)
assert.Equal(t, errNonLinux, err)
assert.Nil(t, scraper)
}
}
1 change: 1 addition & 0 deletions receiver/systemdreceiver/generated_component_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 19 additions & 3 deletions receiver/systemdreceiver/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,29 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/receiver/system
go 1.24.0

require (
github.com/godbus/dbus/v5 v5.1.0
github.com/google/go-cmp v0.7.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.138.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.133.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/collector/component v1.44.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/component/componenttest v0.138.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/confmap v1.44.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/consumer v1.44.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/consumer/consumertest v0.138.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/filter v0.138.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/pdata v1.44.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/receiver v1.44.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/receiver/receivertest v0.138.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/scraper v0.138.1-0.20251021231522-c657d5d4e920
go.opentelemetry.io/collector/scraper/scraperhelper v0.138.1-0.20251021231522-c657d5d4e920
go.uber.org/goleak v1.3.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
)

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand All @@ -31,15 +42,16 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.138.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.138.1-0.20251021231522-c657d5d4e920 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.138.1-0.20251021231522-c657d5d4e920 // indirect
go.opentelemetry.io/collector/featuregate v1.44.1-0.20251021231522-c657d5d4e920 // indirect
go.opentelemetry.io/collector/internal/telemetry v0.138.1-0.20251021231522-c657d5d4e920 // indirect
go.opentelemetry.io/collector/pdata v1.44.1-0.20251021231522-c657d5d4e920 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.138.1-0.20251021231522-c657d5d4e920 // indirect
go.opentelemetry.io/collector/pipeline v1.44.1-0.20251021231522-c657d5d4e920 // indirect
go.opentelemetry.io/collector/receiver/receiverhelper v0.138.1-0.20251021231522-c657d5d4e920 // indirect
go.opentelemetry.io/collector/receiver/xreceiver v0.138.1-0.20251021231522-c657d5d4e920 // indirect
go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
Expand All @@ -48,8 +60,6 @@ require (
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.35.0 // indirect
Expand All @@ -65,3 +75,9 @@ retract (
v0.76.1
v0.65.0
)

replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest => ../../pkg/pdatatest

replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => ../../pkg/golden

replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil => ../../pkg/pdatautil
Loading