Skip to content
Open
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
72 changes: 65 additions & 7 deletions internal/metric/disk.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package metric

import (
"os"
"path/filepath"
"runtime"
"strings"

Check failure on line 7 in internal/metric/disk.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (goimports)

"github.com/shirou/gopsutil/v4/disk"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please run gofmt -w . to fix import and documentation styles? It will add a new line between internal and external imports, and update the function documentation with correct indentation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the formatting is OK, I would like to merge this PR. @VeNoMZiTo

)

Expand Down Expand Up @@ -120,8 +120,9 @@
}

// collectIOStats gathers IO-related metrics for a device.
// Supports LVM/device-mapper by resolving /dev/mapper/* -> /dev/dm-*
// and trying multiple key candidates against the map returned by disk.IOCounters().
// Supports LVM/device-mapper by resolving /dev/mapper/* -> /dev/dm-*,
// searching /sys/block for the matching dm-* device, and trying multiple
// key candidates against the map returned by disk.IOCounters().
func collectIOStats(device string) (*disk.IOCountersStat, *CustomErr) {
// Get all counters once and look up by key
all, err := disk.IOCounters()
Expand Down Expand Up @@ -158,7 +159,11 @@
}

// buildDeviceKeyCandidates returns possible keys for the disk.IOCounters() map.
// Handles paths like /dev/sda, /dev/nvme0n1, /dev/mapper/vg-lv -> dm-0, etc.
// Handles paths like /dev/sda, /dev/nvme0n1, and /dev/mapper/vg-lv by:
// - stripping /dev/
// - taking the basename
// - resolving symlinks when applicable
// - scanning /sys/block/dm-*/dm/name to find the matching dm-* device
func buildDeviceKeyCandidates(device string) []string {
if runtime.GOOS == "windows" {
// On Windows, gopsutil uses names like "C:", so keep as-is.
Expand All @@ -172,18 +177,28 @@
// Strip /dev/
out = append(out, strings.TrimPrefix(d, "/dev/"))
// Basename (e.g., /dev/mapper/vg-lv -> vg-lv)
out = append(out, filepath.Base(d))
base := filepath.Base(d)
out = append(out, base)

// Resolve symlinks: /dev/mapper/vg-lv -> /dev/dm-0 -> dm-0
// Resolve symlinks (works for typical udev/by-id/by-uuid, not for mapper pseudo-devices)
if resolved, err := filepath.EvalSymlinks(d); err == nil && resolved != "" {
out = append(out, strings.TrimPrefix(resolved, "/dev/"))
out = append(out, filepath.Base(resolved))
}

// Deduplicate
// If it's an LVM/device-mapper path, try to discover dm-* via /sys/block
if strings.HasPrefix(d, "/dev/mapper/") || strings.HasPrefix(base, "dm-") {
if dm := findDMForMapperBase(base); dm != "" {
// Add dm-* key (this is what gopsutil uses in IOCounters)
out = append(out, dm)
}
}

// Deduplicate and drop empties
seen := map[string]struct{}{}
uniq := make([]string, 0, len(out))
for _, k := range out {
k = strings.TrimSpace(k)
if k == "" {
continue
}
Expand All @@ -195,6 +210,49 @@
return uniq
}

// findDMForMapperBase tries to map a /dev/mapper/<name> basename to its dm-*
// by scanning /sys/block/dm-*/dm/name and comparing values.
//
// For LVM, the mapper basename typically matches the content of /sys/block/dm-*/dm/name.
// Example:
// /dev/mapper/ubuntu--vg-ubuntu--lv -> /sys/block/dm-0/dm/name == "ubuntu--vg-ubuntu--lv" => dm-0
func findDMForMapperBase(mapperBase string) string {
const sysBlock = "/sys/block"
entries, err := os.ReadDir(sysBlock)
if err != nil {
return ""
}

for _, e := range entries {
name := e.Name()
if !strings.HasPrefix(name, "dm-") {
continue
}
// Read /sys/block/dm-*/dm/name to get the logical name
dmNamePath := filepath.Join(sysBlock, name, "dm", "name")
b, err := os.ReadFile(dmNamePath)
if err != nil {
continue
}
dmLogical := strings.TrimSpace(string(b))

// Compare mapper basename to dm logical name.
// LVM encodes '-' as '--' in names; mapperBase already carries that encoding,
// and /sys/block/.../dm/name typically matches the same encoding.
if dmLogical == mapperBase {
return name
}

// Extra tolerance: also try a relaxed comparison removing all slashes
// and comparing lowercased (helps in edge cases with udev rules).
if strings.EqualFold(strings.ReplaceAll(dmLogical, "/", ""), strings.ReplaceAll(mapperBase, "/", "")) {
return name
}
}

return ""
}

// collectUsageStats collects usage-related metrics for a mountpoint.
func collectUsageStats(mountpoint string) (*disk.UsageStat, *CustomErr) {
diskUsage, diskUsageErr := disk.Usage(mountpoint)
Expand Down
Loading