-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a collector for ZFS, currently focussed on ARC stats. #213
Changes from 5 commits
0a8832e
538183f
62e4a7d
e92e9a2
6c13943
4d15f0b
fdfe4a6
3ae58ba
ffc3455
394305a
e828e04
45e989d
3c33e7e
66f4eeb
3a6a8a5
22bbb85
735effc
6bdd416
60aa697
2f99eb6
006cf29
3bb5356
a102306
e8d45a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package collector | ||
|
||
// +build !nofilesystem | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nozfs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still needs to be changed. |
||
// +build !nozfs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should also have a restriction to only run on linux and freebsd, as it won't compile on other platforms |
||
|
||
import ( | ||
"errors" | ||
|
||
"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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is maybe overdoing the special typing a bit, I'd just keep those as normal strings. |
||
|
||
const ( | ||
arc = zfsSubsystemName("zfs_arc") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the collectors have a shared namespace, so to be safe this should be |
||
) | ||
|
||
//------------------------------------------------------------------------------ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a fan of these big block comments |
||
// Metrics | ||
//------------------------------------------------------------------------------ | ||
|
||
type zfsMetric struct { | ||
subsystem zfsSubsystemName // The Prometheus subsystem name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comments should end in periods |
||
name string // The Prometheus name of the metric | ||
sysctl zfsSysctl // The sysctl of the ZFS metric | ||
} | ||
|
||
func NewZFSMetric(subsystem zfsSubsystemName, sysctl, name string) zfsMetric { | ||
return zfsMetric{ | ||
sysctl: zfsSysctl(sysctl), | ||
subsystem: subsystem, | ||
name: name, | ||
} | ||
} | ||
func (m *zfsMetric) BuildFQName() string { | ||
return prometheus.BuildFQName(Namespace, string(m.subsystem), m.name) | ||
} | ||
|
||
func (m *zfsMetric) HelpString() string { | ||
return m.name | ||
} | ||
|
||
//------------------------------------------------------------------------------ | ||
// Collector | ||
//------------------------------------------------------------------------------ | ||
|
||
func init() { | ||
Factories["zfs"] = NewZFSCollector | ||
} | ||
|
||
type zfsCollector struct { | ||
zfsMetrics []zfsMetric | ||
metricProvider zfsMetricProvider | ||
} | ||
|
||
func NewZFSCollector() (Collector, error) { | ||
err := zfsInitialize() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a noop There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now it is. However, with recent changes (no errors on missing ZFS module) this is no longer possible. Will remove. |
||
switch { | ||
case err == zfsNotAvailableError: | ||
log.Debug(err) | ||
break | ||
return &zfsCollector{}, err | ||
} | ||
|
||
return &zfsCollector{ | ||
zfsMetrics: []zfsMetric{ | ||
NewZFSMetric(arc, "kstat.zfs.misc.arcstats.p", "mru_size"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a simple struct instantiation, no need for a function |
||
NewZFSMetric(arc, "kstat.zfs.misc.arcstats.size", "size"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to supply these to Linux? I'd also be wary of renaming only some metrics, as that'll confuse anyone trying to work backwards. Usually we'd do a straight passthrough. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if I get you right. Are you worried about Linux using different sysctls than FreeBSD / other ZFSs one day? Hence, I conclude that using sysctls as OpenZFS-specific identifiers is acceptable, even if they are mapped to procfs on Linux. |
||
NewZFSMetric(arc, "kstat.zfs.misc.arcstats.c_min", "target_min_size"), | ||
NewZFSMetric(arc, "kstat.zfs.misc.arcstats.c", "target_size"), | ||
NewZFSMetric(arc, "kstat.zfs.misc.arcstats.c_max", "target_max_size"), | ||
NewZFSMetric(arc, "kstat.zfs.misc.arcstats.hits", "hits"), | ||
NewZFSMetric(arc, "kstat.zfs.misc.arcstats.misses", "misses"), | ||
}, | ||
metricProvider: NewZFSMetricProvider(), | ||
}, nil | ||
} | ||
|
||
func (c *zfsCollector) Update(ch chan<- prometheus.Metric) (err error) { | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've noticed you sometimes add extra whitespace at the beginning or end of function blocks. To me, it looks a little bit messy. However, I'm not a member of the Prometheus project, so I will defer to their stylistic preferences. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. |
||
log.Debug("Preparing metrics update") | ||
err = c.metricProvider.PrepareUpdate() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should return the values you need. By keeping them in an object you'll have concurrency issues There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You haven't changed this, you should have this return the map directly rather than going via an object. You should also be able to merge the two classes you currently have. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not encapsulate the strategy of retrieving metrics in a dedicated class? Justifications for my design decision:
What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having a class with effectively one function that is used only once by another function isn't really gaining anything, it's just adding a layer of abstraction that makes the code harder to understand. We don't do this elsewhere, and I don't see a need to do it here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does 6bdd416 look to you? (Removed the metric provider, now the platform specifc code sends metrics into the channel). Please have a look at collectors/zfs_linux.go:43. |
||
switch { | ||
case err == zfsNotAvailableError: | ||
log.Debug(err) | ||
return nil | ||
return err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is unreachable |
||
} | ||
defer c.metricProvider.InvalidateCache() | ||
|
||
log.Debugf("Fetching %d metrics", len(c.zfsMetrics)) | ||
for _, metric := range c.zfsMetrics { | ||
|
||
value, err := c.metricProvider.Value(metric.sysctl) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
ch <- prometheus.MustNewConstMetric( | ||
prometheus.NewDesc( | ||
metric.BuildFQName(), | ||
metric.HelpString(), | ||
nil, | ||
nil, | ||
), | ||
prometheus.UntypedValue, | ||
float64(value), | ||
) | ||
|
||
} | ||
|
||
return err | ||
} | ||
|
||
//------------------------------------------------------------------------------ | ||
// Metrics Provider | ||
// Platform-dependend parts implemented in zfs_${platform} files. | ||
//------------------------------------------------------------------------------ | ||
|
||
type zfsMetricProvider struct { | ||
values map[zfsSysctl]zfsMetricValue | ||
} | ||
|
||
func NewZFSMetricProvider() zfsMetricProvider { | ||
return zfsMetricProvider{ | ||
values: make(map[zfsSysctl]zfsMetricValue), | ||
} | ||
|
||
} | ||
|
||
func (p *zfsMetricProvider) InvalidateCache() { | ||
p.values = make(map[zfsSysctl]zfsMetricValue) | ||
} | ||
|
||
func (p *zfsMetricProvider) Value(s zfsSysctl) (value zfsMetricValue, err error) { | ||
|
||
var ok bool | ||
value = zfsErrorValue | ||
|
||
value, ok = p.values[s] | ||
if !ok { | ||
value, err = p.handleMiss(s) | ||
if err != nil { | ||
return value, err | ||
} | ||
} | ||
|
||
return value, err | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package collector | ||
|
||
// +build freebsd | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for this, go does this automatically from the filename. |
||
|
||
import ( | ||
"fmt" | ||
"unsafe" | ||
) | ||
|
||
/* | ||
#cgo LDFLAGS: | ||
#include <fcntl.h> | ||
#include <stdlib.h> | ||
#include <sys/param.h> | ||
#include <sys/module.h> | ||
#include <sys/pcpu.h> | ||
#include <sys/resource.h> | ||
#include <sys/sysctl.h> | ||
#include <sys/time.h> | ||
|
||
int zfsIntegerSysctl(const char *name) { | ||
|
||
int value; | ||
size_t value_size = sizeof(value); | ||
if (sysctlbyname(name, &value, &value_size, NULL, 0) != -1 || | ||
value_size != sizeof(value)) { | ||
return -1; | ||
} | ||
return value; | ||
|
||
} | ||
|
||
int zfsModuleLoaded() { | ||
int modid = modfind("zfs"); | ||
return modid < 0 ? 0 : -1; | ||
} | ||
|
||
*/ | ||
import "C" | ||
|
||
func zfsInitialize() error { | ||
return nil | ||
} | ||
|
||
func (c *zfsMetricProvider) PrepareUpdate() error { | ||
if C.zfsModuleLoaded() == 0 { | ||
return zfsNotAvailableError | ||
} | ||
return nil | ||
} | ||
|
||
func (p *zfsMetricProvider) handleMiss(s zfsSysctl) (zfsMetricValue, error) { | ||
|
||
sysctlCString := C.CString(string(s)) | ||
defer C.free(unsafe.Pointer(sysctlCString)) | ||
|
||
value := int(C.zfsIntegerSysctl(sysctlCString)) | ||
|
||
if value == -1 { | ||
return zfsErrorValue, fmt.Errorf("Could not retrieve sysctl '%s'", s) | ||
} | ||
|
||
return zfsMetricValue(value), nil | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package collector | ||
|
||
// +build linux | ||
|
||
import ( | ||
"bufio" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/prometheus/common/log" | ||
) | ||
|
||
const zfsArcstatsProcpath = "spl/kstat/zfs/arcstats" | ||
|
||
func zfsInitialize() error { | ||
return nil | ||
} | ||
|
||
func (p *zfsMetricProvider) PrepareUpdate() (err error) { | ||
|
||
err = p.prepareUpdateArcstats(zfsArcstatsProcpath) | ||
if err != nil { | ||
return | ||
} | ||
return nil | ||
} | ||
|
||
func (p *zfsMetricProvider) handleMiss(s zfsSysctl) (value zfsMetricValue, err error) { | ||
// all values are fetched in PrepareUpdate() | ||
return zfsErrorValue, fmt.Errorf("sysctl '%s' found") | ||
} | ||
|
||
func (p *zfsMetricProvider) prepareUpdateArcstats(zfsArcstatsProcpath string) (err error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would benefit from a unittest |
||
|
||
file, err := os.Open(procFilePath(zfsArcstatsProcpath)) | ||
if err != nil { | ||
log.Debugf("Cannot open ZFS arcstats procfs file for reading. " + | ||
" Is the kernel module loaded?") | ||
return zfsNotAvailableError | ||
} | ||
defer file.Close() | ||
|
||
scanner := bufio.NewScanner(file) | ||
|
||
parseLine := false | ||
for scanner.Scan() { | ||
parts := strings.Fields(scanner.Text()) | ||
|
||
if !parseLine && 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 '%s'", key) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
} | ||
log.Debugf("%s = %d", key, value) | ||
p.values[zfsSysctl(key)] = zfsMetricValue(value) | ||
} | ||
if !parseLine { | ||
return errors.New("did not parse a single arcstat metrics") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. *metric |
||
} | ||
|
||
return scanner.Err() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please include the same copyright/license header here as in all the other Node Exporter files. Same for the other Go files here.