Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions collector/filesystem_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,27 @@ func (c *filesystemCollector) processStat(labels filesystemLabels) filesystemSta
return filesystemStats{
labels: labels,
size: float64(buf.Blocks) * float64(buf.Bsize),
free: float64(buf.Bfree) * float64(buf.Bsize),
avail: float64(buf.Bavail) * float64(buf.Bsize),
free: blocksToBytes(buf.Bfree, buf.Bsize),
avail: blocksToBytes(buf.Bavail, buf.Bsize),
files: float64(buf.Files),
filesFree: float64(buf.Ffree),
ro: ro,
}
}

// blocksToBytes converts a block count reported by statfs(2) to bytes.
// Some filesystems (e.g. ext3/ext4 when reserved blocks are exhausted) can
// report a negative free/available block count that overflows the kernel's
// unsigned f_bfree/f_bavail fields, wrapping to a value near 2^64. Treat any
// block count whose sign bit is set as a wrapped negative value and report
// zero instead of an enormous, misleading byte count.
func blocksToBytes(blocks uint64, bsize int64) float64 {
if int64(blocks) < 0 {
return 0
}
return float64(blocks) * float64(bsize)
}

// stuckMountWatcher listens on the given success channel and if the channel closes
// then the watcher does nothing. If instead the timeout is reached, the
// mount point that is being watched is marked as stuck.
Expand Down
49 changes: 49 additions & 0 deletions collector/filesystem_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package collector
import (
"io"
"log/slog"
"math"
"sort"
"strings"
"testing"
Expand Down Expand Up @@ -288,6 +289,54 @@ func TestMountOptionsStringReadOnlyDetection(t *testing.T) {
}
}

func TestBlocksToBytes(t *testing.T) {
tests := []struct {
name string
blocks uint64
bsize int64
want float64
}{
{
name: "normal value",
blocks: 1000,
bsize: 4096,
want: 1000 * 4096,
},
{
name: "zero blocks",
blocks: 0,
bsize: 4096,
want: 0,
},
{
name: "negative block count wrapped into uint64 clamps to zero",
blocks: math.MaxUint64, // two's complement of -1, as reported when reserved blocks exceed free blocks
bsize: 4096,
want: 0,
},
{
name: "smallest block count with sign bit set clamps to zero",
blocks: 1 << 63,
bsize: 4096,
want: 0,
},
{
name: "largest block count without sign bit set is not clamped",
blocks: 1<<63 - 1,
bsize: 4096,
want: float64(1<<63-1) * 4096,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := blocksToBytes(tt.blocks, tt.bsize); got != tt.want {
t.Errorf("blocksToBytes(%d, %d) = %v, want %v", tt.blocks, tt.bsize, got, tt.want)
}
})
}
}

func TestPathRootfs(t *testing.T) {
if _, err := kingpin.CommandLine.Parse([]string{"--path.procfs", "./fixtures_bindmount/proc", "--path.rootfs", "/host"}); err != nil {
t.Fatal(err)
Expand Down