Skip to content

feat(inputs.system): Add operating-system information#18834

Open
bilkoua wants to merge 5 commits intoinfluxdata:masterfrom
bilkoua:master
Open

feat(inputs.system): Add operating-system information#18834
bilkoua wants to merge 5 commits intoinfluxdata:masterfrom
bilkoua:master

Conversation

@bilkoua
Copy link
Copy Markdown
Contributor

@bilkoua bilkoua commented May 4, 2026

Summary

This is the second step of the plan agreed in #18533: add an os value
to the include option of inputs.system that emits a separate
system_os measurement with operating system release and uname
information.

The data is gathered through gopsutil for cross-platform support and
is read with the granular host.PlatformInformation(),
host.KernelVersion() and host.KernelArch() calls instead of
host.Info() to avoid unrelated probes for virtualization, boot time
and process counts.

The result is cached for os_cache_ttl (default 5m) between gathers
since these values are effectively static at runtime; setting
os_cache_ttl = "0s" keeps the cache for the lifetime of the
process, while a tiny positive value such as "1ns" effectively
disables the cache and forces a fresh read on every gather. The TTL
handling matches the convention used by inputs.sqlserver,
inputs.logql, inputs.promql, outputs.iotdb and
outputs.parquet where a zero duration disables the time bound.

Fields emitted by the system_os measurement

Field Type Source
os string runtime.GOOS
platform string /etc/os-release etc.
platform_family string /etc/os-release etc.
platform_version string /etc/os-release etc.
kernel_version string uname -r
kernel_arch string uname -m

On platforms where gopsutil cannot provide a particular value (e.g.
parts of FreeBSD/OpenBSD/Solaris) the corresponding field is left
empty; if no field can be gathered the metric is skipped entirely.

Backward compatibility

The default include set is unchanged, so existing deployments keep
emitting only the system measurement and do not see the new
system_os metric until they opt in.

Example output

system_os,host=worker-01 os="linux",platform="ubuntu",platform_family="debian",platform_version="26.04",kernel_version="7.0.0-7-generic",kernel_arch="x86_64" 1748000000000000000

Checklist

@telegraf-tiger telegraf-tiger Bot added area/system feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin plugin/input 1. Request for new input plugins 2. Issues/PRs that are related to input plugins labels May 4, 2026
@telegraf-tiger
Copy link
Copy Markdown
Contributor

telegraf-tiger Bot commented May 4, 2026

@bilkoua
Copy link
Copy Markdown
Contributor Author

bilkoua commented May 4, 2026

@skartikey @srebhan @Hipska Hi, could you take a look at this PR when you have a moment?

Copy link
Copy Markdown
Member

@srebhan srebhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @bilkoua for the follow-up PR! Please find my comments below...

| `platform_family` | string | Platform family (e.g. `debian`, `rhel`) |
| `platform_version` | string | Platform / distribution version (e.g. `26.04`) |
| `kernel_version` | string | Kernel release as returned by `uname -r` (e.g. `7.0.0-7-generic`) |
| `kernel_arch` | string | Kernel architecture as returned by `uname -m` (e.g. `x86_64`) |
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name this arch or architecture and use GOARCH to fill it?

