diff --git a/collector/fixtures/proc/sys/kernel/tainted b/collector/fixtures/proc/sys/kernel/tainted new file mode 100644 index 0000000000..63fce6863a --- /dev/null +++ b/collector/fixtures/proc/sys/kernel/tainted @@ -0,0 +1 @@ +12288 diff --git a/collector/tainted_linux.go b/collector/tainted_linux.go new file mode 100644 index 0000000000..0ad98ee4ca --- /dev/null +++ b/collector/tainted_linux.go @@ -0,0 +1,76 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "fmt" + "log/slog" + "strconv" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/procfs" +) + +type taintedCollector struct { + logger *slog.Logger + desc *prometheus.Desc +} + +func init() { + registerCollector("tainted", defaultDisabled, NewTaintedCollector) +} + +// NewTaintedCollector returns a Collector exposing kernel taint flags from +// /proc/sys/kernel/tainted as a labelled gauge. +// See https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html +func NewTaintedCollector(logger *slog.Logger) (Collector, error) { + return &taintedCollector{ + logger: logger, + desc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "kernel", "tainted"), + "Taint flags set on the running Linux kernel, as reported by /proc/sys/kernel/tainted. "+ + "Value is 1 if the flag is set, 0 otherwise. "+ + "See https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html for flag meanings.", + []string{"bit", "flag"}, + nil, + ), + }, nil +} + +func (c *taintedCollector) Update(ch chan<- prometheus.Metric) error { + fs, err := procfs.NewFS(*procPath) + if err != nil { + return fmt.Errorf("failed to open procfs: %w", err) + } + + tainted, err := fs.KernelTainted() + if err != nil { + return fmt.Errorf("couldn't read kernel tainted state: %w", err) + } + + for _, b := range tainted.Bits { + var val float64 + if b.Set { + val = 1.0 + } + ch <- prometheus.MustNewConstMetric( + c.desc, + prometheus.GaugeValue, + val, + strconv.Itoa(b.Index), + b.Flag, + ) + } + return nil +} diff --git a/collector/tainted_linux_test.go b/collector/tainted_linux_test.go new file mode 100644 index 0000000000..0ce7e3b621 --- /dev/null +++ b/collector/tainted_linux_test.go @@ -0,0 +1,103 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !notainted + +package collector + +import ( + "io" + "log/slog" + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestTaintedCollector(t *testing.T) { + *procPath = "fixtures/proc" + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + c, err := NewTaintedCollector(logger) + if err != nil { + t.Fatalf("failed to create tainted collector: %v", err) + } + + reg := prometheus.NewPedanticRegistry() + reg.MustRegister(&taintedCollectorWrapper{c.(*taintedCollector)}) + + mfs, err := reg.Gather() + if err != nil { + t.Fatalf("gather failed: %v", err) + } + if len(mfs) != 1 { + t.Fatalf("expected 1 metric family, got %d", len(mfs)) + } + + mf := mfs[0] + if got := mf.GetName(); got != "node_kernel_tainted" { + t.Errorf("metric name: want node_kernel_tainted, got %s", got) + } + + // Expect one series per known taint bit (20 defined by the kernel). + const wantBits = 20 + if got := len(mf.GetMetric()); got != wantBits { + t.Errorf("metric count: want %d, got %d", wantBits, got) + } + + // Build bit → value map for assertion. + // Fixture is 12288 = bit 12 (O) + bit 13 (E). + // Build flag → value map for assertion (labels: bit, flag). + flagVals := make(map[string]float64) + for _, m := range mf.GetMetric() { + // Each metric has exactly 2 labels: bit and flag. + for _, lp := range m.GetLabel() { + if lp.GetName() == "flag" { + flagVals[lp.GetValue()] = m.GetGauge().GetValue() + } + } + } + + // Fixture is 12288 = bit 12 (O) + bit 13 (E). + for _, tc := range []struct { + flag string + want float64 + }{ + {"O", 1}, // Externally-built (out-of-tree) module — set + {"E", 1}, // Unsigned module — set + {"L", 0}, // Soft lockup — must be clear + {"P", 0}, + {"T", 0}, + } { + got, ok := flagVals[tc.flag] + if !ok { + t.Errorf("flag %q not found in metrics", tc.flag) + continue + } + if got != tc.want { + t.Errorf("flag %q: want %.0f, got %.0f", tc.flag, tc.want, got) + } + } +} + +// taintedCollectorWrapper adapts taintedCollector to prometheus.Collector. +type taintedCollectorWrapper struct { + c *taintedCollector +} + +func (w *taintedCollectorWrapper) Describe(ch chan<- *prometheus.Desc) { + ch <- w.c.desc +} + +func (w *taintedCollectorWrapper) Collect(ch chan<- prometheus.Metric) { + _ = w.c.Update(ch) +} diff --git a/go.mod b/go.mod index 7c4458e399..67147b838c 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.70.1 github.com/prometheus/exporter-toolkit v0.17.1 - github.com/prometheus/procfs v0.21.1 + github.com/prometheus/procfs v0.21.2-0.20260717070424-0cd18237af6e github.com/safchain/ethtool v0.7.0 golang.org/x/sys v0.47.0 howett.net/plist v1.0.1 diff --git a/go.sum b/go.sum index 58c02fe429..295885f0ce 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/exporter-toolkit v0.17.1 h1:psKN4wM7shBL/BxZkDHgm6YZJ3fAVG36+r86An/+7q0= github.com/prometheus/exporter-toolkit v0.17.1/go.mod h1:dabwPJvxsC5+tsp2iolQrqBWZh+QlISKlYRpj9Hh5xk= -github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= -github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/prometheus/procfs v0.21.2-0.20260717070424-0cd18237af6e h1:sm1MuWauLFbQsFVJn/5vKTMrolG02N/DT10IPcCYJA4= +github.com/prometheus/procfs v0.21.2-0.20260717070424-0cd18237af6e/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/safchain/ethtool v0.7.0 h1:rlJzfDetsVvT61uz8x1YIcFn12akMfuPulHtZjtb7Is= github.com/safchain/ethtool v0.7.0/go.mod h1:MenQKEjXdfkjD3mp2QdCk8B/hwvkrlOTm/FD4gTpFxQ= github.com/siebenmann/go-kstat v0.0.0-20210513183136-173c9b0a9973 h1:GfSdC6wKfTGcgCS7BtzF5694Amne1pGCSTY252WhlEY=