Jonas Große Sundrup
9 years ago
5 changed files with 340 additions and 1 deletions
@ -0,0 +1,26 @@ |
|||||||
|
Personalities : [linear] [multipath] [raid0] [raid1] [raid6] [raid5] [raid4] [raid10] |
||||||
|
md3 : active raid6 sda1[8] sdh1[7] sdg1[6] sdf1[5] sde1[11] sdd1[3] sdc1[10] sdb1[9] |
||||||
|
5853468288 blocks super 1.2 level 6, 64k chunk, algorithm 2 [8/8] [UUUUUUUU] |
||||||
|
|
||||||
|
md127 : active raid1 sdi2[0] sdj2[1] |
||||||
|
312319552 blocks [2/2] [UU] |
||||||
|
|
||||||
|
md0 : active raid1 sdi1[0] sdj1[1] |
||||||
|
248896 blocks [2/2] [UU] |
||||||
|
|
||||||
|
md4 : inactive raid1 sda3[0] sdb3[1] |
||||||
|
4883648 blocks [2/2] [UU] |
||||||
|
|
||||||
|
md6 : active raid1 sdb2[2] sda2[0] |
||||||
|
195310144 blocks [2/1] [U_] |
||||||
|
[=>...................] recovery = 8.5% (16775552/195310144) finish=17.0min speed=259783K/sec |
||||||
|
|
||||||
|
md8 : active raid1 sdb1[1] sda1[0] |
||||||
|
195310144 blocks [2/2] [UU] |
||||||
|
[=>...................] resync = 8.5% (16775552/195310144) finish=17.0min speed=259783K/sec |
||||||
|
|
||||||
|
md7 : active raid6 sdb1[0] sde1[3] sdd1[2] sdc1[1] |
||||||
|
7813735424 blocks super 1.2 level 6, 512k chunk, algorithm 2 [4/3] [U_UU] |
||||||
|
bitmap: 0/30 pages [0KB], 65536KB chunk |
||||||
|
|
||||||
|
unused devices: <none> |
@ -0,0 +1,279 @@ |
|||||||
|
// +build !nomdadm
|
||||||
|
|
||||||
|
package collector |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"regexp" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus" |
||||||
|
"github.com/prometheus/log" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
statusfile = "/proc/mdstat" |
||||||
|
statuslineRE = regexp.MustCompile(`(\d+) blocks .*\[(\d+)/(\d+)\] \[[U_]+\]`) |
||||||
|
buildlineRE = regexp.MustCompile(`\((\d+)/\d+\)`) |
||||||
|
) |
||||||
|
|
||||||
|
type mdStatus struct { |
||||||
|
mdName string |
||||||
|
isActive bool |
||||||
|
disksActive int64 |
||||||
|
disksTotal int64 |
||||||
|
blocksTotal int64 |
||||||
|
blocksSynced int64 |
||||||
|
} |
||||||
|
|
||||||
|
type mdadmCollector struct{} |
||||||
|
|
||||||
|
func init() { |
||||||
|
Factories["mdadm"] = NewMdadmCollector |
||||||
|
} |
||||||
|
|
||||||
|
func evalStatusline(statusline string) (active, total, size int64, err error) { |
||||||
|
matches := statuslineRE.FindStringSubmatch(statusline) |
||||||
|
|
||||||
|
// +1 to make it more obvious that the whole string containing the info is also returned as matches[0].
|
||||||
|
if len(matches) < 3+1 { |
||||||
|
return 0, 0, 0, fmt.Errorf("too few matches found in statusline: %s", statusline) |
||||||
|
} else { |
||||||
|
if len(matches) > 3+1 { |
||||||
|
return 0, 0, 0, fmt.Errorf("too many matches found in statusline: %s", statusline) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
size, err = strconv.ParseInt(matches[1], 10, 64) |
||||||
|
if err != nil { |
||||||
|
return 0, 0, 0, fmt.Errorf("%s in statusline: %s", err, statusline) |
||||||
|
} |
||||||
|
|
||||||
|
total, err = strconv.ParseInt(matches[2], 10, 64) |
||||||
|
if err != nil { |
||||||
|
return 0, 0, 0, fmt.Errorf("%s in statusline: %s", err, statusline) |
||||||
|
} |
||||||
|
active, err = strconv.ParseInt(matches[3], 10, 64) |
||||||
|
if err != nil { |
||||||
|
return 0, 0, 0, fmt.Errorf("%s in statusline: %s", err, statusline) |
||||||
|
} |
||||||
|
|
||||||
|
return active, total, size, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Gets the size that has already been synced out of the sync-line.
|
||||||
|
func evalBuildline(buildline string) (int64, error) { |
||||||
|
matches := buildlineRE.FindStringSubmatch(buildline) |
||||||
|
|
||||||
|
// +1 to make it more obvious that the whole string containing the info is also returned as matches[0].
|
||||||
|
if len(matches) < 1+1 { |
||||||
|
return 0, fmt.Errorf("too few matches found in buildline: %s", buildline) |
||||||
|
} |
||||||
|
|
||||||
|
if len(matches) > 1+1 { |
||||||
|
return 0, fmt.Errorf("too many matches found in buildline: %s", buildline) |
||||||
|
} |
||||||
|
|
||||||
|
syncedSize, err := strconv.ParseInt(matches[1], 10, 64) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return 0, fmt.Errorf("%s in buildline: %s", err, buildline) |
||||||
|
} |
||||||
|
|
||||||
|
return syncedSize, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Parses an mdstat-file and returns a struct with the relevant infos.
|
||||||
|
func parseMdstat(mdStatusFilePath string) ([]mdStatus, error) { |
||||||
|
content, err := ioutil.ReadFile(mdStatusFilePath) |
||||||
|
if err != nil { |
||||||
|
return []mdStatus{}, fmt.Errorf("error parsing %s: %s", statusfile, err) |
||||||
|
} |
||||||
|
|
||||||
|
mdStatusFile := string(content) |
||||||
|
|
||||||
|
lines := strings.Split(mdStatusFile, "\n") |
||||||
|
var currentMD string |
||||||
|
|
||||||
|
// Each md has at least the deviceline, statusline and one empty line afterwards
|
||||||
|
// so we will have probably something of the order len(lines)/3 devices
|
||||||
|
// so we use that for preallocation.
|
||||||
|
estimateMDs := len(lines) / 3 |
||||||
|
mdStates := make([]mdStatus, 0, estimateMDs) |
||||||
|
|
||||||
|
for i, l := range lines { |
||||||
|
if l == "" { |
||||||
|
// Skip entirely empty lines.
|
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if l[0] == ' ' { |
||||||
|
// Those lines are not the beginning of a md-section.
|
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if strings.HasPrefix(l, "Personalities") || strings.HasPrefix(l, "unused") { |
||||||
|
// We aren't interested in lines with general info.
|
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
mainLine := strings.Split(l, " ") |
||||||
|
if len(mainLine) < 3 { |
||||||
|
return mdStates, fmt.Errorf("error parsing mdline: %s", l) |
||||||
|
} |
||||||
|
currentMD = mainLine[0] // name of md-device
|
||||||
|
isActive := (mainLine[2] == "active") // activity status of said md-device
|
||||||
|
|
||||||
|
if len(lines) <= i+3 { |
||||||
|
return mdStates, fmt.Errorf("error parsing %s: entry for %s has fewer lines than expected", statusfile, currentMD) |
||||||
|
} |
||||||
|
|
||||||
|
active, total, size, err := evalStatusline(lines[i+1]) // parse statusline, always present
|
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return mdStates, fmt.Errorf("error parsing %s: %s", statusfile, err) |
||||||
|
} |
||||||
|
|
||||||
|
// Now get the number of synced blocks.
|
||||||
|
var syncedBlocks int64 |
||||||
|
|
||||||
|
// Get the line number of the syncing-line.
|
||||||
|
var j int |
||||||
|
if strings.Contains(lines[i+2], "bitmap") { // then skip the bitmap line
|
||||||
|
j = i + 3 |
||||||
|
} else { |
||||||
|
j = i + 2 |
||||||
|
} |
||||||
|
|
||||||
|
// If device is syncing at the moment, get the number of currently synced bytes,
|
||||||
|
// otherwise that number equals the size of the device.
|
||||||
|
if strings.Contains(lines[j], "recovery") || strings.Contains(lines[j], "resync") { |
||||||
|
syncedBlocks, err = evalBuildline(lines[j]) |
||||||
|
if err != nil { |
||||||
|
return mdStates, fmt.Errorf("error parsing %s: %s", statusfile, err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
syncedBlocks = size |
||||||
|
} |
||||||
|
|
||||||
|
mdStates = append(mdStates, mdStatus{currentMD, isActive, active, total, size, syncedBlocks}) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
return mdStates, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Just returns the pointer to an empty struct as we only use throwaway-metrics.
|
||||||
|
func NewMdadmCollector() (Collector, error) { |
||||||
|
return &mdadmCollector{}, nil |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
isActiveDesc = prometheus.NewDesc( |
||||||
|
prometheus.BuildFQName(Namespace, "md", "is_active"), |
||||||
|
"Indicator whether the md-device is active or not.", |
||||||
|
[]string{"device"}, |
||||||
|
nil, |
||||||
|
) |
||||||
|
|
||||||
|
disksActiveDesc = prometheus.NewDesc( |
||||||
|
prometheus.BuildFQName(Namespace, "md", "disks_active"), |
||||||
|
"Number of active disks of device.", |
||||||
|
[]string{"device"}, |
||||||
|
nil, |
||||||
|
) |
||||||
|
|
||||||
|
disksTotalDesc = prometheus.NewDesc( |
||||||
|
prometheus.BuildFQName(Namespace, "md", "disks"), |
||||||
|
"Total number of disks of device.", |
||||||
|
[]string{"device"}, |
||||||
|
nil, |
||||||
|
) |
||||||
|
|
||||||
|
blocksTotalDesc = prometheus.NewDesc( |
||||||
|
prometheus.BuildFQName(Namespace, "md", "blocks"), |
||||||
|
"Total number of blocks on device.", |
||||||
|
[]string{"device"}, |
||||||
|
nil, |
||||||
|
) |
||||||
|
|
||||||
|
blocksSyncedDesc = prometheus.NewDesc( |
||||||
|
prometheus.BuildFQName(Namespace, "md", "blocks_synced"), |
||||||
|
"Number of blocks synced on device.", |
||||||
|
[]string{"device"}, |
||||||
|
nil, |
||||||
|
) |
||||||
|
) |
||||||
|
|
||||||
|
func (c *mdadmCollector) Update(ch chan<- prometheus.Metric) (err error) { |
||||||
|
// take care we don't crash on non-existent statusfiles
|
||||||
|
_, err = os.Stat(statusfile) |
||||||
|
if os.IsNotExist(err) { |
||||||
|
// no such file or directory, nothing to do, just return
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if err != nil { // now things get weird, better to return
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// First parse mdstat-file...
|
||||||
|
mdstate, err := parseMdstat(statusfile) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("error parsing %s: %s", statusfile, err) |
||||||
|
} |
||||||
|
|
||||||
|
// ... and then plug the result into the metrics to be exported.
|
||||||
|
var isActiveFloat float64 |
||||||
|
for _, mds := range mdstate { |
||||||
|
|
||||||
|
log.Debugf("collecting metrics for device %s", mds.mdName) |
||||||
|
|
||||||
|
if mds.isActive { |
||||||
|
isActiveFloat = 1 |
||||||
|
} else { |
||||||
|
isActiveFloat = 0 |
||||||
|
} |
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric( |
||||||
|
isActiveDesc, |
||||||
|
prometheus.GaugeValue, |
||||||
|
isActiveFloat, |
||||||
|
mds.mdName, |
||||||
|
) |
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric( |
||||||
|
disksActiveDesc, |
||||||
|
prometheus.GaugeValue, |
||||||
|
float64(mds.disksActive), |
||||||
|
mds.mdName, |
||||||
|
) |
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric( |
||||||
|
disksTotalDesc, |
||||||
|
prometheus.GaugeValue, |
||||||
|
float64(mds.disksTotal), |
||||||
|
mds.mdName, |
||||||
|
) |
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric( |
||||||
|
blocksTotalDesc, |
||||||
|
prometheus.GaugeValue, |
||||||
|
float64(mds.blocksTotal), |
||||||
|
mds.mdName, |
||||||
|
) |
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric( |
||||||
|
blocksSyncedDesc, |
||||||
|
prometheus.GaugeValue, |
||||||
|
float64(mds.blocksSynced), |
||||||
|
mds.mdName, |
||||||
|
) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
package collector |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
func TestMdadm(t *testing.T) { |
||||||
|
mdStates, err := parseMdstat("fixtures/mdstat") |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
t.Fatalf("parsing of reference-file failed entirely: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
refs := map[string]mdStatus{ |
||||||
|
"md3": mdStatus{"md3", true, 8, 8, 5853468288, 5853468288}, |
||||||
|
"md127": mdStatus{"md127", true, 2, 2, 312319552, 312319552}, |
||||||
|
"md0": mdStatus{"md0", true, 2, 2, 248896, 248896}, |
||||||
|
"md4": mdStatus{"md4", false, 2, 2, 4883648, 4883648}, |
||||||
|
"md6": mdStatus{"md6", true, 1, 2, 195310144, 16775552}, |
||||||
|
"md8": mdStatus{"md8", true, 2, 2, 195310144, 16775552}, |
||||||
|
"md7": mdStatus{"md7", true, 3, 4, 7813735424, 7813735424}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, md := range mdStates { |
||||||
|
if md != refs[md.mdName] { |
||||||
|
t.Errorf("failed parsing md-device %s correctly: want %v, got %v", md.mdName, refs[md.mdName], md) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(mdStates) != len(refs) { |
||||||
|
t.Errorf("expected number of parsed md-device to be %s, but was %s", len(refs), len(mdStates)) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue