From 407fdc173bb6f9a416c6ac4b34a24c640a56b8ba Mon Sep 17 00:00:00 2001 From: hys Date: Thu, 2 Apr 2026 17:02:09 +0800 Subject: [PATCH] feat: add lsblk info collector --- Dockerfile | 2 +- collector/lsblk_linux.go | 112 +++++++++++++++++++++++++++++++++++++++ end-to-end-test.sh | 1 + 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 collector/lsblk_linux.go diff --git a/Dockerfile b/Dockerfile index 40d41eb64c..fd733bade7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN go build -ldflags="-s -w" -a -o node_exporter node_exporter.go FROM alpine:3.21 -RUN apk add smartmontools pciutils nvme-cli +RUN apk add smartmontools pciutils nvme-cli util-linux COPY --from=builder /workspace/node_exporter /bin/node_exporter EXPOSE 9100 diff --git a/collector/lsblk_linux.go b/collector/lsblk_linux.go new file mode 100644 index 0000000000..367933899e --- /dev/null +++ b/collector/lsblk_linux.go @@ -0,0 +1,112 @@ +//go:build !nolsblk +// +build !nolsblk + +package collector + +import ( + "encoding/json" + "fmt" + "log/slog" + "os" + + "github.com/alecthomas/kingpin/v2" + "github.com/prometheus/client_golang/prometheus" +) + +const lsblkDiskSubsystem = "disk" + +var lsblkPath = kingpin.Flag( + "collector.lsblk.path", + "Path to the lsblk binary.", +).Default("/bin/lsblk").String() + +var lsblkTimeout = kingpin.Flag( + "collector.lsblk.timeout", + "Timeout for running lsblk.", +).Default("5s").Duration() + +type lsblkCollector struct { + logger *slog.Logger + infoDesc *prometheus.Desc +} + +func init() { + registerCollector("lsblk", defaultEnabled, NewLsblkCollector) +} + +// NewLsblkCollector returns a collector that exposes lsblk(8) JSON output as metrics. +func NewLsblkCollector(logger *slog.Logger) (Collector, error) { + return &lsblkCollector{ + logger: logger, + infoDesc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, lsblkDiskSubsystem, "lsblk_info"), + "Non-numeric block device fields from lsblk --json, value is always 1.", + []string{"name", "parent", "fstype", "mountpoint", "size", "fsused", "fsuse_percent"}, + nil, + ), + }, nil +} + +type lsblkJSONRoot struct { + Blockdevices []lsblkJSONDevice `json:"blockdevices"` +} + +type lsblkJSONDevice struct { + Name string `json:"name"` + Size *string `json:"size"` + Fstype *string `json:"fstype"` + Mountpoint *string `json:"mountpoint"` + Fsused *string `json:"fsused"` + FsusePct *string `json:"fsuse%"` + Children []lsblkJSONDevice `json:"children"` +} + +func lsblkStringPtr(s *string) string { + if s == nil { + return "" + } + return *s +} + +func (c *lsblkCollector) emitRecursive(ch chan<- prometheus.Metric, devices []lsblkJSONDevice, parent string) { + for _, d := range devices { + ch <- prometheus.MustNewConstMetric(c.infoDesc, prometheus.GaugeValue, 1, + d.Name, + parent, + lsblkStringPtr(d.Fstype), + lsblkStringPtr(d.Mountpoint), + lsblkStringPtr(d.Size), + lsblkStringPtr(d.Fsused), + lsblkStringPtr(d.FsusePct), + ) + if len(d.Children) > 0 { + c.emitRecursive(ch, d.Children, d.Name) + } + } +} + +func (c *lsblkCollector) Update(ch chan<- prometheus.Metric) error { + path := *lsblkPath + if path == "" { + return fmt.Errorf("collector.lsblk.path is empty") + } + if _, err := os.Stat(path); err != nil { + c.logger.Debug("lsblk binary not available", "path", path, "err", err) + return ErrNoData + } + + args := []string{"--json", "-o", "NAME,SIZE,FSTYPE,MOUNTPOINT,FSUSED,FSUSE%"} + cmd := execCommand(path, args...) + out, err := CombinedOutputTimeout(cmd, *lsblkTimeout) + if err != nil { + return fmt.Errorf("lsblk failed: %w", err) + } + + var root lsblkJSONRoot + if err := json.Unmarshal(out, &root); err != nil { + return fmt.Errorf("lsblk json parse error: %w", err) + } + + c.emitRecursive(ch, root.Blockdevices, "") + return nil +} diff --git a/end-to-end-test.sh b/end-to-end-test.sh index 944bdc0e26..268cb5d0e2 100755 --- a/end-to-end-test.sh +++ b/end-to-end-test.sh @@ -94,6 +94,7 @@ supported_enabled_collectors=$(supported_collectors "${enabled_collectors}") disabled_collectors=$(cat << COLLECTORS selinux filesystem + lsblk timex uname COLLECTORS