Comment on lines +108 to +110
ttl := time.Duration(s.OSCacheTTL)
expired := ttl > 0 && now.Sub(s.osCachedAt) >= ttl
if s.osCachedAt.IsZero() || expired {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about

Suggested change
ttl := time.Duration(s.OSCacheTTL)
expired := ttl > 0 && now.Sub(s.osCachedAt) >= ttl
if s.osCachedAt.IsZero() || expired {
if time.Since(s.osCachedAt) > time.Duration(s.OSCacheTTL) {

Comment on lines +112 to +116
if err != nil {
acc.AddError(err)
}
s.osFields = osFields
s.osCachedAt = now
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to override the fields if an error occurred?

Comment on lines +194 to +219
var errs []error

platform, family, version, err := host.PlatformInformation()
if err != nil && !strings.Contains(err.Error(), "not implemented") {
errs = append(errs, fmt.Errorf("reading platform information: %w", err))
}
kernelVersion, err := host.KernelVersion()
if err != nil && !strings.Contains(err.Error(), "not implemented") {
errs = append(errs, fmt.Errorf("reading kernel version: %w", err))
}
kernelArch, err := host.KernelArch()
if err != nil && !strings.Contains(err.Error(), "not implemented") {
errs = append(errs, fmt.Errorf("reading kernel architecture: %w", err))
}

if platform == "" && family == "" && version == "" && kernelVersion == "" && kernelArch == "" {
return nil, errors.Join(errs...)
}
return map[string]interface{}{
"os": runtime.GOOS,
"platform": platform,
"platform_family": family,
"platform_version": version,
"kernel_version": kernelVersion,
"kernel_arch": kernelArch,
}, errors.Join(errs...)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to join the errors instead of directly returning as soon as we encounter the first issue?

inputs.Add("system", func() telegraf.Input {
return &System{}
return &System{
OSCacheTTL: config.Duration(defaultOSCacheTTL),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the concrete value here instead of defining a useless constant obfuscating the actual value to the reader

Suggested change
OSCacheTTL: config.Duration(defaultOSCacheTTL),
OSCacheTTL: config.Duration(5 * time.Minute),

Comment on lines +17 to +45
const testOSRelease = `NAME="Telegraf Test OS"
ID=telegraftest
VERSION_ID="1.0"
PRETTY_NAME="Telegraf Test OS 1.0"
`

// setupOS points gopsutil at a synthetic os-release file via HOST_ETC.
// Kernel fields still come from the live uname syscall.
func setupOS(t testing.TB) bool {
t.Helper()
mockOSRelease(t, testOSRelease)
return true
}

func mockOSRelease(t testing.TB, content string) {
t.Helper()
etcDir := os.Getenv("HOST_ETC")
if etcDir == "" {
etcDir = filepath.Join(t.TempDir(), "etc")
require.NoError(t, os.MkdirAll(etcDir, 0750))
t.Setenv("HOST_ETC", etcDir)
}
writeOSRelease(t, etcDir, content)
}

func writeOSRelease(t testing.TB, etcDir, content string) {
t.Helper()
require.NoError(t, os.WriteFile(filepath.Join(etcDir, "os-release"), []byte(content), 0640))
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No! Simply create a testdata directory in the plugin, place the file with the required content there and use t.Setenv("HOST_ETC") to the testdata directory in the actual test!

Comment on lines +47 to +53
func newOSPlugin(ttl time.Duration) *System {
return &System{
Include: []string{"os"},
OSCacheTTL: config.Duration(ttl),
Log: &testutil.Logger{},
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please define this in the tests instead of doing it in a function. For a reviewer (or future coder) your indirection will hide the actual test setup and you need to navigate around in the code to find out what the test actually does.

Comment on lines +64 to +74
m, found := acc.Get("system_os")
require.True(t, found, "system_os metric not produced")

require.Equal(t, "linux", m.Fields["os"])
require.Equal(t, "telegraftest", m.Fields["platform"])
require.Empty(t, m.Fields["platform_family"])
require.Equal(t, "1.0", m.Fields["platform_version"])
require.IsType(t, "", m.Fields["kernel_version"])
require.NotEmpty(t, m.Fields["kernel_version"])
require.IsType(t, "", m.Fields["kernel_arch"])
require.NotEmpty(t, m.Fields["kernel_arch"])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please define the expected metric(s) of type []telegraf.Metric using metric.New and then use testutil.RequireMetricsEqual to verify the collected metric(s) match the expectation!

Same below.

Comment on lines +150 to +163
func BenchmarkGatherOS(b *testing.B) {
setupOS(b)

s := newOSPlugin(defaultOSCacheTTL)
require.NoError(b, s.Init())

var acc testutil.Accumulator
for b.Loop() {
acc.ClearMetrics()
if err := s.Gather(&acc); err != nil {
b.Fatal(err)
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need a benchmark here...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get rid of this. Simply limit the test to Linux and be good.

@srebhan srebhan changed the title feat(inputs.system): Add 'os' include group with platform and kernel information feat(inputs.system): Add operating-system information May 4, 2026
@srebhan srebhan self-assigned this May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/system feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin plugin/input 1. Request for new input plugins 2. Issues/PRs that are related to input plugins

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants