From 3f6d151f1b9805d1bc95c7256c5f326d457a3bb6 Mon Sep 17 00:00:00 2001 From: Kyle Mason Date: Wed, 5 Aug 2026 23:13:56 -0400 Subject: [PATCH] collector/slabinfo: index duplicate slab entries /proc/slabinfo can list the same slab name more than once, for example one cache per device instance. Labelling only by slab name therefore produces duplicate label sets, causing the registry to reject the scrape. Count the entries sharing each name first, then add an index label carrying each entry's position among them. Only names that actually repeat are disambiguated, so a host whose slab names are all unique keeps exactly the series it has today and does not have to change dashboards for a bug it never hit. Where a name does repeat, every kernel cache keeps its own series with its own counts and geometry. The label name matches the ordinal index convention already used by the rapl and sysctl collectors. The ordinal follows /proc/slabinfo ordering and is not a stable identity: if a cache is created or destroyed, subsequent entries shift. The e2e fixture gains a duplicated mlx5_fs_ftes entry so both label shapes appear side by side in the golden output. Fixes #3506 Signed-off-by: Kyle Mason --- collector/fixtures/e2e-64k-page-output.txt | 10 + collector/fixtures/e2e-output.txt | 10 + collector/fixtures/proc/slabinfo | 2 + collector/slabinfo_linux.go | 99 +++++-- collector/slabinfo_linux_test.go | 306 +++++++++++++++++++++ 5 files changed, 397 insertions(+), 30 deletions(-) create mode 100644 collector/slabinfo_linux_test.go diff --git a/collector/fixtures/e2e-64k-page-output.txt b/collector/fixtures/e2e-64k-page-output.txt index f0ac7ebac4..7bda690b16 100644 --- a/collector/fixtures/e2e-64k-page-output.txt +++ b/collector/fixtures/e2e-64k-page-output.txt @@ -3778,30 +3778,40 @@ node_slabinfo_active_objects{slab="dmaengine-unmap-128"} 1206 node_slabinfo_active_objects{slab="kmalloc-8192"} 132 node_slabinfo_active_objects{slab="kmem_cache"} 320 node_slabinfo_active_objects{slab="tw_sock_TCP"} 704 +node_slabinfo_active_objects{index="0",slab="mlx5_fs_ftes"} 44 +node_slabinfo_active_objects{index="1",slab="mlx5_fs_ftes"} 396 # HELP node_slabinfo_object_size_bytes The size of objects in this slab, in bytes. # TYPE node_slabinfo_object_size_bytes gauge node_slabinfo_object_size_bytes{slab="dmaengine-unmap-128"} 1088 node_slabinfo_object_size_bytes{slab="kmalloc-8192"} 8192 node_slabinfo_object_size_bytes{slab="kmem_cache"} 256 node_slabinfo_object_size_bytes{slab="tw_sock_TCP"} 256 +node_slabinfo_object_size_bytes{index="0",slab="mlx5_fs_ftes"} 736 +node_slabinfo_object_size_bytes{index="1",slab="mlx5_fs_ftes"} 736 # HELP node_slabinfo_objects The total number of allocated objects (i.e., objects that are both in use and not in use). # TYPE node_slabinfo_objects gauge node_slabinfo_objects{slab="dmaengine-unmap-128"} 1320 node_slabinfo_objects{slab="kmalloc-8192"} 148 node_slabinfo_objects{slab="kmem_cache"} 320 node_slabinfo_objects{slab="tw_sock_TCP"} 864 +node_slabinfo_objects{index="0",slab="mlx5_fs_ftes"} 44 +node_slabinfo_objects{index="1",slab="mlx5_fs_ftes"} 396 # HELP node_slabinfo_objects_per_slab The number of objects stored in each slab. # TYPE node_slabinfo_objects_per_slab gauge node_slabinfo_objects_per_slab{slab="dmaengine-unmap-128"} 30 node_slabinfo_objects_per_slab{slab="kmalloc-8192"} 4 node_slabinfo_objects_per_slab{slab="kmem_cache"} 32 node_slabinfo_objects_per_slab{slab="tw_sock_TCP"} 32 +node_slabinfo_objects_per_slab{index="0",slab="mlx5_fs_ftes"} 44 +node_slabinfo_objects_per_slab{index="1",slab="mlx5_fs_ftes"} 44 # HELP node_slabinfo_pages_per_slab The number of pages allocated for each slab. # TYPE node_slabinfo_pages_per_slab gauge node_slabinfo_pages_per_slab{slab="dmaengine-unmap-128"} 8 node_slabinfo_pages_per_slab{slab="kmalloc-8192"} 8 node_slabinfo_pages_per_slab{slab="kmem_cache"} 2 node_slabinfo_pages_per_slab{slab="tw_sock_TCP"} 2 +node_slabinfo_pages_per_slab{index="0",slab="mlx5_fs_ftes"} 8 +node_slabinfo_pages_per_slab{index="1",slab="mlx5_fs_ftes"} 8 # HELP node_sockstat_FRAG6_inuse Number of FRAG6 sockets in state inuse. # TYPE node_sockstat_FRAG6_inuse gauge node_sockstat_FRAG6_inuse 0 diff --git a/collector/fixtures/e2e-output.txt b/collector/fixtures/e2e-output.txt index 9afd903d6f..a900357b13 100644 --- a/collector/fixtures/e2e-output.txt +++ b/collector/fixtures/e2e-output.txt @@ -3810,30 +3810,40 @@ node_slabinfo_active_objects{slab="dmaengine-unmap-128"} 1206 node_slabinfo_active_objects{slab="kmalloc-8192"} 132 node_slabinfo_active_objects{slab="kmem_cache"} 320 node_slabinfo_active_objects{slab="tw_sock_TCP"} 704 +node_slabinfo_active_objects{index="0",slab="mlx5_fs_ftes"} 44 +node_slabinfo_active_objects{index="1",slab="mlx5_fs_ftes"} 396 # HELP node_slabinfo_object_size_bytes The size of objects in this slab, in bytes. # TYPE node_slabinfo_object_size_bytes gauge node_slabinfo_object_size_bytes{slab="dmaengine-unmap-128"} 1088 node_slabinfo_object_size_bytes{slab="kmalloc-8192"} 8192 node_slabinfo_object_size_bytes{slab="kmem_cache"} 256 node_slabinfo_object_size_bytes{slab="tw_sock_TCP"} 256 +node_slabinfo_object_size_bytes{index="0",slab="mlx5_fs_ftes"} 736 +node_slabinfo_object_size_bytes{index="1",slab="mlx5_fs_ftes"} 736 # HELP node_slabinfo_objects The total number of allocated objects (i.e., objects that are both in use and not in use). # TYPE node_slabinfo_objects gauge node_slabinfo_objects{slab="dmaengine-unmap-128"} 1320 node_slabinfo_objects{slab="kmalloc-8192"} 148 node_slabinfo_objects{slab="kmem_cache"} 320 node_slabinfo_objects{slab="tw_sock_TCP"} 864 +node_slabinfo_objects{index="0",slab="mlx5_fs_ftes"} 44 +node_slabinfo_objects{index="1",slab="mlx5_fs_ftes"} 396 # HELP node_slabinfo_objects_per_slab The number of objects stored in each slab. # TYPE node_slabinfo_objects_per_slab gauge node_slabinfo_objects_per_slab{slab="dmaengine-unmap-128"} 30 node_slabinfo_objects_per_slab{slab="kmalloc-8192"} 4 node_slabinfo_objects_per_slab{slab="kmem_cache"} 32 node_slabinfo_objects_per_slab{slab="tw_sock_TCP"} 32 +node_slabinfo_objects_per_slab{index="0",slab="mlx5_fs_ftes"} 44 +node_slabinfo_objects_per_slab{index="1",slab="mlx5_fs_ftes"} 44 # HELP node_slabinfo_pages_per_slab The number of pages allocated for each slab. # TYPE node_slabinfo_pages_per_slab gauge node_slabinfo_pages_per_slab{slab="dmaengine-unmap-128"} 8 node_slabinfo_pages_per_slab{slab="kmalloc-8192"} 8 node_slabinfo_pages_per_slab{slab="kmem_cache"} 2 node_slabinfo_pages_per_slab{slab="tw_sock_TCP"} 2 +node_slabinfo_pages_per_slab{index="0",slab="mlx5_fs_ftes"} 8 +node_slabinfo_pages_per_slab{index="1",slab="mlx5_fs_ftes"} 8 # HELP node_sockstat_FRAG6_inuse Number of FRAG6 sockets in state inuse. # TYPE node_sockstat_FRAG6_inuse gauge node_sockstat_FRAG6_inuse 0 diff --git a/collector/fixtures/proc/slabinfo b/collector/fixtures/proc/slabinfo index 8f2de4ba11..a3699d9201 100644 --- a/collector/fixtures/proc/slabinfo +++ b/collector/fixtures/proc/slabinfo @@ -2,5 +2,7 @@ slabinfo - version: 2.1 # name : tunables : slabdata tw_sock_TCP 704 864 256 32 2 : tunables 0 0 0 : slabdata 27 27 0 dmaengine-unmap-128 1206 1320 1088 30 8 : tunables 0 0 0 : slabdata 44 44 0 +mlx5_fs_ftes 44 44 736 44 8 : tunables 0 0 0 : slabdata 1 1 0 +mlx5_fs_ftes 396 396 736 44 8 : tunables 0 0 0 : slabdata 9 9 0 kmalloc-8192 132 148 8192 4 8 : tunables 0 0 0 : slabdata 37 37 0 kmem_cache 320 320 256 32 2 : tunables 0 0 0 : slabdata 10 10 0 diff --git a/collector/slabinfo_linux.go b/collector/slabinfo_linux.go index 48bbf2abfe..8e439ef102 100644 --- a/collector/slabinfo_linux.go +++ b/collector/slabinfo_linux.go @@ -18,6 +18,7 @@ package collector import ( "fmt" "log/slog" + "strconv" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" @@ -34,6 +35,7 @@ type slabinfoCollector struct { logger *slog.Logger subsystem string labels []string + indexedLabels []string slabNameFilter deviceFilter } @@ -51,6 +53,7 @@ func NewSlabinfoCollector(logger *slog.Logger) (Collector, error) { fs: fs, subsystem: "slabinfo", labels: []string{"slab"}, + indexedLabels: []string{"slab", "index"}, slabNameFilter: newDeviceFilter(*slabNameExclude, *slabNameInclude), }, nil } @@ -61,71 +64,107 @@ func (c *slabinfoCollector) Update(ch chan<- prometheus.Metric) error { return fmt.Errorf("couldn't get %s: %w", c.subsystem, err) } + // /proc/slabinfo can list the same slab name more than once, for example one + // cache per device instance. The kernel permits this: the duplicate-name check + // in kmem_cache_sanity_check() only WARNs, and is compiled out unless + // CONFIG_DEBUG_VM is set. Labelling by name alone therefore produces duplicate + // label sets, which fails the entire scrape. + // + // Pass 1: count the entries sharing each name. The filter matches on name, so + // every entry with a given name is kept or dropped together and counting ahead + // of it gives the same answer. + counts := make(map[string]int, len(slabinfo.Slabs)) + for _, slab := range slabinfo.Slabs { + counts[slab.Name]++ + } + + // Pass 2: emit. Only names that actually collide are disambiguated, so a host + // whose slab names are all unique keeps exactly the series it had before. Where + // a name does repeat, each entry is given its position among the entries + // sharing that name, so every cache keeps its own series and its own geometry. + // The ordinal reflects the order of /proc/slabinfo and is not a stable + // identity: if a cache is created or destroyed, subsequent entries shift. + seen := make(map[string]int, len(counts)) for _, slab := range slabinfo.Slabs { if c.slabNameFilter.ignored(slab.Name) { continue } - ch <- c.activeObjects(slab.Name, slab.ObjActive) - ch <- c.objects(slab.Name, slab.ObjNum) - ch <- c.objectSizeBytes(slab.Name, slab.ObjSize) - ch <- c.objectsPerSlab(slab.Name, slab.ObjPerSlab) - ch <- c.pagesPerSlab(slab.Name, slab.PagesPerSlab) + labelValues := []string{slab.Name} + if counts[slab.Name] > 1 { + labelValues = append(labelValues, strconv.Itoa(seen[slab.Name])) + seen[slab.Name]++ + } + ch <- c.activeObjects(labelValues, slab.ObjActive) + ch <- c.objects(labelValues, slab.ObjNum) + ch <- c.objectSizeBytes(labelValues, slab.ObjSize) + ch <- c.objectsPerSlab(labelValues, slab.ObjPerSlab) + ch <- c.pagesPerSlab(labelValues, slab.PagesPerSlab) } return nil } -func (c *slabinfoCollector) activeObjects(label string, val int64) prometheus.Metric { - desc := prometheus.NewDesc( - prometheus.BuildFQName(namespace, c.subsystem, "active_objects"), +// desc builds the descriptor for one slabinfo column. Entries whose name is +// unique are described by {slab}, the label set this collector has always used; +// only entries carrying an ordinal are described by {slab,index}. +func (c *slabinfoCollector) desc(name, help string, labelValues []string) *prometheus.Desc { + labels := c.labels + if len(labelValues) > len(c.labels) { + labels = c.indexedLabels + } + + return prometheus.NewDesc( + prometheus.BuildFQName(namespace, c.subsystem, name), + help, + labels, nil) +} + +func (c *slabinfoCollector) activeObjects(labelValues []string, val int64) prometheus.Metric { + desc := c.desc("active_objects", "The number of objects that are currently active (i.e., in use).", - c.labels, nil) + labelValues) return prometheus.MustNewConstMetric( - desc, prometheus.GaugeValue, float64(val), label, + desc, prometheus.GaugeValue, float64(val), labelValues..., ) } -func (c *slabinfoCollector) objects(label string, val int64) prometheus.Metric { - desc := prometheus.NewDesc( - prometheus.BuildFQName(namespace, c.subsystem, "objects"), +func (c *slabinfoCollector) objects(labelValues []string, val int64) prometheus.Metric { + desc := c.desc("objects", "The total number of allocated objects (i.e., objects that are both in use and not in use).", - c.labels, nil) + labelValues) return prometheus.MustNewConstMetric( - desc, prometheus.GaugeValue, float64(val), label, + desc, prometheus.GaugeValue, float64(val), labelValues..., ) } -func (c *slabinfoCollector) objectSizeBytes(label string, val int64) prometheus.Metric { - desc := prometheus.NewDesc( - prometheus.BuildFQName(namespace, c.subsystem, "object_size_bytes"), +func (c *slabinfoCollector) objectSizeBytes(labelValues []string, val int64) prometheus.Metric { + desc := c.desc("object_size_bytes", "The size of objects in this slab, in bytes.", - c.labels, nil) + labelValues) return prometheus.MustNewConstMetric( - desc, prometheus.GaugeValue, float64(val), label, + desc, prometheus.GaugeValue, float64(val), labelValues..., ) } -func (c *slabinfoCollector) objectsPerSlab(label string, val int64) prometheus.Metric { - desc := prometheus.NewDesc( - prometheus.BuildFQName(namespace, c.subsystem, "objects_per_slab"), +func (c *slabinfoCollector) objectsPerSlab(labelValues []string, val int64) prometheus.Metric { + desc := c.desc("objects_per_slab", "The number of objects stored in each slab.", - c.labels, nil) + labelValues) return prometheus.MustNewConstMetric( - desc, prometheus.GaugeValue, float64(val), label, + desc, prometheus.GaugeValue, float64(val), labelValues..., ) } -func (c *slabinfoCollector) pagesPerSlab(label string, val int64) prometheus.Metric { - desc := prometheus.NewDesc( - prometheus.BuildFQName(namespace, c.subsystem, "pages_per_slab"), +func (c *slabinfoCollector) pagesPerSlab(labelValues []string, val int64) prometheus.Metric { + desc := c.desc("pages_per_slab", "The number of pages allocated for each slab.", - c.labels, nil) + labelValues) return prometheus.MustNewConstMetric( - desc, prometheus.GaugeValue, float64(val), label, + desc, prometheus.GaugeValue, float64(val), labelValues..., ) } diff --git a/collector/slabinfo_linux_test.go b/collector/slabinfo_linux_test.go new file mode 100644 index 0000000000..7bdbf4c46a --- /dev/null +++ b/collector/slabinfo_linux_test.go @@ -0,0 +1,306 @@ +// 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 !noslabinfo + +package collector + +import ( + "io" + "log/slog" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" +) + +const slabinfoHeader = `slabinfo - version: 2.1 +# name : tunables : slabdata +` + +// setProcPath points the collector at dir for the duration of the test. procPath +// is a package-global flag value, so it is restored afterwards to keep these +// tests independent of execution order. These tests must not call t.Parallel() +// for the same reason. +func setProcPath(t *testing.T, dir string) { + t.Helper() + orig := *procPath + t.Cleanup(func() { *procPath = orig }) + *procPath = dir +} + +// writeSlabinfo writes body into a temporary procfs and points the collector at +// it for the duration of the test. +func writeSlabinfo(t *testing.T, body string) { + t.Helper() + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "slabinfo"), []byte(slabinfoHeader+body), 0o644); err != nil { + t.Fatal(err) + } + setProcPath(t, dir) +} + +// slabinfoTestCollector adapts the collector to prometheus.Collector so the +// metrics can be gathered through a registry, which is what surfaces duplicate +// label sets. +// +// Describe is deliberately empty, which registers this as an unchecked +// collector. Because the index label is conditional, one metric family can now +// carry both the {slab} and the {slab,index} descriptor, and Register rejects +// two descriptors that share a fully-qualified name but declare different label +// names: +// +// descriptors reported by collector have inconsistent label names or help +// strings for the same fully-qualified name +// +// Gathering mixed label sets is fine; only the Describe-time check objects. +// Production never reaches it because NodeCollector.Describe reports just the +// two scrape descriptors. Do not "fix" this by describing through Collect +// (prometheus.DescribeByCollect or prometheus.CollectorFunc): that reintroduces +// the registration failure. +type slabinfoTestCollector struct { + t *testing.T + c Collector +} + +func (slabinfoTestCollector) Describe(chan<- *prometheus.Desc) {} + +func (tc slabinfoTestCollector) Collect(ch chan<- prometheus.Metric) { + if err := tc.c.Update(ch); err != nil { + tc.t.Errorf("Update failed: %v", err) + } +} + +// expectSlabinfo gathers the collector and compares the complete exposition, so +// extra series, missing series, wrong labels, wrong values and changed HELP or +// TYPE lines all fail. +func expectSlabinfo(t *testing.T, want string) { + t.Helper() + c, err := NewSlabinfoCollector(slog.New(slog.NewTextHandler(io.Discard, nil))) + if err != nil { + t.Fatal(err) + } + + reg := prometheus.NewRegistry() + reg.MustRegister(slabinfoTestCollector{t: t, c: c}) + + if err := testutil.GatherAndCompare(reg, strings.NewReader(want)); err != nil { + t.Error(err) + } +} + +// Slab names that occur once must keep the exact identity they had before +// duplicate handling existed: bare {slab}, with no index label anywhere. This is +// the guarantee the whole change rests on, mirroring +// TestHwmonUniqueChipNamesAreUnchanged. +func TestSlabinfoUniqueSlabNamesAreUnchanged(t *testing.T) { + writeSlabinfo(t, `tw_sock_TCP 704 864 256 32 2 : tunables 0 0 0 : slabdata 27 27 0 +kmem_cache 320 320 256 32 2 : tunables 0 0 0 : slabdata 10 10 0 +`) + expectSlabinfo(t, `# HELP node_slabinfo_active_objects The number of objects that are currently active (i.e., in use). +# TYPE node_slabinfo_active_objects gauge +node_slabinfo_active_objects{slab="kmem_cache"} 320 +node_slabinfo_active_objects{slab="tw_sock_TCP"} 704 +# HELP node_slabinfo_object_size_bytes The size of objects in this slab, in bytes. +# TYPE node_slabinfo_object_size_bytes gauge +node_slabinfo_object_size_bytes{slab="kmem_cache"} 256 +node_slabinfo_object_size_bytes{slab="tw_sock_TCP"} 256 +# HELP node_slabinfo_objects The total number of allocated objects (i.e., objects that are both in use and not in use). +# TYPE node_slabinfo_objects gauge +node_slabinfo_objects{slab="kmem_cache"} 320 +node_slabinfo_objects{slab="tw_sock_TCP"} 864 +# HELP node_slabinfo_objects_per_slab The number of objects stored in each slab. +# TYPE node_slabinfo_objects_per_slab gauge +node_slabinfo_objects_per_slab{slab="kmem_cache"} 32 +node_slabinfo_objects_per_slab{slab="tw_sock_TCP"} 32 +# HELP node_slabinfo_pages_per_slab The number of pages allocated for each slab. +# TYPE node_slabinfo_pages_per_slab gauge +node_slabinfo_pages_per_slab{slab="kmem_cache"} 2 +node_slabinfo_pages_per_slab{slab="tw_sock_TCP"} 2 +`) +} + +// A slab name may appear more than once, for example one cache per device +// instance. Each entry then keeps its own series, distinguished by an ordinal in +// /proc/slabinfo order. +func TestSlabinfoDuplicateSlabNames(t *testing.T) { + writeSlabinfo(t, `mlx5_fs_ftes 44 44 736 44 8 : tunables 0 0 0 : slabdata 1 1 0 +mlx5_fs_ftes 396 396 736 44 8 : tunables 0 0 0 : slabdata 9 9 0 +mlx5_fs_ftes 792 792 736 44 8 : tunables 0 0 0 : slabdata 18 18 0 +`) + expectSlabinfo(t, `# HELP node_slabinfo_active_objects The number of objects that are currently active (i.e., in use). +# TYPE node_slabinfo_active_objects gauge +node_slabinfo_active_objects{index="0",slab="mlx5_fs_ftes"} 44 +node_slabinfo_active_objects{index="1",slab="mlx5_fs_ftes"} 396 +node_slabinfo_active_objects{index="2",slab="mlx5_fs_ftes"} 792 +# HELP node_slabinfo_object_size_bytes The size of objects in this slab, in bytes. +# TYPE node_slabinfo_object_size_bytes gauge +node_slabinfo_object_size_bytes{index="0",slab="mlx5_fs_ftes"} 736 +node_slabinfo_object_size_bytes{index="1",slab="mlx5_fs_ftes"} 736 +node_slabinfo_object_size_bytes{index="2",slab="mlx5_fs_ftes"} 736 +# HELP node_slabinfo_objects The total number of allocated objects (i.e., objects that are both in use and not in use). +# TYPE node_slabinfo_objects gauge +node_slabinfo_objects{index="0",slab="mlx5_fs_ftes"} 44 +node_slabinfo_objects{index="1",slab="mlx5_fs_ftes"} 396 +node_slabinfo_objects{index="2",slab="mlx5_fs_ftes"} 792 +# HELP node_slabinfo_objects_per_slab The number of objects stored in each slab. +# TYPE node_slabinfo_objects_per_slab gauge +node_slabinfo_objects_per_slab{index="0",slab="mlx5_fs_ftes"} 44 +node_slabinfo_objects_per_slab{index="1",slab="mlx5_fs_ftes"} 44 +node_slabinfo_objects_per_slab{index="2",slab="mlx5_fs_ftes"} 44 +# HELP node_slabinfo_pages_per_slab The number of pages allocated for each slab. +# TYPE node_slabinfo_pages_per_slab gauge +node_slabinfo_pages_per_slab{index="0",slab="mlx5_fs_ftes"} 8 +node_slabinfo_pages_per_slab{index="1",slab="mlx5_fs_ftes"} 8 +node_slabinfo_pages_per_slab{index="2",slab="mlx5_fs_ftes"} 8 +`) +} + +// Unique and duplicated names coexist inside the same metric family, one carrying +// the index label and one not. This is the mixed-label-set case: a registry +// accepts it at gather time even though the family holds two label shapes. +func TestSlabinfoMixedUniqueAndDuplicateSlabNames(t *testing.T) { + writeSlabinfo(t, `kmem_cache 320 320 256 32 2 : tunables 0 0 0 : slabdata 10 10 0 +mlx5_fs_fgs 42 42 776 42 8 : tunables 0 0 0 : slabdata 1 1 0 +mlx5_fs_fgs 84 84 776 42 8 : tunables 0 0 0 : slabdata 2 2 0 +tw_sock_TCP 704 864 256 32 2 : tunables 0 0 0 : slabdata 27 27 0 +`) + expectSlabinfo(t, `# HELP node_slabinfo_active_objects The number of objects that are currently active (i.e., in use). +# TYPE node_slabinfo_active_objects gauge +node_slabinfo_active_objects{slab="kmem_cache"} 320 +node_slabinfo_active_objects{index="0",slab="mlx5_fs_fgs"} 42 +node_slabinfo_active_objects{index="1",slab="mlx5_fs_fgs"} 84 +node_slabinfo_active_objects{slab="tw_sock_TCP"} 704 +# HELP node_slabinfo_object_size_bytes The size of objects in this slab, in bytes. +# TYPE node_slabinfo_object_size_bytes gauge +node_slabinfo_object_size_bytes{slab="kmem_cache"} 256 +node_slabinfo_object_size_bytes{index="0",slab="mlx5_fs_fgs"} 776 +node_slabinfo_object_size_bytes{index="1",slab="mlx5_fs_fgs"} 776 +node_slabinfo_object_size_bytes{slab="tw_sock_TCP"} 256 +# HELP node_slabinfo_objects The total number of allocated objects (i.e., objects that are both in use and not in use). +# TYPE node_slabinfo_objects gauge +node_slabinfo_objects{slab="kmem_cache"} 320 +node_slabinfo_objects{index="0",slab="mlx5_fs_fgs"} 42 +node_slabinfo_objects{index="1",slab="mlx5_fs_fgs"} 84 +node_slabinfo_objects{slab="tw_sock_TCP"} 864 +# HELP node_slabinfo_objects_per_slab The number of objects stored in each slab. +# TYPE node_slabinfo_objects_per_slab gauge +node_slabinfo_objects_per_slab{slab="kmem_cache"} 32 +node_slabinfo_objects_per_slab{index="0",slab="mlx5_fs_fgs"} 42 +node_slabinfo_objects_per_slab{index="1",slab="mlx5_fs_fgs"} 42 +node_slabinfo_objects_per_slab{slab="tw_sock_TCP"} 32 +# HELP node_slabinfo_pages_per_slab The number of pages allocated for each slab. +# TYPE node_slabinfo_pages_per_slab gauge +node_slabinfo_pages_per_slab{slab="kmem_cache"} 2 +node_slabinfo_pages_per_slab{index="0",slab="mlx5_fs_fgs"} 8 +node_slabinfo_pages_per_slab{index="1",slab="mlx5_fs_fgs"} 8 +node_slabinfo_pages_per_slab{slab="tw_sock_TCP"} 2 +`) +} + +// The kernel does not guarantee that same-named caches share geometry: the +// duplicate-name check in kmem_cache_sanity_check() only WARNs and is compiled +// out without CONFIG_DEBUG_VM, and object_size is per-cache. Indexing keeps each +// cache's own geometry, so no representative value has to be chosen. +func TestSlabinfoDuplicateSlabNamesDifferentGeometry(t *testing.T) { + writeSlabinfo(t, `weird_cache 10 10 128 32 2 : tunables 0 0 0 : slabdata 1 1 0 +weird_cache 20 20 256 16 4 : tunables 0 0 0 : slabdata 2 2 0 +`) + expectSlabinfo(t, `# HELP node_slabinfo_active_objects The number of objects that are currently active (i.e., in use). +# TYPE node_slabinfo_active_objects gauge +node_slabinfo_active_objects{index="0",slab="weird_cache"} 10 +node_slabinfo_active_objects{index="1",slab="weird_cache"} 20 +# HELP node_slabinfo_object_size_bytes The size of objects in this slab, in bytes. +# TYPE node_slabinfo_object_size_bytes gauge +node_slabinfo_object_size_bytes{index="0",slab="weird_cache"} 128 +node_slabinfo_object_size_bytes{index="1",slab="weird_cache"} 256 +# HELP node_slabinfo_objects The total number of allocated objects (i.e., objects that are both in use and not in use). +# TYPE node_slabinfo_objects gauge +node_slabinfo_objects{index="0",slab="weird_cache"} 10 +node_slabinfo_objects{index="1",slab="weird_cache"} 20 +# HELP node_slabinfo_objects_per_slab The number of objects stored in each slab. +# TYPE node_slabinfo_objects_per_slab gauge +node_slabinfo_objects_per_slab{index="0",slab="weird_cache"} 32 +node_slabinfo_objects_per_slab{index="1",slab="weird_cache"} 16 +# HELP node_slabinfo_pages_per_slab The number of pages allocated for each slab. +# TYPE node_slabinfo_pages_per_slab gauge +node_slabinfo_pages_per_slab{index="0",slab="weird_cache"} 2 +node_slabinfo_pages_per_slab{index="1",slab="weird_cache"} 4 +`) +} + +// The filter matches on slab name, so every entry sharing a name is kept or +// dropped together. An excluded name contributes no series at all, and excluding +// it leaves the collision handling of the names that remain untouched: a +// duplicated name is still indexed and a unique name is still bare. +func TestSlabinfoExcludedSlabsAreNotCollected(t *testing.T) { + origExclude := *slabNameExclude + t.Cleanup(func() { *slabNameExclude = origExclude }) + *slabNameExclude = "^drop_me$" + + writeSlabinfo(t, `keep_me 10 10 128 32 2 : tunables 0 0 0 : slabdata 1 1 0 +drop_me 99 99 999 99 9 : tunables 0 0 0 : slabdata 9 9 0 +drop_me 98 98 999 99 9 : tunables 0 0 0 : slabdata 8 8 0 +dup_cache 20 20 128 32 2 : tunables 0 0 0 : slabdata 2 2 0 +dup_cache 30 30 128 32 2 : tunables 0 0 0 : slabdata 3 3 0 +`) + expectSlabinfo(t, `# HELP node_slabinfo_active_objects The number of objects that are currently active (i.e., in use). +# TYPE node_slabinfo_active_objects gauge +node_slabinfo_active_objects{index="0",slab="dup_cache"} 20 +node_slabinfo_active_objects{index="1",slab="dup_cache"} 30 +node_slabinfo_active_objects{slab="keep_me"} 10 +# HELP node_slabinfo_object_size_bytes The size of objects in this slab, in bytes. +# TYPE node_slabinfo_object_size_bytes gauge +node_slabinfo_object_size_bytes{index="0",slab="dup_cache"} 128 +node_slabinfo_object_size_bytes{index="1",slab="dup_cache"} 128 +node_slabinfo_object_size_bytes{slab="keep_me"} 128 +# HELP node_slabinfo_objects The total number of allocated objects (i.e., objects that are both in use and not in use). +# TYPE node_slabinfo_objects gauge +node_slabinfo_objects{index="0",slab="dup_cache"} 20 +node_slabinfo_objects{index="1",slab="dup_cache"} 30 +node_slabinfo_objects{slab="keep_me"} 10 +# HELP node_slabinfo_objects_per_slab The number of objects stored in each slab. +# TYPE node_slabinfo_objects_per_slab gauge +node_slabinfo_objects_per_slab{index="0",slab="dup_cache"} 32 +node_slabinfo_objects_per_slab{index="1",slab="dup_cache"} 32 +node_slabinfo_objects_per_slab{slab="keep_me"} 32 +# HELP node_slabinfo_pages_per_slab The number of pages allocated for each slab. +# TYPE node_slabinfo_pages_per_slab gauge +node_slabinfo_pages_per_slab{index="0",slab="dup_cache"} 2 +node_slabinfo_pages_per_slab{index="1",slab="dup_cache"} 2 +node_slabinfo_pages_per_slab{slab="keep_me"} 2 +`) +} + +// An unreadable /proc/slabinfo must fail the collector rather than report a +// partial scrape. +func TestSlabinfoNoSlabinfoFile(t *testing.T) { + setProcPath(t, t.TempDir()) + + c, err := NewSlabinfoCollector(slog.New(slog.NewTextHandler(io.Discard, nil))) + if err != nil { + t.Fatal(err) + } + + ch := make(chan prometheus.Metric, 1) + if err := c.Update(ch); err == nil { + t.Fatal("expected an error when /proc/slabinfo is missing, got nil") + } + close(ch) + if n := len(ch); n != 0 { + t.Errorf("expected no metrics, got %d", n) + } +}