From 5616828c4c757d493bf372642a5a513a95b1ada3 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 27 Aug 2025 07:30:26 +0100 Subject: [PATCH 1/7] [systemdreceiver] Scraping unit status --- .chloggen/systemd-receiver.yaml | 27 ++ receiver/systemdreceiver/README.md | 25 ++ receiver/systemdreceiver/config.go | 37 ++- receiver/systemdreceiver/config_test.go | 54 ++++ receiver/systemdreceiver/documentation.md | 28 ++ receiver/systemdreceiver/factory.go | 44 ++- receiver/systemdreceiver/factory_test.go | 43 +++ .../generated_component_test.go | 1 + receiver/systemdreceiver/go.mod | 21 +- receiver/systemdreceiver/go.sum | 10 + .../internal/metadata/generated_config.go | 50 ++++ .../metadata/generated_config_test.go | 60 ++++ .../internal/metadata/generated_metrics.go | 266 ++++++++++++++++++ .../metadata/generated_metrics_test.go | 112 ++++++++ .../internal/metadata/testdata/config.yaml | 9 + receiver/systemdreceiver/metadata.yaml | 31 ++ receiver/systemdreceiver/receiver.go | 19 -- receiver/systemdreceiver/scraper.go | 103 +++++++ receiver/systemdreceiver/scraper_test.go | 188 +++++++++++++ .../expected_metrics/nginx-active.yaml | 93 ++++++ .../expected_metrics/nginx-failed.yaml | 93 ++++++ 21 files changed, 1283 insertions(+), 31 deletions(-) create mode 100644 .chloggen/systemd-receiver.yaml create mode 100644 receiver/systemdreceiver/config_test.go create mode 100644 receiver/systemdreceiver/documentation.md create mode 100644 receiver/systemdreceiver/factory_test.go create mode 100644 receiver/systemdreceiver/internal/metadata/generated_config.go create mode 100644 receiver/systemdreceiver/internal/metadata/generated_config_test.go create mode 100644 receiver/systemdreceiver/internal/metadata/generated_metrics.go create mode 100644 receiver/systemdreceiver/internal/metadata/generated_metrics_test.go create mode 100644 receiver/systemdreceiver/internal/metadata/testdata/config.yaml delete mode 100644 receiver/systemdreceiver/receiver.go create mode 100644 receiver/systemdreceiver/scraper.go create mode 100644 receiver/systemdreceiver/scraper_test.go create mode 100644 receiver/systemdreceiver/testdata/expected_metrics/nginx-active.yaml create mode 100644 receiver/systemdreceiver/testdata/expected_metrics/nginx-failed.yaml diff --git a/.chloggen/systemd-receiver.yaml b/.chloggen/systemd-receiver.yaml new file mode 100644 index 0000000000000..231ee2c7398af --- /dev/null +++ b/.chloggen/systemd-receiver.yaml @@ -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. filelogreceiver) +component: systemdreceiver + +# 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] diff --git a/receiver/systemdreceiver/README.md b/receiver/systemdreceiver/README.md index 3b8f319aea5f9..78ba538f87fb2 100644 --- a/receiver/systemdreceiver/README.md +++ b/receiver/systemdreceiver/README.md @@ -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) | @@ -13,3 +14,27 @@ [development]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#development +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 diff --git a/receiver/systemdreceiver/config.go b/receiver/systemdreceiver/config.go index 999c1fbc54f6c..aea831c764aa3 100644 --- a/receiver/systemdreceiver/config.go +++ b/receiver/systemdreceiver/config.go @@ -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 +} diff --git a/receiver/systemdreceiver/config_test.go b/receiver/systemdreceiver/config_test.go new file mode 100644 index 0000000000000..a8df0c2433619 --- /dev/null +++ b/receiver/systemdreceiver/config_test.go @@ -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) + } + }) + } +} diff --git a/receiver/systemdreceiver/documentation.md b/receiver/systemdreceiver/documentation.md new file mode 100644 index 0000000000000..e3f6e144b9ca5 --- /dev/null +++ b/receiver/systemdreceiver/documentation.md @@ -0,0 +1,28 @@ +[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: + : + 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 | Optional | +| ---- | ----------- | ------ | -------- | +| systemd.unit.name | Name of the systemd unit | Any Str | false | +| 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`` | false | diff --git a/receiver/systemdreceiver/factory.go b/receiver/systemdreceiver/factory.go index 0891b5652978c..fc64ddac3c1b3 100644 --- a/receiver/systemdreceiver/factory.go +++ b/receiver/systemdreceiver/factory.go @@ -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( @@ -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)) } diff --git a/receiver/systemdreceiver/factory_test.go b/receiver/systemdreceiver/factory_test.go new file mode 100644 index 0000000000000..4550a02bfc4f7 --- /dev/null +++ b/receiver/systemdreceiver/factory_test.go @@ -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) + } +} diff --git a/receiver/systemdreceiver/generated_component_test.go b/receiver/systemdreceiver/generated_component_test.go index a0c96f6bb1930..a09894dcb7201 100644 --- a/receiver/systemdreceiver/generated_component_test.go +++ b/receiver/systemdreceiver/generated_component_test.go @@ -1,4 +1,5 @@ // Code generated by mdatagen. DO NOT EDIT. +//go:build !darwin && !windows package systemdreceiver diff --git a/receiver/systemdreceiver/go.mod b/receiver/systemdreceiver/go.mod index ac5290d71f1ff..091ac52e8bed4 100644 --- a/receiver/systemdreceiver/go.mod +++ b/receiver/systemdreceiver/go.mod @@ -3,18 +3,28 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/receiver/system go 1.24 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.133.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.133.0 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/collector/component v1.39.0 go.opentelemetry.io/collector/component/componenttest v0.133.0 go.opentelemetry.io/collector/confmap v1.39.0 go.opentelemetry.io/collector/consumer v1.39.0 go.opentelemetry.io/collector/consumer/consumertest v0.133.0 + go.opentelemetry.io/collector/pdata v1.39.0 go.opentelemetry.io/collector/receiver v1.39.0 go.opentelemetry.io/collector/receiver/receivertest v0.133.0 + go.opentelemetry.io/collector/scraper v0.133.0 + go.opentelemetry.io/collector/scraper/scraperhelper v0.133.0 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 @@ -31,15 +41,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.133.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.133.0 // indirect go.opentelemetry.io/collector/consumer/xconsumer v0.133.0 // indirect go.opentelemetry.io/collector/featuregate v1.39.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.133.0 // indirect - go.opentelemetry.io/collector/pdata v1.39.0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.133.0 // indirect go.opentelemetry.io/collector/pipeline v1.39.0 // indirect + go.opentelemetry.io/collector/receiver/receiverhelper v0.133.0 // indirect go.opentelemetry.io/collector/receiver/xreceiver v0.133.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.12.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect @@ -48,8 +59,6 @@ require ( go.opentelemetry.io/otel/sdk v1.37.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.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.41.0 // indirect golang.org/x/sys v0.33.0 // indirect @@ -65,3 +74,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 diff --git a/receiver/systemdreceiver/go.sum b/receiver/systemdreceiver/go.sum index 40dfee2cab958..90f2c734c70a9 100644 --- a/receiver/systemdreceiver/go.sum +++ b/receiver/systemdreceiver/go.sum @@ -1,3 +1,5 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -10,6 +12,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -85,10 +89,16 @@ go.opentelemetry.io/collector/pipeline v1.39.0 h1:CcEn30qdoHEzehFxgx0Ma0pWYGhrrI go.opentelemetry.io/collector/pipeline v1.39.0/go.mod h1:NdM+ZqkPe9KahtOXG28RHTRQu4m/FD1i3Ew4qCRdOr8= go.opentelemetry.io/collector/receiver v1.39.0 h1:RdZn4v9wUa4QVu3+5zJcdM3BJeFM1l8hO/eZmNxKkBA= go.opentelemetry.io/collector/receiver v1.39.0/go.mod h1:wKHijIb17Dsj02z2j8JahvAn9ANEe6itosIHZlwu9bc= +go.opentelemetry.io/collector/receiver/receiverhelper v0.133.0 h1:L/Ky4LEGMTL3dRkXJoQWSv48s6zvik99uy5FeEQfCC8= +go.opentelemetry.io/collector/receiver/receiverhelper v0.133.0/go.mod h1:xsWABdnQRHUWhd5l4H1wEuaxotGxjqM4044ej4vRC78= go.opentelemetry.io/collector/receiver/receivertest v0.133.0 h1:WRwXNWO3pQikr30G86kUyvR9JXu7holcNrk6g9rFNTQ= go.opentelemetry.io/collector/receiver/receivertest v0.133.0/go.mod h1:bvcaf7Z2FvPOm/dBlW0CBEReVdtrdgMUg4JOLJ50NEY= go.opentelemetry.io/collector/receiver/xreceiver v0.133.0 h1:AvgzAg5u90TJ7+taSyZ5mSnQn4GrrV1qHbrx+AXD1X0= go.opentelemetry.io/collector/receiver/xreceiver v0.133.0/go.mod h1:ZqAFQ2Ew/ftQGvbEvftITh0IheQD300A0HsuCB5Qgdk= +go.opentelemetry.io/collector/scraper v0.133.0 h1:1IZhGkaer+RcCNGLfmJbinDMfPlZ2mGcD7WTrJIgOQ4= +go.opentelemetry.io/collector/scraper v0.133.0/go.mod h1:oODdsXeUcNQkuH8r7j75PRT4u9p0h+GeByWF3TWOjs4= +go.opentelemetry.io/collector/scraper/scraperhelper v0.133.0 h1:34tjdWUGAMu3dhjYobUCnhFwL2F3JuRn81jFmwU/7GE= +go.opentelemetry.io/collector/scraper/scraperhelper v0.133.0/go.mod h1:FIuG2QeE12c5xPxmm2n4hSKA4/atGTD5zp2rNudXfC0= go.opentelemetry.io/contrib/bridges/otelzap v0.12.0 h1:FGre0nZh5BSw7G73VpT3xs38HchsfPsa2aZtMp0NPOs= go.opentelemetry.io/contrib/bridges/otelzap v0.12.0/go.mod h1:X2PYPViI2wTPIMIOBjG17KNybTzsrATnvPJ02kkz7LM= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= diff --git a/receiver/systemdreceiver/internal/metadata/generated_config.go b/receiver/systemdreceiver/internal/metadata/generated_config.go new file mode 100644 index 0000000000000..04a93ba56771b --- /dev/null +++ b/receiver/systemdreceiver/internal/metadata/generated_config.go @@ -0,0 +1,50 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/confmap" +) + +// MetricConfig provides common config for a particular metric. +type MetricConfig struct { + Enabled bool `mapstructure:"enabled"` + + enabledSetByUser bool +} + +func (ms *MetricConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(ms) + if err != nil { + return err + } + ms.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// MetricsConfig provides config for systemd metrics. +type MetricsConfig struct { + SystemdUnitState MetricConfig `mapstructure:"systemd.unit.state"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + SystemdUnitState: MetricConfig{ + Enabled: true, + }, + } +} + +// MetricsBuilderConfig is a configuration for systemd metrics builder. +type MetricsBuilderConfig struct { + Metrics MetricsConfig `mapstructure:"metrics"` +} + +func DefaultMetricsBuilderConfig() MetricsBuilderConfig { + return MetricsBuilderConfig{ + Metrics: DefaultMetricsConfig(), + } +} diff --git a/receiver/systemdreceiver/internal/metadata/generated_config_test.go b/receiver/systemdreceiver/internal/metadata/generated_config_test.go new file mode 100644 index 0000000000000..1f8d8d18911e5 --- /dev/null +++ b/receiver/systemdreceiver/internal/metadata/generated_config_test.go @@ -0,0 +1,60 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +func TestMetricsBuilderConfig(t *testing.T) { + tests := []struct { + name string + want MetricsBuilderConfig + }{ + { + name: "default", + want: DefaultMetricsBuilderConfig(), + }, + { + name: "all_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + SystemdUnitState: MetricConfig{Enabled: true}, + }, + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + SystemdUnitState: MetricConfig{Enabled: false}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadMetricsBuilderConfig(t, tt.name) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{})) + require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) + }) + } +} + +func loadMetricsBuilderConfig(t *testing.T, name string) MetricsBuilderConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + cfg := DefaultMetricsBuilderConfig() + require.NoError(t, sub.Unmarshal(&cfg, confmap.WithIgnoreUnused())) + return cfg +} diff --git a/receiver/systemdreceiver/internal/metadata/generated_metrics.go b/receiver/systemdreceiver/internal/metadata/generated_metrics.go new file mode 100644 index 0000000000000..14dfc3b462eac --- /dev/null +++ b/receiver/systemdreceiver/internal/metadata/generated_metrics.go @@ -0,0 +1,266 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver" +) + +// AttributeSystemdUnitActiveState specifies the value systemd.unit.active_state attribute. +type AttributeSystemdUnitActiveState int + +const ( + _ AttributeSystemdUnitActiveState = iota + AttributeSystemdUnitActiveStateActive + AttributeSystemdUnitActiveStateReloading + AttributeSystemdUnitActiveStateInactive + AttributeSystemdUnitActiveStateFailed + AttributeSystemdUnitActiveStateActivating + AttributeSystemdUnitActiveStateDeactivating + AttributeSystemdUnitActiveStateMaintenance + AttributeSystemdUnitActiveStateRefreshing +) + +// String returns the string representation of the AttributeSystemdUnitActiveState. +func (av AttributeSystemdUnitActiveState) String() string { + switch av { + case AttributeSystemdUnitActiveStateActive: + return "active" + case AttributeSystemdUnitActiveStateReloading: + return "reloading" + case AttributeSystemdUnitActiveStateInactive: + return "inactive" + case AttributeSystemdUnitActiveStateFailed: + return "failed" + case AttributeSystemdUnitActiveStateActivating: + return "activating" + case AttributeSystemdUnitActiveStateDeactivating: + return "deactivating" + case AttributeSystemdUnitActiveStateMaintenance: + return "maintenance" + case AttributeSystemdUnitActiveStateRefreshing: + return "refreshing" + } + return "" +} + +// MapAttributeSystemdUnitActiveState is a helper map of string to AttributeSystemdUnitActiveState attribute value. +var MapAttributeSystemdUnitActiveState = map[string]AttributeSystemdUnitActiveState{ + "active": AttributeSystemdUnitActiveStateActive, + "reloading": AttributeSystemdUnitActiveStateReloading, + "inactive": AttributeSystemdUnitActiveStateInactive, + "failed": AttributeSystemdUnitActiveStateFailed, + "activating": AttributeSystemdUnitActiveStateActivating, + "deactivating": AttributeSystemdUnitActiveStateDeactivating, + "maintenance": AttributeSystemdUnitActiveStateMaintenance, + "refreshing": AttributeSystemdUnitActiveStateRefreshing, +} + +var MetricsInfo = metricsInfo{ + SystemdUnitState: metricInfo{ + Name: "systemd.unit.state", + }, +} + +type metricsInfo struct { + SystemdUnitState metricInfo +} + +type metricInfo struct { + Name string +} + +type metricSystemdUnitState struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills systemd.unit.state metric with initial data. +func (m *metricSystemdUnitState) init() { + m.data.SetName("systemd.unit.state") + m.data.SetDescription("1 if the check resulted in active_state matching the current state, otherwise 0.") + m.data.SetUnit("1") + m.data.SetEmptySum() + m.data.Sum().SetIsMonotonic(false) + m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) + m.data.Sum().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricSystemdUnitState) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, systemdUnitNameAttributeValue string, systemdUnitActiveStateAttributeValue string) { + if !m.config.Enabled { + return + } + dp := m.data.Sum().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) + dp.Attributes().PutStr("systemd.unit.name", systemdUnitNameAttributeValue) + dp.Attributes().PutStr("systemd.unit.active_state", systemdUnitActiveStateAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricSystemdUnitState) updateCapacity() { + if m.data.Sum().DataPoints().Len() > m.capacity { + m.capacity = m.data.Sum().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricSystemdUnitState) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Sum().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricSystemdUnitState(cfg MetricConfig) metricSystemdUnitState { + m := metricSystemdUnitState{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +// MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations +// required to produce metric representation defined in metadata and user config. +type MetricsBuilder struct { + config MetricsBuilderConfig // config of the metrics builder. + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information. + metricSystemdUnitState metricSystemdUnitState +} + +// MetricBuilderOption applies changes to default metrics builder. +type MetricBuilderOption interface { + apply(*MetricsBuilder) +} + +type metricBuilderOptionFunc func(mb *MetricsBuilder) + +func (mbof metricBuilderOptionFunc) apply(mb *MetricsBuilder) { + mbof(mb) +} + +// WithStartTime sets startTime on the metrics builder. +func WithStartTime(startTime pcommon.Timestamp) MetricBuilderOption { + return metricBuilderOptionFunc(func(mb *MetricsBuilder) { + mb.startTime = startTime + }) +} +func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, options ...MetricBuilderOption) *MetricsBuilder { + mb := &MetricsBuilder{ + config: mbc, + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + metricSystemdUnitState: newMetricSystemdUnitState(mbc.Metrics.SystemdUnitState), + } + + for _, op := range options { + op.apply(mb) + } + return mb +} + +// updateCapacity updates max length of metrics and resource attributes that will be used for the slice capacity. +func (mb *MetricsBuilder) updateCapacity(rm pmetric.ResourceMetrics) { + if mb.metricsCapacity < rm.ScopeMetrics().At(0).Metrics().Len() { + mb.metricsCapacity = rm.ScopeMetrics().At(0).Metrics().Len() + } +} + +// ResourceMetricsOption applies changes to provided resource metrics. +type ResourceMetricsOption interface { + apply(pmetric.ResourceMetrics) +} + +type resourceMetricsOptionFunc func(pmetric.ResourceMetrics) + +func (rmof resourceMetricsOptionFunc) apply(rm pmetric.ResourceMetrics) { + rmof(rm) +} + +// WithResource sets the provided resource on the emitted ResourceMetrics. +// It's recommended to use ResourceBuilder to create the resource. +func WithResource(res pcommon.Resource) ResourceMetricsOption { + return resourceMetricsOptionFunc(func(rm pmetric.ResourceMetrics) { + res.CopyTo(rm.Resource()) + }) +} + +// WithStartTimeOverride overrides start time for all the resource metrics data points. +// This option should be only used if different start time has to be set on metrics coming from different resources. +func WithStartTimeOverride(start pcommon.Timestamp) ResourceMetricsOption { + return resourceMetricsOptionFunc(func(rm pmetric.ResourceMetrics) { + var dps pmetric.NumberDataPointSlice + metrics := rm.ScopeMetrics().At(0).Metrics() + for i := 0; i < metrics.Len(); i++ { + switch metrics.At(i).Type() { + case pmetric.MetricTypeGauge: + dps = metrics.At(i).Gauge().DataPoints() + case pmetric.MetricTypeSum: + dps = metrics.At(i).Sum().DataPoints() + } + for j := 0; j < dps.Len(); j++ { + dps.At(j).SetStartTimestamp(start) + } + } + }) +} + +// EmitForResource saves all the generated metrics under a new resource and updates the internal state to be ready for +// recording another set of data points as part of another resource. This function can be helpful when one scraper +// needs to emit metrics from several resources. Otherwise calling this function is not required, +// just `Emit` function can be called instead. +// Resource attributes should be provided as ResourceMetricsOption arguments. +func (mb *MetricsBuilder) EmitForResource(options ...ResourceMetricsOption) { + rm := pmetric.NewResourceMetrics() + ils := rm.ScopeMetrics().AppendEmpty() + ils.Scope().SetName(ScopeName) + ils.Scope().SetVersion(mb.buildInfo.Version) + ils.Metrics().EnsureCapacity(mb.metricsCapacity) + mb.metricSystemdUnitState.emit(ils.Metrics()) + + for _, op := range options { + op.apply(rm) + } + + if ils.Metrics().Len() > 0 { + mb.updateCapacity(rm) + rm.MoveTo(mb.metricsBuffer.ResourceMetrics().AppendEmpty()) + } +} + +// Emit returns all the metrics accumulated by the metrics builder and updates the internal state to be ready for +// recording another set of metrics. This function will be responsible for applying all the transformations required to +// produce metric representation defined in metadata and user config, e.g. delta or cumulative. +func (mb *MetricsBuilder) Emit(options ...ResourceMetricsOption) pmetric.Metrics { + mb.EmitForResource(options...) + metrics := mb.metricsBuffer + mb.metricsBuffer = pmetric.NewMetrics() + return metrics +} + +// RecordSystemdUnitStateDataPoint adds a data point to systemd.unit.state metric. +func (mb *MetricsBuilder) RecordSystemdUnitStateDataPoint(ts pcommon.Timestamp, val int64, systemdUnitNameAttributeValue string, systemdUnitActiveStateAttributeValue AttributeSystemdUnitActiveState) { + mb.metricSystemdUnitState.recordDataPoint(mb.startTime, ts, val, systemdUnitNameAttributeValue, systemdUnitActiveStateAttributeValue.String()) +} + +// Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, +// and metrics builder should update its startTime and reset it's internal state accordingly. +func (mb *MetricsBuilder) Reset(options ...MetricBuilderOption) { + mb.startTime = pcommon.NewTimestampFromTime(time.Now()) + for _, op := range options { + op.apply(mb) + } +} diff --git a/receiver/systemdreceiver/internal/metadata/generated_metrics_test.go b/receiver/systemdreceiver/internal/metadata/generated_metrics_test.go new file mode 100644 index 0000000000000..2b51e7f9a3507 --- /dev/null +++ b/receiver/systemdreceiver/internal/metadata/generated_metrics_test.go @@ -0,0 +1,112 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver/receivertest" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" +) + +type testDataSet int + +const ( + testDataSetDefault testDataSet = iota + testDataSetAll + testDataSetNone +) + +func TestMetricsBuilder(t *testing.T) { + tests := []struct { + name string + metricsSet testDataSet + resAttrsSet testDataSet + expectEmpty bool + }{ + { + name: "default", + }, + { + name: "all_set", + metricsSet: testDataSetAll, + resAttrsSet: testDataSetAll, + }, + { + name: "none_set", + metricsSet: testDataSetNone, + resAttrsSet: testDataSetNone, + expectEmpty: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start := pcommon.Timestamp(1_000_000_000) + ts := pcommon.Timestamp(1_000_001_000) + observedZapCore, observedLogs := observer.New(zap.WarnLevel) + settings := receivertest.NewNopSettings(receivertest.NopType) + settings.Logger = zap.New(observedZapCore) + mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, tt.name), settings, WithStartTime(start)) + + expectedWarnings := 0 + + assert.Equal(t, expectedWarnings, observedLogs.Len()) + + defaultMetricsCount := 0 + allMetricsCount := 0 + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordSystemdUnitStateDataPoint(ts, 1, "systemd.unit.name-val", AttributeSystemdUnitActiveStateActive) + + res := pcommon.NewResource() + metrics := mb.Emit(WithResource(res)) + + if tt.expectEmpty { + assert.Equal(t, 0, metrics.ResourceMetrics().Len()) + return + } + + assert.Equal(t, 1, metrics.ResourceMetrics().Len()) + rm := metrics.ResourceMetrics().At(0) + assert.Equal(t, res, rm.Resource()) + assert.Equal(t, 1, rm.ScopeMetrics().Len()) + ms := rm.ScopeMetrics().At(0).Metrics() + if tt.metricsSet == testDataSetDefault { + assert.Equal(t, defaultMetricsCount, ms.Len()) + } + if tt.metricsSet == testDataSetAll { + assert.Equal(t, allMetricsCount, ms.Len()) + } + validatedMetrics := make(map[string]bool) + for i := 0; i < ms.Len(); i++ { + switch ms.At(i).Name() { + case "systemd.unit.state": + assert.False(t, validatedMetrics["systemd.unit.state"], "Found a duplicate in the metrics slice: systemd.unit.state") + validatedMetrics["systemd.unit.state"] = true + assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) + assert.Equal(t, "1 if the check resulted in active_state matching the current state, otherwise 0.", ms.At(i).Description()) + assert.Equal(t, "1", ms.At(i).Unit()) + assert.False(t, ms.At(i).Sum().IsMonotonic()) + assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) + dp := ms.At(i).Sum().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + attrVal, ok := dp.Attributes().Get("systemd.unit.name") + assert.True(t, ok) + assert.Equal(t, "systemd.unit.name-val", attrVal.Str()) + attrVal, ok = dp.Attributes().Get("systemd.unit.active_state") + assert.True(t, ok) + assert.Equal(t, "active", attrVal.Str()) + } + } + }) + } +} diff --git a/receiver/systemdreceiver/internal/metadata/testdata/config.yaml b/receiver/systemdreceiver/internal/metadata/testdata/config.yaml new file mode 100644 index 0000000000000..7c056c75a7bbd --- /dev/null +++ b/receiver/systemdreceiver/internal/metadata/testdata/config.yaml @@ -0,0 +1,9 @@ +default: +all_set: + metrics: + systemd.unit.state: + enabled: true +none_set: + metrics: + systemd.unit.state: + enabled: false diff --git a/receiver/systemdreceiver/metadata.yaml b/receiver/systemdreceiver/metadata.yaml index c4fc79a00c908..d2a99854becb0 100644 --- a/receiver/systemdreceiver/metadata.yaml +++ b/receiver/systemdreceiver/metadata.yaml @@ -8,5 +8,36 @@ status: codeowners: active: [atoulme] emeritus: [Hemansh31] + unsupported_platforms: [darwin, windows] + +attributes: + systemd.unit.name: + description: Name of the systemd unit + type: string + enabled: true + + systemd.unit.active_state: + description: The active state of the unit (https://www.freedesktop.org/software/systemd/man/latest/systemd.html#Units) + type: string + enum: + - active + - reloading + - inactive + - failed + - activating + - deactivating + - maintenance + - refreshing + +metrics: + systemd.unit.state: + description: 1 if the check resulted in active_state matching the current state, otherwise 0. + enabled: true + sum: + value_type: int + aggregation_temporality: cumulative + monotonic: false + unit: "1" + attributes: [systemd.unit.name, systemd.unit.active_state] tests: diff --git a/receiver/systemdreceiver/receiver.go b/receiver/systemdreceiver/receiver.go deleted file mode 100644 index 1c121a78b8795..0000000000000 --- a/receiver/systemdreceiver/receiver.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package systemdreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver" -import ( - "context" - - "go.opentelemetry.io/collector/component" -) - -type systemdReceiver struct{} - -func (systemdReceiver) Start(context.Context, component.Host) error { - return nil -} - -func (systemdReceiver) Shutdown(context.Context) error { - return nil -} diff --git a/receiver/systemdreceiver/scraper.go b/receiver/systemdreceiver/scraper.go new file mode 100644 index 0000000000000..2db30aedcfc31 --- /dev/null +++ b/receiver/systemdreceiver/scraper.go @@ -0,0 +1,103 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package systemdreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver" + +import ( + "context" + "time" + + "github.com/godbus/dbus/v5" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/receiver" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver/internal/metadata" +) + +// A connection to dbus. +// +// This has the same interface as [dbus.Conn], but allows us to mock it out for testing. +type dbusConnection interface { + Object(dest string, path dbus.ObjectPath) dbus.BusObject + Close() error +} + +type systemdScraper struct { + conn dbusConnection + cfg *Config + settings component.TelemetrySettings + mb *metadata.MetricsBuilder +} + +func (s *systemdScraper) start(ctx context.Context, _ component.Host) (err error) { + var conn *dbus.Conn + switch s.cfg.Scope { + case "system": + conn, err = dbus.ConnectSystemBus(dbus.WithContext(ctx)) + case "user": + conn, err = dbus.ConnectSessionBus(dbus.WithContext(ctx)) + default: + return errInvalidScope + } + + if err != nil { + return err + } + + s.conn = conn + + return +} + +func (s *systemdScraper) shutdown(_ context.Context) (err error) { + if s.conn != nil { + err = s.conn.Close() + } + + return +} + +type unitTuple struct { + Name string + Description string + LoadState string + ActiveState string + SubState string + Following string + Path dbus.ObjectPath + JobID uint32 + JobType string + JobPath dbus.ObjectPath +} + +func (s *systemdScraper) scrape(ctx context.Context) (pmetric.Metrics, error) { + now := pcommon.NewTimestampFromTime(time.Now()) + + var units []unitTuple + if err := s.conn.Object("org.freedesktop.systemd1", "/org/freedesktop/systemd1").CallWithContext(ctx, "org.freedesktop.systemd1.Manager.ListUnitsByPatterns", 0, []string{}, s.cfg.Units).Store(&units); err != nil { + return pmetric.NewMetrics(), err + } + + for _, unit := range units { + for stateName, state := range metadata.MapAttributeSystemdUnitActiveState { + if unit.ActiveState == stateName { + s.mb.RecordSystemdUnitStateDataPoint(now, int64(1), unit.Name, state) + } else { + s.mb.RecordSystemdUnitStateDataPoint(now, int64(0), unit.Name, state) + } + } + } + + metrics := s.mb.Emit() + return metrics, nil +} + +func newScraper(conf *Config, settings receiver.Settings) *systemdScraper { + return &systemdScraper{ + cfg: conf, + settings: settings.TelemetrySettings, + mb: metadata.NewMetricsBuilder(conf.MetricsBuilderConfig, settings), + } +} diff --git a/receiver/systemdreceiver/scraper_test.go b/receiver/systemdreceiver/scraper_test.go new file mode 100644 index 0000000000000..5f4a01b822324 --- /dev/null +++ b/receiver/systemdreceiver/scraper_test.go @@ -0,0 +1,188 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package systemdreceiver + +import ( + "context" + "errors" + "fmt" + "path/filepath" + "testing" + + "github.com/godbus/dbus/v5" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/receiver/receivertest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/pmetrictest" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver/internal/metadata" +) + +var errUnimplementedMethod = errors.New("unimplemented test method") + +type testDbusObject struct { + destination string + path dbus.ObjectPath + methods map[string][]any + properties map[string]dbus.Variant +} + +func (s *testDbusObject) Call(method string, _ dbus.Flags, _ ...any) *dbus.Call { + if value, exists := s.methods[method]; exists { + return &dbus.Call{Body: value} + } + return &dbus.Call{Err: fmt.Errorf("no such method %s", method)} +} + +func (s *testDbusObject) CallWithContext(_ context.Context, method string, flags dbus.Flags, args ...any) *dbus.Call { + return s.Call(method, flags, args) +} + +func (*testDbusObject) Go(_ string, _ dbus.Flags, _ chan *dbus.Call, _ ...any) *dbus.Call { + return &dbus.Call{Err: errUnimplementedMethod} +} + +func (*testDbusObject) GoWithContext(_ context.Context, _ string, _ dbus.Flags, _ chan *dbus.Call, _ ...any) *dbus.Call { + return &dbus.Call{Err: errUnimplementedMethod} +} + +func (*testDbusObject) AddMatchSignal(_, _ string, _ ...dbus.MatchOption) *dbus.Call { + return &dbus.Call{Err: errUnimplementedMethod} +} + +func (*testDbusObject) RemoveMatchSignal(_, _ string, _ ...dbus.MatchOption) *dbus.Call { + return &dbus.Call{Err: errUnimplementedMethod} +} + +func (s *testDbusObject) GetProperty(p string) (dbus.Variant, error) { + if value, exists := s.properties[p]; exists { + return value, nil + } + + return dbus.Variant{}, errors.New("no such property") +} + +func (*testDbusObject) StoreProperty(_ string, _ any) error { + return errUnimplementedMethod +} + +func (*testDbusObject) SetProperty(_ string, _ any) error { + return errUnimplementedMethod +} + +func (s *testDbusObject) Destination() string { + return s.destination +} + +func (s *testDbusObject) Path() dbus.ObjectPath { + return s.path +} + +type testDbusConnection struct { + isOpen bool + units []unitTuple +} + +func (s *testDbusConnection) Close() error { + if !s.isOpen { + return errors.New("connection already closed") + } + + s.isOpen = false + return nil +} + +func (s *testDbusConnection) Object(dest string, path dbus.ObjectPath) dbus.BusObject { + if dest == "org.freedesktop.systemd1" && path == "/org/freedesktop/systemd1" { + return &testDbusObject{ + destination: dest, + path: path, + methods: map[string][]any{ + "org.freedesktop.systemd1.Manager.ListUnitsByPatterns": {s.units}, + }, + } + } + + panic("unsupported object") +} + +func newTestScraper(conf *Config, units []unitTuple) *systemdScraper { + scraper := newScraper(conf, receivertest.NewNopSettings(metadata.Type)) + scraper.conn = &testDbusConnection{isOpen: true, units: units} + return scraper +} + +func TestScraperScrape(t *testing.T) { + testCases := []struct { + desc string + units []unitTuple + goldenName string + expectedErr error + }{ + { + desc: "Active service", + units: []unitTuple{ + { + Name: "nginx.service", + Description: "A high performance web server and a reverse proxy server", + LoadState: "loaded", + ActiveState: "active", + SubState: "plugged", + Following: "", + Path: "/org/freedesktop/systemd1/unit/nginx_2eservice", + JobID: uint32(0), + JobType: "", + JobPath: "/", + }, + }, + goldenName: "nginx-active", + expectedErr: nil, + }, + + { + desc: "Failed service", + units: []unitTuple{ + { + Name: "nginx.service", + Description: "A high performance web server and a reverse proxy server", + LoadState: "loaded", + ActiveState: "failed", + SubState: "failed", + Following: "", + Path: "/org/freedesktop/systemd1/unit/nginx_2eservice", + JobID: uint32(0), + JobType: "", + JobPath: "/", + }, + }, + goldenName: "nginx-failed", + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + scraper := newTestScraper(createDefaultConfig().(*Config), tc.units) + + actualMetrics, err := scraper.scrape(t.Context()) + if tc.expectedErr == nil { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr.Error()) + } + + goldenPath := filepath.Join("testdata", "expected_metrics", tc.goldenName+".yaml") + // golden.WriteMetrics(t, goldenPath, actualMetrics) + + expectedMetrics, err := golden.ReadMetrics(goldenPath) + require.NoError(t, err) + require.NoError(t, pmetrictest.CompareMetrics( + expectedMetrics, actualMetrics, + pmetrictest.IgnoreMetricDataPointsOrder(), + pmetrictest.IgnoreStartTimestamp(), + pmetrictest.IgnoreTimestamp(), + )) + }) + } +} diff --git a/receiver/systemdreceiver/testdata/expected_metrics/nginx-active.yaml b/receiver/systemdreceiver/testdata/expected_metrics/nginx-active.yaml new file mode 100644 index 0000000000000..4d384f30e7df1 --- /dev/null +++ b/receiver/systemdreceiver/testdata/expected_metrics/nginx-active.yaml @@ -0,0 +1,93 @@ +resourceMetrics: + - resource: {} + scopeMetrics: + - metrics: + - description: 1 if the check resulted in active_state matching the current state, otherwise 0. + name: systemd.unit.state + sum: + aggregationTemporality: 2 + dataPoints: + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: activating + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "1" + attributes: + - key: systemd.unit.active_state + value: + stringValue: active + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: deactivating + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: failed + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: inactive + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: maintenance + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: refreshing + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: reloading + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + unit: "1" + scope: + name: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver + version: latest diff --git a/receiver/systemdreceiver/testdata/expected_metrics/nginx-failed.yaml b/receiver/systemdreceiver/testdata/expected_metrics/nginx-failed.yaml new file mode 100644 index 0000000000000..c0ae98b2c1f89 --- /dev/null +++ b/receiver/systemdreceiver/testdata/expected_metrics/nginx-failed.yaml @@ -0,0 +1,93 @@ +resourceMetrics: + - resource: {} + scopeMetrics: + - metrics: + - description: 1 if the check resulted in active_state matching the current state, otherwise 0. + name: systemd.unit.state + sum: + aggregationTemporality: 2 + dataPoints: + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: activating + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: active + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: deactivating + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "1" + attributes: + - key: systemd.unit.active_state + value: + stringValue: failed + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: inactive + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: maintenance + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: refreshing + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: reloading + - key: systemd.unit.name + value: + stringValue: nginx.service + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + unit: "1" + scope: + name: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver + version: latest From ba45aaa8039d31f5d73cb45662cedde52ed04914 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 27 Aug 2025 07:41:19 +0100 Subject: [PATCH 2/7] [systemdreceiver] Fix go.mod --- receiver/systemdreceiver/go.mod | 1 + receiver/systemdreceiver/go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/receiver/systemdreceiver/go.mod b/receiver/systemdreceiver/go.mod index 091ac52e8bed4..e8ae22dcd1ecf 100644 --- a/receiver/systemdreceiver/go.mod +++ b/receiver/systemdreceiver/go.mod @@ -47,6 +47,7 @@ require ( go.opentelemetry.io/collector/consumer/consumererror v0.133.0 // indirect go.opentelemetry.io/collector/consumer/xconsumer v0.133.0 // indirect go.opentelemetry.io/collector/featuregate v1.39.0 // indirect + go.opentelemetry.io/collector/filter v0.133.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.133.0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.133.0 // indirect go.opentelemetry.io/collector/pipeline v1.39.0 // indirect diff --git a/receiver/systemdreceiver/go.sum b/receiver/systemdreceiver/go.sum index 90f2c734c70a9..cbd64bf42833d 100644 --- a/receiver/systemdreceiver/go.sum +++ b/receiver/systemdreceiver/go.sum @@ -77,6 +77,8 @@ go.opentelemetry.io/collector/consumer/xconsumer v0.133.0 h1:Xx4Yna/We4qDlbAla1n go.opentelemetry.io/collector/consumer/xconsumer v0.133.0/go.mod h1:he874Md/0uAS2Fs+TDHAy10OBLRSw8233LdREizVvG4= go.opentelemetry.io/collector/featuregate v1.39.0 h1:OlXZWW+WUP8cgKh2mnwgWXUJO/29irb0hG6jvwscRKM= go.opentelemetry.io/collector/featuregate v1.39.0/go.mod h1:A72x92glpH3zxekaUybml1vMSv94BH6jQRn5+/htcjw= +go.opentelemetry.io/collector/filter v0.133.0 h1:p17IVDd3M6ngPYSZLd3kJoeimyQ+IRNZvmOFHmKmxcg= +go.opentelemetry.io/collector/filter v0.133.0/go.mod h1:Ce+BktMgItbXJ8LGC25xdLELgG2U0EW/dPgo3cQZ/9Y= go.opentelemetry.io/collector/internal/telemetry v0.133.0 h1:YxbckZC9HniNOZgnSofTOe0AB/bEsmISNdQeS+3CU3o= go.opentelemetry.io/collector/internal/telemetry v0.133.0/go.mod h1:akUK7X6ZQ+CbbCjyXLv9y/EHt5jIy+J+nGoLvndZN14= go.opentelemetry.io/collector/pdata v1.39.0 h1:jr0f033o57Hpbj2Il8M15tPbvrOgY/Aoc+/+sxzhSFU= From 26d8b37e6840e0fcd784143dd366832f7c9754c8 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 27 Aug 2025 08:11:31 +0100 Subject: [PATCH 3/7] [systemdreceiver] Expose unit name as a resource attribute - Store possible unit states in a slice, to ensure metrics are emitted in a deterministic order. --- receiver/systemdreceiver/documentation.md | 7 +- receiver/systemdreceiver/go.mod | 2 +- .../internal/metadata/generated_config.go | 46 +++++++- .../metadata/generated_config_test.go | 51 ++++++++- .../internal/metadata/generated_metrics.go | 55 +++++++--- .../metadata/generated_metrics_test.go | 20 ++-- .../internal/metadata/generated_resource.go | 36 +++++++ .../metadata/generated_resource_test.go | 40 +++++++ .../internal/metadata/testdata/config.yaml | 18 ++++ receiver/systemdreceiver/metadata.yaml | 5 +- receiver/systemdreceiver/scraper.go | 8 +- receiver/systemdreceiver/scraper_test.go | 18 +--- .../{nginx-active.yaml => basic-scrape.yaml} | 100 +++++++++++++----- .../expected_metrics/nginx-failed.yaml | 93 ---------------- 14 files changed, 339 insertions(+), 160 deletions(-) create mode 100644 receiver/systemdreceiver/internal/metadata/generated_resource.go create mode 100644 receiver/systemdreceiver/internal/metadata/generated_resource_test.go rename receiver/systemdreceiver/testdata/expected_metrics/{nginx-active.yaml => basic-scrape.yaml} (52%) delete mode 100644 receiver/systemdreceiver/testdata/expected_metrics/nginx-failed.yaml diff --git a/receiver/systemdreceiver/documentation.md b/receiver/systemdreceiver/documentation.md index e3f6e144b9ca5..6c9192b8d7c78 100644 --- a/receiver/systemdreceiver/documentation.md +++ b/receiver/systemdreceiver/documentation.md @@ -24,5 +24,10 @@ metrics: | Name | Description | Values | Optional | | ---- | ----------- | ------ | -------- | -| systemd.unit.name | Name of the systemd unit | Any Str | false | | 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`` | false | + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +| systemd.unit.name | Name of the systemd unit | Any Str | true | diff --git a/receiver/systemdreceiver/go.mod b/receiver/systemdreceiver/go.mod index e8ae22dcd1ecf..6f894f87b9906 100644 --- a/receiver/systemdreceiver/go.mod +++ b/receiver/systemdreceiver/go.mod @@ -13,6 +13,7 @@ require ( go.opentelemetry.io/collector/confmap v1.39.0 go.opentelemetry.io/collector/consumer v1.39.0 go.opentelemetry.io/collector/consumer/consumertest v0.133.0 + go.opentelemetry.io/collector/filter v0.133.0 go.opentelemetry.io/collector/pdata v1.39.0 go.opentelemetry.io/collector/receiver v1.39.0 go.opentelemetry.io/collector/receiver/receivertest v0.133.0 @@ -47,7 +48,6 @@ require ( go.opentelemetry.io/collector/consumer/consumererror v0.133.0 // indirect go.opentelemetry.io/collector/consumer/xconsumer v0.133.0 // indirect go.opentelemetry.io/collector/featuregate v1.39.0 // indirect - go.opentelemetry.io/collector/filter v0.133.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.133.0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.133.0 // indirect go.opentelemetry.io/collector/pipeline v1.39.0 // indirect diff --git a/receiver/systemdreceiver/internal/metadata/generated_config.go b/receiver/systemdreceiver/internal/metadata/generated_config.go index 04a93ba56771b..518f36ad2cde8 100644 --- a/receiver/systemdreceiver/internal/metadata/generated_config.go +++ b/receiver/systemdreceiver/internal/metadata/generated_config.go @@ -4,6 +4,7 @@ package metadata import ( "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/filter" ) // MetricConfig provides common config for a particular metric. @@ -38,13 +39,54 @@ func DefaultMetricsConfig() MetricsConfig { } } +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` + // Experimental: MetricsInclude defines a list of filters for attribute values. + // If the list is not empty, only metrics with matching resource attribute values will be emitted. + MetricsInclude []filter.Config `mapstructure:"metrics_include"` + // Experimental: MetricsExclude defines a list of filters for attribute values. + // If the list is not empty, metrics with matching resource attribute values will not be emitted. + // MetricsInclude has higher priority than MetricsExclude. + MetricsExclude []filter.Config `mapstructure:"metrics_exclude"` + + enabledSetByUser bool +} + +func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(rac) + if err != nil { + return err + } + rac.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// ResourceAttributesConfig provides config for systemd resource attributes. +type ResourceAttributesConfig struct { + SystemdUnitName ResourceAttributeConfig `mapstructure:"systemd.unit.name"` +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + SystemdUnitName: ResourceAttributeConfig{ + Enabled: true, + }, + } +} + // MetricsBuilderConfig is a configuration for systemd metrics builder. type MetricsBuilderConfig struct { - Metrics MetricsConfig `mapstructure:"metrics"` + Metrics MetricsConfig `mapstructure:"metrics"` + ResourceAttributes ResourceAttributesConfig `mapstructure:"resource_attributes"` } func DefaultMetricsBuilderConfig() MetricsBuilderConfig { return MetricsBuilderConfig{ - Metrics: DefaultMetricsConfig(), + Metrics: DefaultMetricsConfig(), + ResourceAttributes: DefaultResourceAttributesConfig(), } } diff --git a/receiver/systemdreceiver/internal/metadata/generated_config_test.go b/receiver/systemdreceiver/internal/metadata/generated_config_test.go index 1f8d8d18911e5..8f2a03d28b6bc 100644 --- a/receiver/systemdreceiver/internal/metadata/generated_config_test.go +++ b/receiver/systemdreceiver/internal/metadata/generated_config_test.go @@ -29,6 +29,9 @@ func TestMetricsBuilderConfig(t *testing.T) { Metrics: MetricsConfig{ SystemdUnitState: MetricConfig{Enabled: true}, }, + ResourceAttributes: ResourceAttributesConfig{ + SystemdUnitName: ResourceAttributeConfig{Enabled: true}, + }, }, }, { @@ -37,13 +40,16 @@ func TestMetricsBuilderConfig(t *testing.T) { Metrics: MetricsConfig{ SystemdUnitState: MetricConfig{Enabled: false}, }, + ResourceAttributes: ResourceAttributesConfig{ + SystemdUnitName: ResourceAttributeConfig{Enabled: false}, + }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := loadMetricsBuilderConfig(t, tt.name) - diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{})) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{}, ResourceAttributeConfig{})) require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) }) } @@ -58,3 +64,46 @@ func loadMetricsBuilderConfig(t *testing.T, name string) MetricsBuilderConfig { require.NoError(t, sub.Unmarshal(&cfg, confmap.WithIgnoreUnused())) return cfg } + +func TestResourceAttributesConfig(t *testing.T) { + tests := []struct { + name string + want ResourceAttributesConfig + }{ + { + name: "default", + want: DefaultResourceAttributesConfig(), + }, + { + name: "all_set", + want: ResourceAttributesConfig{ + SystemdUnitName: ResourceAttributeConfig{Enabled: true}, + }, + }, + { + name: "none_set", + want: ResourceAttributesConfig{ + SystemdUnitName: ResourceAttributeConfig{Enabled: false}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt.name) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(ResourceAttributeConfig{})) + require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) + }) + } +} + +func loadResourceAttributesConfig(t *testing.T, name string) ResourceAttributesConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + sub, err = sub.Sub("resource_attributes") + require.NoError(t, err) + cfg := DefaultResourceAttributesConfig() + require.NoError(t, sub.Unmarshal(&cfg)) + return cfg +} diff --git a/receiver/systemdreceiver/internal/metadata/generated_metrics.go b/receiver/systemdreceiver/internal/metadata/generated_metrics.go index 14dfc3b462eac..3878891b284da 100644 --- a/receiver/systemdreceiver/internal/metadata/generated_metrics.go +++ b/receiver/systemdreceiver/internal/metadata/generated_metrics.go @@ -6,6 +6,7 @@ import ( "time" "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/filter" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/receiver" @@ -92,7 +93,7 @@ func (m *metricSystemdUnitState) init() { m.data.Sum().DataPoints().EnsureCapacity(m.capacity) } -func (m *metricSystemdUnitState) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, systemdUnitNameAttributeValue string, systemdUnitActiveStateAttributeValue string) { +func (m *metricSystemdUnitState) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, systemdUnitActiveStateAttributeValue string) { if !m.config.Enabled { return } @@ -100,7 +101,6 @@ func (m *metricSystemdUnitState) recordDataPoint(start pcommon.Timestamp, ts pco dp.SetStartTimestamp(start) dp.SetTimestamp(ts) dp.SetIntValue(val) - dp.Attributes().PutStr("systemd.unit.name", systemdUnitNameAttributeValue) dp.Attributes().PutStr("systemd.unit.active_state", systemdUnitActiveStateAttributeValue) } @@ -132,12 +132,14 @@ func newMetricSystemdUnitState(cfg MetricConfig) metricSystemdUnitState { // MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations // required to produce metric representation defined in metadata and user config. type MetricsBuilder struct { - config MetricsBuilderConfig // config of the metrics builder. - startTime pcommon.Timestamp // start time that will be applied to all recorded data points. - metricsCapacity int // maximum observed number of metrics per resource. - metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. - buildInfo component.BuildInfo // contains version information. - metricSystemdUnitState metricSystemdUnitState + config MetricsBuilderConfig // config of the metrics builder. + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information. + resourceAttributeIncludeFilter map[string]filter.Filter + resourceAttributeExcludeFilter map[string]filter.Filter + metricSystemdUnitState metricSystemdUnitState } // MetricBuilderOption applies changes to default metrics builder. @@ -159,11 +161,19 @@ func WithStartTime(startTime pcommon.Timestamp) MetricBuilderOption { } func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, options ...MetricBuilderOption) *MetricsBuilder { mb := &MetricsBuilder{ - config: mbc, - startTime: pcommon.NewTimestampFromTime(time.Now()), - metricsBuffer: pmetric.NewMetrics(), - buildInfo: settings.BuildInfo, - metricSystemdUnitState: newMetricSystemdUnitState(mbc.Metrics.SystemdUnitState), + config: mbc, + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + metricSystemdUnitState: newMetricSystemdUnitState(mbc.Metrics.SystemdUnitState), + resourceAttributeIncludeFilter: make(map[string]filter.Filter), + resourceAttributeExcludeFilter: make(map[string]filter.Filter), + } + if mbc.ResourceAttributes.SystemdUnitName.MetricsInclude != nil { + mb.resourceAttributeIncludeFilter["systemd.unit.name"] = filter.CreateFilter(mbc.ResourceAttributes.SystemdUnitName.MetricsInclude) + } + if mbc.ResourceAttributes.SystemdUnitName.MetricsExclude != nil { + mb.resourceAttributeExcludeFilter["systemd.unit.name"] = filter.CreateFilter(mbc.ResourceAttributes.SystemdUnitName.MetricsExclude) } for _, op := range options { @@ -172,6 +182,11 @@ func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, opt return mb } +// NewResourceBuilder returns a new resource builder that should be used to build a resource associated with for the emitted metrics. +func (mb *MetricsBuilder) NewResourceBuilder() *ResourceBuilder { + return NewResourceBuilder(mb.config.ResourceAttributes) +} + // updateCapacity updates max length of metrics and resource attributes that will be used for the slice capacity. func (mb *MetricsBuilder) updateCapacity(rm pmetric.ResourceMetrics) { if mb.metricsCapacity < rm.ScopeMetrics().At(0).Metrics().Len() { @@ -234,6 +249,16 @@ func (mb *MetricsBuilder) EmitForResource(options ...ResourceMetricsOption) { for _, op := range options { op.apply(rm) } + for attr, filter := range mb.resourceAttributeIncludeFilter { + if val, ok := rm.Resource().Attributes().Get(attr); ok && !filter.Matches(val.AsString()) { + return + } + } + for attr, filter := range mb.resourceAttributeExcludeFilter { + if val, ok := rm.Resource().Attributes().Get(attr); ok && filter.Matches(val.AsString()) { + return + } + } if ils.Metrics().Len() > 0 { mb.updateCapacity(rm) @@ -252,8 +277,8 @@ func (mb *MetricsBuilder) Emit(options ...ResourceMetricsOption) pmetric.Metrics } // RecordSystemdUnitStateDataPoint adds a data point to systemd.unit.state metric. -func (mb *MetricsBuilder) RecordSystemdUnitStateDataPoint(ts pcommon.Timestamp, val int64, systemdUnitNameAttributeValue string, systemdUnitActiveStateAttributeValue AttributeSystemdUnitActiveState) { - mb.metricSystemdUnitState.recordDataPoint(mb.startTime, ts, val, systemdUnitNameAttributeValue, systemdUnitActiveStateAttributeValue.String()) +func (mb *MetricsBuilder) RecordSystemdUnitStateDataPoint(ts pcommon.Timestamp, val int64, systemdUnitActiveStateAttributeValue AttributeSystemdUnitActiveState) { + mb.metricSystemdUnitState.recordDataPoint(mb.startTime, ts, val, systemdUnitActiveStateAttributeValue.String()) } // Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, diff --git a/receiver/systemdreceiver/internal/metadata/generated_metrics_test.go b/receiver/systemdreceiver/internal/metadata/generated_metrics_test.go index 2b51e7f9a3507..f7d3f66515b12 100644 --- a/receiver/systemdreceiver/internal/metadata/generated_metrics_test.go +++ b/receiver/systemdreceiver/internal/metadata/generated_metrics_test.go @@ -42,6 +42,15 @@ func TestMetricsBuilder(t *testing.T) { resAttrsSet: testDataSetNone, expectEmpty: true, }, + { + name: "filter_set_include", + resAttrsSet: testDataSetAll, + }, + { + name: "filter_set_exclude", + resAttrsSet: testDataSetAll, + expectEmpty: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -61,9 +70,11 @@ func TestMetricsBuilder(t *testing.T) { defaultMetricsCount++ allMetricsCount++ - mb.RecordSystemdUnitStateDataPoint(ts, 1, "systemd.unit.name-val", AttributeSystemdUnitActiveStateActive) + mb.RecordSystemdUnitStateDataPoint(ts, 1, AttributeSystemdUnitActiveStateActive) - res := pcommon.NewResource() + rb := mb.NewResourceBuilder() + rb.SetSystemdUnitName("systemd.unit.name-val") + res := rb.Emit() metrics := mb.Emit(WithResource(res)) if tt.expectEmpty { @@ -99,10 +110,7 @@ func TestMetricsBuilder(t *testing.T) { assert.Equal(t, ts, dp.Timestamp()) assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) assert.Equal(t, int64(1), dp.IntValue()) - attrVal, ok := dp.Attributes().Get("systemd.unit.name") - assert.True(t, ok) - assert.Equal(t, "systemd.unit.name-val", attrVal.Str()) - attrVal, ok = dp.Attributes().Get("systemd.unit.active_state") + attrVal, ok := dp.Attributes().Get("systemd.unit.active_state") assert.True(t, ok) assert.Equal(t, "active", attrVal.Str()) } diff --git a/receiver/systemdreceiver/internal/metadata/generated_resource.go b/receiver/systemdreceiver/internal/metadata/generated_resource.go new file mode 100644 index 0000000000000..9268c7eb8b306 --- /dev/null +++ b/receiver/systemdreceiver/internal/metadata/generated_resource.go @@ -0,0 +1,36 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" +) + +// ResourceBuilder is a helper struct to build resources predefined in metadata.yaml. +// The ResourceBuilder is not thread-safe and must not to be used in multiple goroutines. +type ResourceBuilder struct { + config ResourceAttributesConfig + res pcommon.Resource +} + +// NewResourceBuilder creates a new ResourceBuilder. This method should be called on the start of the application. +func NewResourceBuilder(rac ResourceAttributesConfig) *ResourceBuilder { + return &ResourceBuilder{ + config: rac, + res: pcommon.NewResource(), + } +} + +// SetSystemdUnitName sets provided value as "systemd.unit.name" attribute. +func (rb *ResourceBuilder) SetSystemdUnitName(val string) { + if rb.config.SystemdUnitName.Enabled { + rb.res.Attributes().PutStr("systemd.unit.name", val) + } +} + +// Emit returns the built resource and resets the internal builder state. +func (rb *ResourceBuilder) Emit() pcommon.Resource { + r := rb.res + rb.res = pcommon.NewResource() + return r +} diff --git a/receiver/systemdreceiver/internal/metadata/generated_resource_test.go b/receiver/systemdreceiver/internal/metadata/generated_resource_test.go new file mode 100644 index 0000000000000..eb77215b214b6 --- /dev/null +++ b/receiver/systemdreceiver/internal/metadata/generated_resource_test.go @@ -0,0 +1,40 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResourceBuilder(t *testing.T) { + for _, tt := range []string{"default", "all_set", "none_set"} { + t.Run(tt, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt) + rb := NewResourceBuilder(cfg) + rb.SetSystemdUnitName("systemd.unit.name-val") + + res := rb.Emit() + assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource + + switch tt { + case "default": + assert.Equal(t, 1, res.Attributes().Len()) + case "all_set": + assert.Equal(t, 1, res.Attributes().Len()) + case "none_set": + assert.Equal(t, 0, res.Attributes().Len()) + return + default: + assert.Failf(t, "unexpected test case: %s", tt) + } + + val, ok := res.Attributes().Get("systemd.unit.name") + assert.True(t, ok) + if ok { + assert.Equal(t, "systemd.unit.name-val", val.Str()) + } + }) + } +} diff --git a/receiver/systemdreceiver/internal/metadata/testdata/config.yaml b/receiver/systemdreceiver/internal/metadata/testdata/config.yaml index 7c056c75a7bbd..66dfe42708f6a 100644 --- a/receiver/systemdreceiver/internal/metadata/testdata/config.yaml +++ b/receiver/systemdreceiver/internal/metadata/testdata/config.yaml @@ -3,7 +3,25 @@ all_set: metrics: systemd.unit.state: enabled: true + resource_attributes: + systemd.unit.name: + enabled: true none_set: metrics: systemd.unit.state: enabled: false + resource_attributes: + systemd.unit.name: + enabled: false +filter_set_include: + resource_attributes: + systemd.unit.name: + enabled: true + metrics_include: + - regexp: ".*" +filter_set_exclude: + resource_attributes: + systemd.unit.name: + enabled: true + metrics_exclude: + - strict: "systemd.unit.name-val" diff --git a/receiver/systemdreceiver/metadata.yaml b/receiver/systemdreceiver/metadata.yaml index d2a99854becb0..9f35702983cf1 100644 --- a/receiver/systemdreceiver/metadata.yaml +++ b/receiver/systemdreceiver/metadata.yaml @@ -10,12 +10,13 @@ status: emeritus: [Hemansh31] unsupported_platforms: [darwin, windows] -attributes: +resource_attributes: systemd.unit.name: description: Name of the systemd unit type: string enabled: true +attributes: systemd.unit.active_state: description: The active state of the unit (https://www.freedesktop.org/software/systemd/man/latest/systemd.html#Units) type: string @@ -38,6 +39,6 @@ metrics: aggregation_temporality: cumulative monotonic: false unit: "1" - attributes: [systemd.unit.name, systemd.unit.active_state] + attributes: [systemd.unit.active_state] tests: diff --git a/receiver/systemdreceiver/scraper.go b/receiver/systemdreceiver/scraper.go index 2db30aedcfc31..de98c81dc4af4 100644 --- a/receiver/systemdreceiver/scraper.go +++ b/receiver/systemdreceiver/scraper.go @@ -83,11 +83,15 @@ func (s *systemdScraper) scrape(ctx context.Context) (pmetric.Metrics, error) { for _, unit := range units { for stateName, state := range metadata.MapAttributeSystemdUnitActiveState { if unit.ActiveState == stateName { - s.mb.RecordSystemdUnitStateDataPoint(now, int64(1), unit.Name, state) + s.mb.RecordSystemdUnitStateDataPoint(now, int64(1), state) } else { - s.mb.RecordSystemdUnitStateDataPoint(now, int64(0), unit.Name, state) + s.mb.RecordSystemdUnitStateDataPoint(now, int64(0), state) } } + + resource := s.mb.NewResourceBuilder() + resource.SetSystemdUnitName(unit.Name) + s.mb.EmitForResource(metadata.WithResource(resource.Emit())) } metrics := s.mb.Emit() diff --git a/receiver/systemdreceiver/scraper_test.go b/receiver/systemdreceiver/scraper_test.go index 5f4a01b822324..8624bb6eaca30 100644 --- a/receiver/systemdreceiver/scraper_test.go +++ b/receiver/systemdreceiver/scraper_test.go @@ -121,7 +121,7 @@ func TestScraperScrape(t *testing.T) { expectedErr error }{ { - desc: "Active service", + desc: "Basic scrape", units: []unitTuple{ { Name: "nginx.service", @@ -135,28 +135,20 @@ func TestScraperScrape(t *testing.T) { JobType: "", JobPath: "/", }, - }, - goldenName: "nginx-active", - expectedErr: nil, - }, - - { - desc: "Failed service", - units: []unitTuple{ { - Name: "nginx.service", - Description: "A high performance web server and a reverse proxy server", + Name: "rsyslog.service", + Description: "Advanced key-value store", LoadState: "loaded", ActiveState: "failed", SubState: "failed", Following: "", - Path: "/org/freedesktop/systemd1/unit/nginx_2eservice", + Path: "/org/freedesktop/systemd1/unit/rsyslog_2eservice", JobID: uint32(0), JobType: "", JobPath: "/", }, }, - goldenName: "nginx-failed", + goldenName: "basic-scrape", expectedErr: nil, }, } diff --git a/receiver/systemdreceiver/testdata/expected_metrics/nginx-active.yaml b/receiver/systemdreceiver/testdata/expected_metrics/basic-scrape.yaml similarity index 52% rename from receiver/systemdreceiver/testdata/expected_metrics/nginx-active.yaml rename to receiver/systemdreceiver/testdata/expected_metrics/basic-scrape.yaml index 4d384f30e7df1..0a91c7ee99576 100644 --- a/receiver/systemdreceiver/testdata/expected_metrics/nginx-active.yaml +++ b/receiver/systemdreceiver/testdata/expected_metrics/basic-scrape.yaml @@ -1,5 +1,9 @@ resourceMetrics: - - resource: {} + - resource: + attributes: + - key: systemd.unit.name + value: + stringValue: nginx.service scopeMetrics: - metrics: - description: 1 if the check resulted in active_state matching the current state, otherwise 0. @@ -12,9 +16,6 @@ resourceMetrics: - key: systemd.unit.active_state value: stringValue: activating - - key: systemd.unit.name - value: - stringValue: nginx.service startTimeUnixNano: "1000000" timeUnixNano: "2000000" - asInt: "1" @@ -22,9 +23,6 @@ resourceMetrics: - key: systemd.unit.active_state value: stringValue: active - - key: systemd.unit.name - value: - stringValue: nginx.service startTimeUnixNano: "1000000" timeUnixNano: "2000000" - asInt: "0" @@ -32,9 +30,6 @@ resourceMetrics: - key: systemd.unit.active_state value: stringValue: deactivating - - key: systemd.unit.name - value: - stringValue: nginx.service startTimeUnixNano: "1000000" timeUnixNano: "2000000" - asInt: "0" @@ -42,9 +37,6 @@ resourceMetrics: - key: systemd.unit.active_state value: stringValue: failed - - key: systemd.unit.name - value: - stringValue: nginx.service startTimeUnixNano: "1000000" timeUnixNano: "2000000" - asInt: "0" @@ -52,9 +44,6 @@ resourceMetrics: - key: systemd.unit.active_state value: stringValue: inactive - - key: systemd.unit.name - value: - stringValue: nginx.service startTimeUnixNano: "1000000" timeUnixNano: "2000000" - asInt: "0" @@ -62,9 +51,6 @@ resourceMetrics: - key: systemd.unit.active_state value: stringValue: maintenance - - key: systemd.unit.name - value: - stringValue: nginx.service startTimeUnixNano: "1000000" timeUnixNano: "2000000" - asInt: "0" @@ -72,9 +58,6 @@ resourceMetrics: - key: systemd.unit.active_state value: stringValue: refreshing - - key: systemd.unit.name - value: - stringValue: nginx.service startTimeUnixNano: "1000000" timeUnixNano: "2000000" - asInt: "0" @@ -82,9 +65,78 @@ resourceMetrics: - key: systemd.unit.active_state value: stringValue: reloading - - key: systemd.unit.name + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + unit: "1" + scope: + name: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver + version: latest + - resource: + attributes: + - key: systemd.unit.name + value: + stringValue: rsyslog.service + scopeMetrics: + - metrics: + - description: 1 if the check resulted in active_state matching the current state, otherwise 0. + name: systemd.unit.state + sum: + aggregationTemporality: 2 + dataPoints: + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: activating + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: active + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state value: - stringValue: nginx.service + stringValue: deactivating + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "1" + attributes: + - key: systemd.unit.active_state + value: + stringValue: failed + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: inactive + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: maintenance + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: refreshing + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asInt: "0" + attributes: + - key: systemd.unit.active_state + value: + stringValue: reloading startTimeUnixNano: "1000000" timeUnixNano: "2000000" unit: "1" diff --git a/receiver/systemdreceiver/testdata/expected_metrics/nginx-failed.yaml b/receiver/systemdreceiver/testdata/expected_metrics/nginx-failed.yaml deleted file mode 100644 index c0ae98b2c1f89..0000000000000 --- a/receiver/systemdreceiver/testdata/expected_metrics/nginx-failed.yaml +++ /dev/null @@ -1,93 +0,0 @@ -resourceMetrics: - - resource: {} - scopeMetrics: - - metrics: - - description: 1 if the check resulted in active_state matching the current state, otherwise 0. - name: systemd.unit.state - sum: - aggregationTemporality: 2 - dataPoints: - - asInt: "0" - attributes: - - key: systemd.unit.active_state - value: - stringValue: activating - - key: systemd.unit.name - value: - stringValue: nginx.service - startTimeUnixNano: "1000000" - timeUnixNano: "2000000" - - asInt: "0" - attributes: - - key: systemd.unit.active_state - value: - stringValue: active - - key: systemd.unit.name - value: - stringValue: nginx.service - startTimeUnixNano: "1000000" - timeUnixNano: "2000000" - - asInt: "0" - attributes: - - key: systemd.unit.active_state - value: - stringValue: deactivating - - key: systemd.unit.name - value: - stringValue: nginx.service - startTimeUnixNano: "1000000" - timeUnixNano: "2000000" - - asInt: "1" - attributes: - - key: systemd.unit.active_state - value: - stringValue: failed - - key: systemd.unit.name - value: - stringValue: nginx.service - startTimeUnixNano: "1000000" - timeUnixNano: "2000000" - - asInt: "0" - attributes: - - key: systemd.unit.active_state - value: - stringValue: inactive - - key: systemd.unit.name - value: - stringValue: nginx.service - startTimeUnixNano: "1000000" - timeUnixNano: "2000000" - - asInt: "0" - attributes: - - key: systemd.unit.active_state - value: - stringValue: maintenance - - key: systemd.unit.name - value: - stringValue: nginx.service - startTimeUnixNano: "1000000" - timeUnixNano: "2000000" - - asInt: "0" - attributes: - - key: systemd.unit.active_state - value: - stringValue: refreshing - - key: systemd.unit.name - value: - stringValue: nginx.service - startTimeUnixNano: "1000000" - timeUnixNano: "2000000" - - asInt: "0" - attributes: - - key: systemd.unit.active_state - value: - stringValue: reloading - - key: systemd.unit.name - value: - stringValue: nginx.service - startTimeUnixNano: "1000000" - timeUnixNano: "2000000" - unit: "1" - scope: - name: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/systemdreceiver - version: latest From 4a4cc84b5af66c87b60f85a45bc0d104b5dc4ea6 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 18 Sep 2025 08:43:58 +0100 Subject: [PATCH 4/7] Fix incorrect version number --- receiver/systemdreceiver/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/receiver/systemdreceiver/go.mod b/receiver/systemdreceiver/go.mod index e01cefca6ac46..d67bf90a2d98a 100644 --- a/receiver/systemdreceiver/go.mod +++ b/receiver/systemdreceiver/go.mod @@ -51,7 +51,7 @@ require ( go.opentelemetry.io/collector/internal/telemetry v0.135.1-0.20250911155607-37a3ace6274c // indirect go.opentelemetry.io/collector/pdata/pprofile v0.135.1-0.20250911155607-37a3ace6274c // indirect go.opentelemetry.io/collector/pipeline v1.41.1-0.20250911155607-37a3ace6274c // indirect - go.opentelemetry.io/collector/receiver/receiverhelper v0.135.0 // indirect + go.opentelemetry.io/collector/receiver/receiverhelper v0.135.1-0.20250911155607-37a3ace6274c // indirect go.opentelemetry.io/collector/receiver/xreceiver v0.135.1-0.20250911155607-37a3ace6274c // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.12.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect From a3ae77adf9c1cf4a6ebf8d74cabdc36a63f49a00 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 18 Sep 2025 08:58:14 +0100 Subject: [PATCH 5/7] Oh, and tidy as well --- receiver/systemdreceiver/go.sum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/receiver/systemdreceiver/go.sum b/receiver/systemdreceiver/go.sum index fe90504a720d3..34bd6c70eb86f 100644 --- a/receiver/systemdreceiver/go.sum +++ b/receiver/systemdreceiver/go.sum @@ -91,8 +91,8 @@ go.opentelemetry.io/collector/pipeline v1.41.1-0.20250911155607-37a3ace6274c h1: go.opentelemetry.io/collector/pipeline v1.41.1-0.20250911155607-37a3ace6274c/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= go.opentelemetry.io/collector/receiver v1.41.1-0.20250911155607-37a3ace6274c h1:5q7dHiRYWHEt+cPF64MfyMOGCp+gIn6AKqWEIQnX1P0= go.opentelemetry.io/collector/receiver v1.41.1-0.20250911155607-37a3ace6274c/go.mod h1:nyeySqSugNPraMeC+/wxIvrvOHXU7kqYXxjUVeV47qo= -go.opentelemetry.io/collector/receiver/receiverhelper v0.135.0 h1:qaqTLP7NVoWvJQ0zOQ8P39/8f8l6QoyH3bZ2JE8yMT4= -go.opentelemetry.io/collector/receiver/receiverhelper v0.135.0/go.mod h1:6GX9twiGgrn9iopvvN88Y26PMMl+ExaM9kMFv9kPFEg= +go.opentelemetry.io/collector/receiver/receiverhelper v0.135.1-0.20250911155607-37a3ace6274c h1:5KkZ1PG7ERMW56M0mrXhW7EbRBVmLbZ3kJHUnUSiJp4= +go.opentelemetry.io/collector/receiver/receiverhelper v0.135.1-0.20250911155607-37a3ace6274c/go.mod h1:fIm4Sj/ChJuMxWXDii9EguOhLJHiFh3ypbCAySTbxg0= go.opentelemetry.io/collector/receiver/receivertest v0.135.1-0.20250911155607-37a3ace6274c h1:F+eHWJvvf5WwNbzJIM3VholBaX0XHOmOkjXXrwMGjU4= go.opentelemetry.io/collector/receiver/receivertest v0.135.1-0.20250911155607-37a3ace6274c/go.mod h1:ykdxpT+F1G2byz9zX3KsF4XsBQm00D2ypwyQjMo64F4= go.opentelemetry.io/collector/receiver/xreceiver v0.135.1-0.20250911155607-37a3ace6274c h1:M3pUQbsppITtMsrIrjRhnPHlu0a22sKLNW02nNTuCX0= From b8e8472d8e703c442ce2afcc859fb2c70a6dbb27 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Tue, 21 Oct 2025 08:09:38 +0100 Subject: [PATCH 6/7] Update changelog name to match changes in #43215 --- .chloggen/systemd-receiver.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.chloggen/systemd-receiver.yaml b/.chloggen/systemd-receiver.yaml index 231ee2c7398af..ff1459d957164 100644 --- a/.chloggen/systemd-receiver.yaml +++ b/.chloggen/systemd-receiver.yaml @@ -3,8 +3,8 @@ # 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. filelogreceiver) -component: systemdreceiver +# 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. From d7b1ad6b28777cc38f9ca5a4f0a39ae97711a0aa Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 22 Oct 2025 17:52:31 +0100 Subject: [PATCH 7/7] Make generate --- receiver/systemdreceiver/documentation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/receiver/systemdreceiver/documentation.md b/receiver/systemdreceiver/documentation.md index 6c9192b8d7c78..1bfea333e0cc1 100644 --- a/receiver/systemdreceiver/documentation.md +++ b/receiver/systemdreceiver/documentation.md @@ -22,9 +22,9 @@ metrics: #### Attributes -| Name | Description | Values | Optional | +| 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`` | false | +| 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