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) + } +}