From ea537b101e8deaa2fe0daa8954b920875843f71d Mon Sep 17 00:00:00 2001 From: pittu sharma Date: Mon, 30 Mar 2026 18:54:50 +0530 Subject: [PATCH] collector: Add CIFS metrics collector (#987) Signed-off-by: pittu sharma --- README.md | 1 + collector/cifs_linux.go | 233 ++++++++++++++++++++++++++ collector/cifs_linux_test.go | 93 ++++++++++ collector/fixtures/proc/fs/cifs/Stats | 19 +++ 4 files changed, 346 insertions(+) create mode 100644 collector/cifs_linux.go create mode 100644 collector/cifs_linux_test.go create mode 100644 collector/fixtures/proc/fs/cifs/Stats diff --git a/README.md b/README.md index 0931584f..875426b5 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ Name | Description | OS ---------|-------------|---- buddyinfo | Exposes statistics of memory fragments as reported by /proc/buddyinfo. | Linux cgroups | A summary of the number of active and enabled cgroups | Linux +cifs | Exposes CIFS client statistics from `/proc/fs/cifs/Stats`. | Linux cpu\_vulnerabilities | Exposes CPU vulnerability information from sysfs. | Linux devstat | Exposes device statistics | Dragonfly, FreeBSD drm | Expose GPU metrics using sysfs / DRM, `amdgpu` is the only driver which exposes this information through DRM | Linux diff --git a/collector/cifs_linux.go b/collector/cifs_linux.go new file mode 100644 index 00000000..b13c4b7b --- /dev/null +++ b/collector/cifs_linux.go @@ -0,0 +1,233 @@ +// Copyright 2026 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 !nocifs + +package collector + +import ( + "bufio" + "errors" + "fmt" + "log/slog" + "os" + "strconv" + "strings" + + "github.com/prometheus/client_golang/prometheus" +) + +const cifsSubsystem = "cifs" + +type cifsCollector struct { + logger *slog.Logger + + sessionsDesc *prometheus.Desc + sharesDesc *prometheus.Desc + sessionReconnectsDesc *prometheus.Desc + shareReconnectsDesc *prometheus.Desc + vfsOpsDesc *prometheus.Desc + smbsDesc *prometheus.Desc + readBytesDesc *prometheus.Desc + writeBytesDesc *prometheus.Desc +} + +func init() { + registerCollector("cifs", defaultDisabled, NewCIFSCollector) +} + +// NewCIFSCollector returns a new Collector exposing CIFS client statistics. +func NewCIFSCollector(logger *slog.Logger) (Collector, error) { + return &cifsCollector{ + logger: logger, + sessionsDesc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, cifsSubsystem, "sessions"), + "Number of active CIFS sessions.", + nil, nil, + ), + sharesDesc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, cifsSubsystem, "shares"), + "Number of unique mount targets.", + nil, nil, + ), + sessionReconnectsDesc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, cifsSubsystem, "session_reconnects_total"), + "Total number of CIFS session reconnects.", + nil, nil, + ), + shareReconnectsDesc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, cifsSubsystem, "share_reconnects_total"), + "Total number of CIFS share reconnects.", + nil, nil, + ), + vfsOpsDesc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, cifsSubsystem, "vfs_operations_total"), + "Total number of VFS operations.", + nil, nil, + ), + smbsDesc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, cifsSubsystem, "smbs_total"), + "Total number of SMBs sent for this share.", + []string{"share"}, nil, + ), + readBytesDesc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, cifsSubsystem, "read_bytes_total"), + "Total bytes read from this share.", + []string{"share"}, nil, + ), + writeBytesDesc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, cifsSubsystem, "write_bytes_total"), + "Total bytes written to this share.", + []string{"share"}, nil, + ), + }, nil +} + +func (c *cifsCollector) Update(ch chan<- prometheus.Metric) error { + stats, err := parseCIFSStats(procFilePath("fs/cifs/Stats")) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + c.logger.Debug("Not collecting CIFS metrics", "err", err) + return ErrNoData + } + return fmt.Errorf("failed to read CIFS stats: %w", err) + } + + ch <- prometheus.MustNewConstMetric(c.sessionsDesc, prometheus.GaugeValue, stats.sessions) + ch <- prometheus.MustNewConstMetric(c.sharesDesc, prometheus.GaugeValue, stats.shares) + ch <- prometheus.MustNewConstMetric(c.sessionReconnectsDesc, prometheus.CounterValue, stats.sessionReconnects) + ch <- prometheus.MustNewConstMetric(c.shareReconnectsDesc, prometheus.CounterValue, stats.shareReconnects) + ch <- prometheus.MustNewConstMetric(c.vfsOpsDesc, prometheus.CounterValue, stats.vfsOps) + + for _, share := range stats.perShare { + ch <- prometheus.MustNewConstMetric(c.smbsDesc, prometheus.CounterValue, share.smbs, share.name) + ch <- prometheus.MustNewConstMetric(c.readBytesDesc, prometheus.CounterValue, share.readBytes, share.name) + ch <- prometheus.MustNewConstMetric(c.writeBytesDesc, prometheus.CounterValue, share.writeBytes, share.name) + } + + return nil +} + +type cifsStats struct { + sessions float64 + shares float64 + sessionReconnects float64 + shareReconnects float64 + vfsOps float64 + perShare []cifsShareStats +} + +type cifsShareStats struct { + name string + smbs float64 + readBytes float64 + writeBytes float64 +} + +func parseCIFSStats(path string) (*cifsStats, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + stats := &cifsStats{} + scanner := bufio.NewScanner(f) + + var currentShare string + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + + fields := strings.Fields(line) + + if strings.HasPrefix(line, "CIFS Session:") && len(fields) >= 3 { + v, err := strconv.ParseFloat(fields[2], 64) + if err == nil { + stats.sessions = v + } + continue + } + + if strings.HasPrefix(line, "Share (unique mount targets):") && len(fields) >= 5 { + v, err := strconv.ParseFloat(fields[4], 64) + if err == nil { + stats.shares = v + } + continue + } + + if strings.HasSuffix(line, "share reconnects") && len(fields) >= 4 { + v, err := strconv.ParseFloat(fields[0], 64) + if err == nil { + stats.sessionReconnects = v + } + v, err = strconv.ParseFloat(fields[2], 64) + if err == nil { + stats.shareReconnects = v + } + continue + } + + if strings.HasPrefix(line, "Total vfs operations:") && len(fields) >= 4 { + v, err := strconv.ParseFloat(fields[3], 64) + if err == nil { + stats.vfsOps = v + } + continue + } + + if len(fields) >= 2 && strings.HasSuffix(fields[0], ")") { + _, err := strconv.Atoi(strings.TrimSuffix(fields[0], ")")) + if err == nil { + currentShare = fields[1] + stats.perShare = append(stats.perShare, cifsShareStats{name: currentShare}) + } + continue + } + + if currentShare == "" { + continue + } + idx := len(stats.perShare) - 1 + + if strings.HasPrefix(line, "SMBs:") && len(fields) >= 2 { + v, err := strconv.ParseFloat(fields[1], 64) + if err == nil { + stats.perShare[idx].smbs = v + } + continue + } + + if strings.HasPrefix(line, "Bytes read:") && len(fields) >= 3 { + v, err := strconv.ParseFloat(fields[2], 64) + if err == nil { + stats.perShare[idx].readBytes = v + } + continue + } + + if strings.HasPrefix(line, "Bytes written:") && len(fields) >= 3 { + v, err := strconv.ParseFloat(fields[2], 64) + if err == nil { + stats.perShare[idx].writeBytes = v + } + continue + } + } + + return stats, scanner.Err() +} diff --git a/collector/cifs_linux_test.go b/collector/cifs_linux_test.go new file mode 100644 index 00000000..bc075322 --- /dev/null +++ b/collector/cifs_linux_test.go @@ -0,0 +1,93 @@ +// Copyright 2026 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 !nocifs + +package collector + +import ( + "fmt" + "io" + "log/slog" + "strings" + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" +) + +type testCIFSCollector struct { + cc Collector +} + +func (c testCIFSCollector) Collect(ch chan<- prometheus.Metric) { + c.cc.Update(ch) +} + +func (c testCIFSCollector) Describe(ch chan<- *prometheus.Desc) { + prometheus.DescribeByCollect(c, ch) +} + +func TestCIFSStats(t *testing.T) { + testcase := `# HELP node_cifs_read_bytes_total Total bytes read from this share. + # TYPE node_cifs_read_bytes_total counter + node_cifs_read_bytes_total{share="\\\\server1\\share1"} 123456 + node_cifs_read_bytes_total{share="\\\\server2\\share2"} 789012 + # HELP node_cifs_session_reconnects_total Total number of CIFS session reconnects. + # TYPE node_cifs_session_reconnects_total counter + node_cifs_session_reconnects_total 3 + # HELP node_cifs_sessions Number of active CIFS sessions. + # TYPE node_cifs_sessions gauge + node_cifs_sessions 2 + # HELP node_cifs_share_reconnects_total Total number of CIFS share reconnects. + # TYPE node_cifs_share_reconnects_total counter + node_cifs_share_reconnects_total 5 + # HELP node_cifs_shares Number of unique mount targets. + # TYPE node_cifs_shares gauge + node_cifs_shares 3 + # HELP node_cifs_smbs_total Total number of SMBs sent for this share. + # TYPE node_cifs_smbs_total counter + node_cifs_smbs_total{share="\\\\server1\\share1"} 1234 + node_cifs_smbs_total{share="\\\\server2\\share2"} 5678 + # HELP node_cifs_vfs_operations_total Total number of VFS operations. + # TYPE node_cifs_vfs_operations_total counter + node_cifs_vfs_operations_total 67 + # HELP node_cifs_write_bytes_total Total bytes written to this share. + # TYPE node_cifs_write_bytes_total counter + node_cifs_write_bytes_total{share="\\\\server1\\share1"} 654321 + node_cifs_write_bytes_total{share="\\\\server2\\share2"} 210987 + ` + *procPath = "fixtures/proc" + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + c, err := NewCIFSCollector(logger) + if err != nil { + t.Fatal(err) + } + reg := prometheus.NewRegistry() + reg.MustRegister(&testCIFSCollector{cc: c}) + + sink := make(chan prometheus.Metric) + go func() { + err = c.Update(sink) + if err != nil { + panic(fmt.Errorf("failed to update collector: %s", err)) + } + close(sink) + }() + + err = testutil.GatherAndCompare(reg, strings.NewReader(testcase)) + if err != nil { + t.Fatal(err) + } +} diff --git a/collector/fixtures/proc/fs/cifs/Stats b/collector/fixtures/proc/fs/cifs/Stats new file mode 100644 index 00000000..c4f26cef --- /dev/null +++ b/collector/fixtures/proc/fs/cifs/Stats @@ -0,0 +1,19 @@ +Resources in use +CIFS Session: 2 +Share (unique mount targets): 3 +SMB Request/Response Buffer: 1 Pool size: 5 +SMB Small Req/Resp Buffer: 1 Pool size: 30 +Operations (MIDs): 0 + +3 session 5 share reconnects +Total vfs operations: 67 maximum at one time: 2 + +1) \\server1\share1 +SMBs: 1234 +Bytes read: 123456 +Bytes written: 654321 + +2) \\server2\share2 +SMBs: 5678 +Bytes read: 789012 +Bytes written: 210987