diff --git a/collector/fixtures/proc/spl/kstat/zfs/arcstats b/collector/fixtures/proc/spl/kstat/zfs/arcstats new file mode 100644 index 00000000..48a73a2c --- /dev/null +++ b/collector/fixtures/proc/spl/kstat/zfs/arcstats @@ -0,0 +1,93 @@ +6 1 0x01 91 4368 5266997922 97951858082072 +name type data +hits 4 8772612 +misses 4 604635 +demand_data_hits 4 7221032 +demand_data_misses 4 73300 +demand_metadata_hits 4 1464353 +demand_metadata_misses 4 498170 +prefetch_data_hits 4 3615 +prefetch_data_misses 4 17094 +prefetch_metadata_hits 4 83612 +prefetch_metadata_misses 4 16071 +mru_hits 4 855535 +mru_ghost_hits 4 21100 +mfu_hits 4 7829854 +mfu_ghost_hits 4 821 +deleted 4 60403 +mutex_miss 4 2 +evict_skip 4 2265729 +evict_not_enough 4 680 +evict_l2_cached 4 0 +evict_l2_eligible 4 8992514560 +evict_l2_ineligible 4 992552448 +evict_l2_skip 4 0 +hash_elements 4 42359 +hash_elements_max 4 88245 +hash_collisions 4 50564 +hash_chains 4 412 +hash_chain_max 4 3 +p 4 516395305 +c 4 1643208777 +c_min 4 33554432 +c_max 4 8367976448 +size 4 1603939792 +hdr_size 4 16361080 +data_size 4 1295836160 +metadata_size 4 175298560 +other_size 4 116443992 +anon_size 4 1917440 +anon_evictable_data 4 0 +anon_evictable_metadata 4 0 +mru_size 4 402593792 +mru_evictable_data 4 278091264 +mru_evictable_metadata 4 18606592 +mru_ghost_size 4 999728128 +mru_ghost_evictable_data 4 883765248 +mru_ghost_evictable_metadata 4 115962880 +mfu_size 4 1066623488 +mfu_evictable_data 4 1017613824 +mfu_evictable_metadata 4 9163776 +mfu_ghost_size 4 104936448 +mfu_ghost_evictable_data 4 96731136 +mfu_ghost_evictable_metadata 4 8205312 +l2_hits 4 0 +l2_misses 4 0 +l2_feeds 4 0 +l2_rw_clash 4 0 +l2_read_bytes 4 0 +l2_write_bytes 4 0 +l2_writes_sent 4 0 +l2_writes_done 4 0 +l2_writes_error 4 0 +l2_writes_lock_retry 4 0 +l2_evict_lock_retry 4 0 +l2_evict_reading 4 0 +l2_evict_l1cached 4 0 +l2_free_on_write 4 0 +l2_cdata_free_on_write 4 0 +l2_abort_lowmem 4 0 +l2_cksum_bad 4 0 +l2_io_error 4 0 +l2_size 4 0 +l2_asize 4 0 +l2_hdr_size 4 0 +l2_compress_successes 4 0 +l2_compress_zeros 4 0 +l2_compress_failures 4 0 +memory_throttle_count 4 0 +duplicate_buffers 4 0 +duplicate_buffers_size 4 0 +duplicate_reads 4 0 +memory_direct_count 4 542 +memory_indirect_count 4 3006 +arc_no_grow 4 0 +arc_tempreserve 4 0 +arc_loaned_bytes 4 0 +arc_prune 4 0 +arc_meta_used 4 308103632 +arc_meta_limit 4 6275982336 +arc_meta_max 4 449286096 +arc_meta_min 4 16777216 +arc_need_free 4 0 +arc_sys_free 4 261496832 diff --git a/collector/fixtures/sysctl/freebsd/kstat.zfs.misc.arcstats.txt b/collector/fixtures/sysctl/freebsd/kstat.zfs.misc.arcstats.txt new file mode 100644 index 00000000..aaaf6503 --- /dev/null +++ b/collector/fixtures/sysctl/freebsd/kstat.zfs.misc.arcstats.txt @@ -0,0 +1,78 @@ +kstat.zfs.misc.arcstats.arc_meta_max: 1503210048 +kstat.zfs.misc.arcstats.arc_meta_limit: 393216000 +kstat.zfs.misc.arcstats.arc_meta_used: 392649848 +kstat.zfs.misc.arcstats.duplicate_reads: 0 +kstat.zfs.misc.arcstats.duplicate_buffers_size: 0 +kstat.zfs.misc.arcstats.duplicate_buffers: 0 +kstat.zfs.misc.arcstats.memory_throttle_count: 0 +kstat.zfs.misc.arcstats.l2_write_buffer_list_null_iter: 0 +kstat.zfs.misc.arcstats.l2_write_buffer_list_iter: 0 +kstat.zfs.misc.arcstats.l2_write_buffer_bytes_scanned: 0 +kstat.zfs.misc.arcstats.l2_write_pios: 0 +kstat.zfs.misc.arcstats.l2_write_buffer_iter: 0 +kstat.zfs.misc.arcstats.l2_write_full: 0 +kstat.zfs.misc.arcstats.l2_write_not_cacheable: 29425 +kstat.zfs.misc.arcstats.l2_write_io_in_progress: 0 +kstat.zfs.misc.arcstats.l2_write_in_l2: 0 +kstat.zfs.misc.arcstats.l2_write_spa_mismatch: 0 +kstat.zfs.misc.arcstats.l2_write_passed_headroom: 0 +kstat.zfs.misc.arcstats.l2_write_trylock_fail: 0 +kstat.zfs.misc.arcstats.l2_compress_failures: 0 +kstat.zfs.misc.arcstats.l2_compress_zeros: 0 +kstat.zfs.misc.arcstats.l2_compress_successes: 0 +kstat.zfs.misc.arcstats.l2_hdr_size: 0 +kstat.zfs.misc.arcstats.l2_asize: 0 +kstat.zfs.misc.arcstats.l2_size: 0 +kstat.zfs.misc.arcstats.l2_io_error: 0 +kstat.zfs.misc.arcstats.l2_cksum_bad: 0 +kstat.zfs.misc.arcstats.l2_abort_lowmem: 0 +kstat.zfs.misc.arcstats.l2_cdata_free_on_write: 0 +kstat.zfs.misc.arcstats.l2_free_on_write: 0 +kstat.zfs.misc.arcstats.l2_evict_reading: 0 +kstat.zfs.misc.arcstats.l2_evict_lock_retry: 0 +kstat.zfs.misc.arcstats.l2_writes_hdr_miss: 0 +kstat.zfs.misc.arcstats.l2_writes_error: 0 +kstat.zfs.misc.arcstats.l2_writes_done: 0 +kstat.zfs.misc.arcstats.l2_writes_sent: 0 +kstat.zfs.misc.arcstats.l2_write_bytes: 0 +kstat.zfs.misc.arcstats.l2_read_bytes: 0 +kstat.zfs.misc.arcstats.l2_rw_clash: 0 +kstat.zfs.misc.arcstats.l2_feeds: 0 +kstat.zfs.misc.arcstats.l2_misses: 0 +kstat.zfs.misc.arcstats.l2_hits: 0 +kstat.zfs.misc.arcstats.other_size: 166832272 +kstat.zfs.misc.arcstats.data_size: 1200779776 +kstat.zfs.misc.arcstats.hdr_size: 27244008 +kstat.zfs.misc.arcstats.size: 1394856056 +kstat.zfs.misc.arcstats.c_max: 1572864000 +kstat.zfs.misc.arcstats.c_min: 196608000 +kstat.zfs.misc.arcstats.c: 1470553736 +kstat.zfs.misc.arcstats.p: 665524427 +kstat.zfs.misc.arcstats.hash_chain_max: 7 +kstat.zfs.misc.arcstats.hash_chains: 14180 +kstat.zfs.misc.arcstats.hash_collisions: 2180398 +kstat.zfs.misc.arcstats.hash_elements_max: 238188 +kstat.zfs.misc.arcstats.hash_elements: 111458 +kstat.zfs.misc.arcstats.evict_l2_ineligible: 60262400 +kstat.zfs.misc.arcstats.evict_l2_eligible: 35702978560 +kstat.zfs.misc.arcstats.evict_l2_cached: 0 +kstat.zfs.misc.arcstats.evict_skip: 21716568 +kstat.zfs.misc.arcstats.mutex_miss: 873 +kstat.zfs.misc.arcstats.recycle_miss: 5018771 +kstat.zfs.misc.arcstats.stolen: 1327563 +kstat.zfs.misc.arcstats.deleted: 1187256 +kstat.zfs.misc.arcstats.allocated: 10150518 +kstat.zfs.misc.arcstats.mfu_ghost_hits: 1408986 +kstat.zfs.misc.arcstats.mfu_hits: 51952454 +kstat.zfs.misc.arcstats.mru_ghost_hits: 696819 +kstat.zfs.misc.arcstats.mru_hits: 11115835 +kstat.zfs.misc.arcstats.prefetch_metadata_misses: 32 +kstat.zfs.misc.arcstats.prefetch_metadata_hits: 2 +kstat.zfs.misc.arcstats.prefetch_data_misses: 0 +kstat.zfs.misc.arcstats.prefetch_data_hits: 0 +kstat.zfs.misc.arcstats.demand_metadata_misses: 9231542 +kstat.zfs.misc.arcstats.demand_metadata_hits: 40650947 +kstat.zfs.misc.arcstats.demand_data_misses: 75230 +kstat.zfs.misc.arcstats.demand_data_hits: 22417340 +kstat.zfs.misc.arcstats.misses: 9306804 +kstat.zfs.misc.arcstats.hits: 63068289 diff --git a/collector/fixtures/zfs/zpool_stats_stdout.txt b/collector/fixtures/zfs/zpool_stats_stdout.txt new file mode 100644 index 00000000..104e2d18 --- /dev/null +++ b/collector/fixtures/zfs/zpool_stats_stdout.txt @@ -0,0 +1,12 @@ +trout size 4294967296 - +trout free 1040117248 - +trout allocated 70144 - +trout capacity 0% - +trout dedupratio 1.00x - +trout fragmentation 0% - +zroot size 118111600640 - +zroot free 3990917120 - +zroot allocated 114120683520 - +zroot capacity 50% - +zroot dedupratio 1.00x - +zroot fragmentation 67% - diff --git a/collector/zfs.go b/collector/zfs.go new file mode 100644 index 00000000..9749a0b3 --- /dev/null +++ b/collector/zfs.go @@ -0,0 +1,109 @@ +package collector + +// +build linux freebsd +// +build !nozfs + +import ( + "errors" + "strings" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/log" +) + +type zfsMetricValue int + +const zfsErrorValue = zfsMetricValue(-1) + +var zfsNotAvailableError = errors.New("ZFS / ZFS statistics are not available") + +type zfsSysctl string +type zfsSubsystemName string + +const ( + arc = zfsSubsystemName("zfsArc") + zpoolSubsystem = zfsSubsystemName("zfsPool") +) + +// Metrics + +type zfsMetric struct { + subsystem zfsSubsystemName // The Prometheus subsystem name. + name string // The Prometheus name of the metric. + sysctl zfsSysctl // The sysctl of the ZFS metric. +} + +type datasetMetric struct { + subsystem zfsSubsystemName + name string +} + +// Collector + +func init() { + Factories["zfs"] = NewZFSCollector +} + +type zfsCollector struct { + zfsMetrics []zfsMetric +} + +func NewZFSCollector() (Collector, error) { + return &zfsCollector{}, nil +} + +func (c *zfsCollector) Update(ch chan<- prometheus.Metric) (err error) { + + err = c.zfsAvailable() + switch { + case err == zfsNotAvailableError: + log.Debug(err) + return nil + case err != nil: + return err + } + + // Arcstats + err = c.updateArcstats(ch) + if err != nil { + return err + } + + // Pool stats + return c.updatePoolStats(ch) +} + +func (s zfsSysctl) metricName() string { + parts := strings.Split(string(s), ".") + return parts[len(parts)-1] +} + +func (c *zfsCollector) ConstSysctlMetric(subsystem zfsSubsystemName, sysctl zfsSysctl, value zfsMetricValue) prometheus.Metric { + + metricName := sysctl.metricName() + + return prometheus.MustNewConstMetric( + prometheus.NewDesc( + prometheus.BuildFQName(Namespace, string(subsystem), metricName), + string(sysctl), + nil, + nil, + ), + prometheus.UntypedValue, + float64(value), + ) +} + +func (c *zfsCollector) ConstZpoolMetric(pool, name string, value float64) prometheus.Metric { + return prometheus.MustNewConstMetric( + prometheus.NewDesc( + prometheus.BuildFQName(Namespace, string(zpoolSubsystem), name), + name, + []string{"pool"}, + nil, + ), + prometheus.UntypedValue, + float64(value), + pool, + ) +} diff --git a/collector/zfs_freebsd.go b/collector/zfs_freebsd.go new file mode 100644 index 00000000..9d344dc8 --- /dev/null +++ b/collector/zfs_freebsd.go @@ -0,0 +1,110 @@ +package collector + +import ( + "bufio" + "io" + "os/exec" + "strconv" + "strings" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/log" +) + +/* +#cgo LDFLAGS: +#include +#include + +int zfsModuleLoaded() { + int modid = modfind("zfs"); + return modid < 0 ? 0 : -1; +} + +*/ +import "C" + +func (c *zfsCollector) zfsAvailable() error { + if C.zfsModuleLoaded() == 0 { + return zfsNotAvailableError + } + return nil +} + +const zfsArcstatsSysctl = "kstat.zfs.misc.arcstats" + +func (c *zfsCollector) RunOnStdout(cmd *exec.Cmd, handler func(io.Reader) error) (err error) { + + stdout, err := cmd.StdoutPipe() + if err != nil { + return + } + + if err = cmd.Start(); err != nil { + return + } + + err = handler(stdout) + if err != nil { + return + } + + return cmd.Wait() +} + +func (c *zfsCollector) updateArcstats(ch chan<- prometheus.Metric) (err error) { + + cmd := exec.Command("sysctl", zfsArcstatsSysctl) + + err = c.RunOnStdout(cmd, func(stdout io.Reader) error { + return c.parseArcstatsSysctlOutput(stdout, func(sysctl zfsSysctl, value zfsMetricValue) { + ch <- c.ConstSysctlMetric(arc, sysctl, zfsMetricValue(value)) + }) + }) + return err + +} + +func (c *zfsCollector) parseArcstatsSysctlOutput(reader io.Reader, handler func(zfsSysctl, zfsMetricValue)) (err error) { + + // Decode values + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + + fields := strings.Fields(scanner.Text()) + + if len(fields) != 2 || + !strings.HasPrefix(fields[0], zfsArcstatsSysctl) || + !strings.HasSuffix(fields[0], ":") { + + log.Debugf("Skipping line of unknown format: %q", scanner.Text()) + continue + + } + + sysctl := zfsSysctl(strings.TrimSuffix(fields[0], ":")) + value, err := strconv.Atoi(fields[1]) + if err != nil { + return err + } + + handler(sysctl, zfsMetricValue(value)) + } + return scanner.Err() + +} + +func (c *zfsCollector) updatePoolStats(ch chan<- prometheus.Metric) (err error) { + + poolProperties := []string{"size", "free", "allocated", "capacity", "dedupratio", "fragmentation"} + + cmd := exec.Command("zpool", "get", "-pH", strings.Join(poolProperties, ",")) + + err = c.RunOnStdout(cmd, func(stdout io.Reader) error { + return c.parseZpoolOutput(stdout, func(pool, name string, value float64) { + ch <- c.ConstZpoolMetric(pool, name, value) + }) + }) + + return err +} diff --git a/collector/zfs_freebsd_test.go b/collector/zfs_freebsd_test.go new file mode 100644 index 00000000..cad90eab --- /dev/null +++ b/collector/zfs_freebsd_test.go @@ -0,0 +1,44 @@ +package collector + +import ( + "os" + "testing" +) + +func TestArcstatsParsing(t *testing.T) { + + arcstatsOutput, err := os.Open("fixtures/sysctl/freebsd/kstat.zfs.misc.arcstats.txt") + if err != nil { + t.Fatal(err) + } + defer arcstatsOutput.Close() + + c := zfsCollector{} + if err != nil { + t.Fatal(err) + } + + handlerCalled := false + err = c.parseArcstatsSysctlOutput(arcstatsOutput, func(s zfsSysctl, v zfsMetricValue) { + + if s != zfsSysctl("kstat.zfs.misc.arcstats.hits") { + return + } + + handlerCalled = true + + if v != zfsMetricValue(63068289) { + t.Fatalf("Incorrect value parsed from sysctl output") + } + + }) + + if err != nil { + t.Fatal(err) + } + + if !handlerCalled { + t.Fatal("Arcstats parsing handler was not called for some expected sysctls") + } + +} diff --git a/collector/zfs_linux.go b/collector/zfs_linux.go new file mode 100644 index 00000000..0a1c10e1 --- /dev/null +++ b/collector/zfs_linux.go @@ -0,0 +1,88 @@ +package collector + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/log" +) + +const ( + zfsArcstatsProcpath = "spl/kstat/zfs/arcstats" +) + +func (c *zfsCollector) zfsAvailable() (err error) { + file, err := c.openArcstatsFile() + if err != nil { + file.Close() + } + return err +} + +func (c *zfsCollector) openArcstatsFile() (file *os.File, err error) { + file, err = os.Open(procFilePath(zfsArcstatsProcpath)) + if err != nil { + log.Debugf("Cannot open %q for reading. Is the kernel module loaded?", procFilePath(zfsArcstatsProcpath)) + err = zfsNotAvailableError + } + return +} + +func (c *zfsCollector) updateArcstats(ch chan<- prometheus.Metric) (err error) { + + file, err := c.openArcstatsFile() + if err != nil { + return err + } + defer file.Close() + + return c.parseArcstatsProcfsFile(file, func(s zfsSysctl, v zfsMetricValue) { + ch <- c.ConstSysctlMetric(arc, s, v) + }) + +} + +func (c *zfsCollector) parseArcstatsProcfsFile(reader io.Reader, handler func(zfsSysctl, zfsMetricValue)) (err error) { + + scanner := bufio.NewScanner(reader) + + parseLine := false + for scanner.Scan() { + + parts := strings.Fields(scanner.Text()) + + if !parseLine && len(parts) == 3 && parts[0] == "name" && parts[1] == "type" && parts[2] == "data" { + // Start parsing from here. + parseLine = true + continue + } + + if !parseLine || len(parts) < 3 { + continue + } + + key := fmt.Sprintf("kstat.zfs.misc.arcstats.%s", parts[0]) + + value, err := strconv.Atoi(parts[2]) + if err != nil { + return fmt.Errorf("could not parse expected integer value for %q", key) + } + handler(zfsSysctl(key), zfsMetricValue(value)) + + } + if !parseLine { + return errors.New("did not parse a single arcstat metric") + } + + return scanner.Err() +} + +func (c *zfsCollector) updatePoolStats(ch chan<- prometheus.Metric) (err error) { + return nil +} diff --git a/collector/zfs_linux_test.go b/collector/zfs_linux_test.go new file mode 100644 index 00000000..2640ebac --- /dev/null +++ b/collector/zfs_linux_test.go @@ -0,0 +1,44 @@ +package collector + +import ( + "os" + "testing" +) + +func TestArcstatsParsing(t *testing.T) { + + arcstatsFile, err := os.Open("fixtures/proc/spl/kstat/zfs/arcstats") + if err != nil { + t.Fatal(err) + } + defer arcstatsFile.Close() + + c := zfsCollector{} + if err != nil { + t.Fatal(err) + } + + handlerCalled := false + err = c.parseArcstatsProcfsFile(arcstatsFile, func(s zfsSysctl, v zfsMetricValue) { + + if s != zfsSysctl("kstat.zfs.misc.arcstats.hits") { + return + } + + handlerCalled = true + + if v != zfsMetricValue(8772612) { + t.Fatalf("Incorrect value parsed from procfs data") + } + + }) + + if err != nil { + t.Fatal(err) + } + + if !handlerCalled { + t.Fatal("Arcstats parsing handler was not called for some expected sysctls") + } + +} diff --git a/collector/zfs_zpool.go b/collector/zfs_zpool.go new file mode 100644 index 00000000..8ebdba9a --- /dev/null +++ b/collector/zfs_zpool.go @@ -0,0 +1,41 @@ +package collector + +import ( + "bufio" + "fmt" + "io" + "strconv" + "strings" +) + +// zpool metrics + +func (c *zfsCollector) parseZpoolOutput(reader io.Reader, handler func(string, string, float64)) (err error) { + + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + + fields := strings.Fields(scanner.Text()) + if len(fields) != 4 { + return fmt.Errorf("Unexpected output of zpool command") + } + + valueString := fields[2] + switch { + case strings.HasSuffix(fields[2], "%"): + percentage := strings.TrimSuffix(fields[2], "%") + valueString = "0." + percentage + case strings.HasSuffix(fields[2], "x"): + valueString = strings.TrimSuffix(fields[2], "x") + } + + value, err := strconv.ParseFloat(valueString, 64) + if err != nil { + return err + } + handler(fields[0], fields[1], value) + + } + return scanner.Err() + +} diff --git a/collector/zfs_zpool_test.go b/collector/zfs_zpool_test.go new file mode 100644 index 00000000..fd09babe --- /dev/null +++ b/collector/zfs_zpool_test.go @@ -0,0 +1,59 @@ +package collector + +import ( + "os" + "testing" +) + +func TestZpoolParsing(t *testing.T) { + + zpoolOutput, err := os.Open("fixtures/zfs/zpool_stats_stdout.txt") + if err != nil { + t.Fatal(err) + } + defer zpoolOutput.Close() + + c := zfsCollector{} + if err != nil { + t.Fatal(err) + } + + pools := make([]string, 2) + troutSize := float64(-1) + troutDedupratio := float64(-1) + zrootCapacity := float64(-1) + + err = c.parseZpoolOutput(zpoolOutput, func(pool, name string, value float64) { + pools = append(pools, pool) + if pool == "trout" && name == "size" { + troutSize = value + } + if pool == "trout" && name == "dedupratio" { + troutDedupratio = value + } + if pool == "zroot" && name == "capacity" { + zrootCapacity = value + } + }) + + if err != nil { + t.Fatal(err) + } + + if pools[0] == "trout" && pools[1] == "zroot" { + t.Fatal("Did not parse all pools in fixture") + } + + if troutSize != float64(4294967296) { + t.Fatal("Unexpected value for pool 'trout's size value") + } + + if troutDedupratio != float64(1.0) { + t.Fatal("Unexpected value for pool 'trout's dedupratio value") + } + + if zrootCapacity != float64(0.5) { + t.Fatal("Unexpected value for pool 'zroot's capacity value") + } + +}