option for set minimum time period between run smartctl; smartctl info metric

This commit is contained in:
Горлов Максим 2019-08-16 00:01:16 +03:00
parent de13d91241
commit beb765eb1a
7 changed files with 139 additions and 42 deletions

13
main.go
View File

@ -24,15 +24,14 @@ func (i SMARTctlManagerCollector) Describe(ch chan<- *prometheus.Desc) {
// Collect is called by the Prometheus registry when collecting metrics. // Collect is called by the Prometheus registry when collecting metrics.
func (i SMARTctlManagerCollector) Collect(ch chan<- prometheus.Metric) { func (i SMARTctlManagerCollector) Collect(ch chan<- prometheus.Metric) {
info := NewSMARTctlInfo(ch)
for _, device := range options.SMARTctl.Devices { for _, device := range options.SMARTctl.Devices {
json, err := readData(device) json := readData(device)
if err != nil { info.SetJSON(json)
logger.Error("Failed parsing SMARTctl data: %s", err) smart := NewSMARTctl(json, ch)
} else { smart.Collect()
smart := NewSMARTctl(json, ch)
smart.Collect()
}
} }
info.Collect()
} }
func init() { func init() {

View File

@ -139,4 +139,15 @@ var (
}, },
nil, nil,
) )
metricDeviceExitStatus = prometheus.NewDesc(
"smartctl_device_smartctl_exit_status",
"Exit status of smartctl on device",
[]string{
"device",
"model_family",
"model_name",
"serial_number",
},
nil,
)
) )

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"time"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
@ -15,11 +16,13 @@ var (
// SMARTOptions is a inner representation of a options // SMARTOptions is a inner representation of a options
type SMARTOptions struct { type SMARTOptions struct {
BindTo string `yaml:"bind_to"` BindTo string `yaml:"bind_to"`
URLPath string `yaml:"url_path"` URLPath string `yaml:"url_path"`
FakeJSON bool `yaml:"fake_json"` FakeJSON bool `yaml:"fake_json"`
SMARTctlLocation string `yaml:"smartctl_location"` SMARTctlLocation string `yaml:"smartctl_location"`
Devices []string `yaml:"devices"` CollectPeriod string `yaml:"collect_not_more_than_period"`
CollectPeriodDuration time.Duration
Devices []string `yaml:"devices"`
} }
// Options is a representation of a options // Options is a representation of a options
@ -54,6 +57,7 @@ func loadOptions() Options {
URLPath: "/metrics", URLPath: "/metrics",
FakeJSON: false, FakeJSON: false,
SMARTctlLocation: "/usr/sbin/smartctl", SMARTctlLocation: "/usr/sbin/smartctl",
CollectPeriod: "60s",
Devices: []string{}, Devices: []string{},
}, },
} }
@ -61,6 +65,14 @@ func loadOptions() Options {
if yaml.Unmarshal(yamlFile, &opts) != nil { if yaml.Unmarshal(yamlFile, &opts) != nil {
logger.Panic("Failed parse %s: %s", configFile, err) logger.Panic("Failed parse %s: %s", configFile, err)
} }
d, err := time.ParseDuration(opts.SMARTctl.CollectPeriod)
if err != nil {
logger.Panic("Failed read collect_not_more_than_period (%s): %s", opts.SMARTctl.CollectPeriod, err)
}
opts.SMARTctl.CollectPeriodDuration = d
logger.Debug("Parsed options: %s", opts) logger.Debug("Parsed options: %s", opts)
return opts return opts
} }

View File

@ -1,25 +1,39 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os/exec" "os/exec"
"strings" "strings"
"time"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
// JSONCache caching json
type JSONCache struct {
JSON gjson.Result
LastCollect time.Time
}
var (
jsonCache map[string]JSONCache
)
func init() {
jsonCache = make(map[string]JSONCache)
}
// Parse json to gjson object // Parse json to gjson object
func parseJSON(data string) (gjson.Result, error) { func parseJSON(data string) gjson.Result {
if !gjson.Valid(data) { if !gjson.Valid(data) {
return gjson.Parse("{}"), errors.New("Invalid JSON") return gjson.Parse("{}")
} }
return gjson.Parse(data), nil return gjson.Parse(data)
} }
// Reading fake smartctl json // Reading fake smartctl json
func readFakeSMARTctl(device string) (gjson.Result, error) { func readFakeSMARTctl(device string) gjson.Result {
splitted := strings.Split(device, "/") splitted := strings.Split(device, "/")
filename := fmt.Sprintf("%s.json", splitted[len(splitted)-1]) filename := fmt.Sprintf("%s.json", splitted[len(splitted)-1])
logger.Verbose("Read fake S.M.A.R.T. data from json: %s", filename) logger.Verbose("Read fake S.M.A.R.T. data from json: %s", filename)
@ -32,19 +46,30 @@ func readFakeSMARTctl(device string) (gjson.Result, error) {
} }
// Get json from smartctl and parse it // Get json from smartctl and parse it
func readSMARTctl(device string) (gjson.Result, error) { func readSMARTctl(device string) gjson.Result {
logger.Debug("Collecting S.M.A.R.T. counters, device: %s...", device) logger.Debug("Collecting S.M.A.R.T. counters, device: %s", device)
out, err := exec.Command(options.SMARTctl.SMARTctlLocation, "--json", "--xall", device).Output() out, err := exec.Command(options.SMARTctl.SMARTctlLocation, "--json", "--xall", device).Output()
if err != nil { if err != nil {
logger.Error("S.M.A.R.T. output reading error: %s", err) logger.Warning("S.M.A.R.T. output reading error: %s", err)
} }
return parseJSON(string(out)) return parseJSON(string(out))
} }
// Select json source and parse // Select json source and parse
func readData(device string) (gjson.Result, error) { func readData(device string) gjson.Result {
if options.SMARTctl.FakeJSON { if options.SMARTctl.FakeJSON {
return readFakeSMARTctl(device) return readFakeSMARTctl(device)
} }
return readSMARTctl(device)
if value, ok := jsonCache[device]; ok {
// logger.Debug("Cache exists")
if time.Now().After(value.LastCollect.Add(options.SMARTctl.CollectPeriodDuration)) {
// logger.Debug("Cache update")
jsonCache[device] = JSONCache{JSON: readSMARTctl(device), LastCollect: time.Now()}
}
} else {
// logger.Debug("Cache not exists")
jsonCache[device] = JSONCache{JSON: readSMARTctl(device), LastCollect: time.Now()}
}
return jsonCache[device].JSON
} }

View File

@ -39,8 +39,8 @@ func NewSMARTctl(json gjson.Result, ch chan<- prometheus.Metric) SMARTctl {
} }
// Collect metrics // Collect metrics
func (smart SMARTctl) Collect() { func (smart *SMARTctl) Collect() {
// smart.mineVersion() smart.mineExitStatus()
smart.mineDevice() smart.mineDevice()
smart.mineCapacity() smart.mineCapacity()
smart.mineInterfaceSpeed() smart.mineInterfaceSpeed()
@ -48,24 +48,22 @@ func (smart SMARTctl) Collect() {
smart.minePowerOnSeconds() smart.minePowerOnSeconds()
smart.mineRotationRate() smart.mineRotationRate()
smart.mineTemperatures() smart.mineTemperatures()
smart.minePowerCycleCount()
} }
func (smart SMARTctl) mineVersion() { func (smart *SMARTctl) mineExitStatus() {
jsonVersion := smart.json.Get("json_format_version").Array()
smartctlJSON := smart.json.Get("smartctl")
smartctlVersion := smartctlJSON.Get("version").Array()
smart.ch <- prometheus.MustNewConstMetric( smart.ch <- prometheus.MustNewConstMetric(
metricSmartctlVersion, metricDeviceExitStatus,
prometheus.GaugeValue, prometheus.GaugeValue,
1, smart.json.Get("smartctl.exit_status").Float(),
fmt.Sprintf("%d.%d", jsonVersion[0].Int(), jsonVersion[1].Int()), smart.device.device,
fmt.Sprintf("%d.%d", smartctlVersion[0].Int(), smartctlVersion[1].Int()), smart.device.family,
smartctlJSON.Get("svn_revision").String(), smart.device.model,
smartctlJSON.Get("build_info").String(), smart.device.serial,
) )
} }
func (smart SMARTctl) mineDevice() { func (smart *SMARTctl) mineDevice() {
device := smart.json.Get("device") device := smart.json.Get("device")
smart.ch <- prometheus.MustNewConstMetric( smart.ch <- prometheus.MustNewConstMetric(
metricDeviceModel, metricDeviceModel,
@ -84,7 +82,7 @@ func (smart SMARTctl) mineDevice() {
) )
} }
func (smart SMARTctl) mineCapacity() { func (smart *SMARTctl) mineCapacity() {
capacity := smart.json.Get("user_capacity") capacity := smart.json.Get("user_capacity")
smart.ch <- prometheus.MustNewConstMetric( smart.ch <- prometheus.MustNewConstMetric(
metricDeviceCapacityBlocks, metricDeviceCapacityBlocks,
@ -118,7 +116,7 @@ func (smart SMARTctl) mineCapacity() {
} }
} }
func (smart SMARTctl) mineInterfaceSpeed() { func (smart *SMARTctl) mineInterfaceSpeed() {
iSpeed := smart.json.Get("interface_speed") iSpeed := smart.json.Get("interface_speed")
for _, speedType := range []string{"max", "current"} { for _, speedType := range []string{"max", "current"} {
tSpeed := iSpeed.Get(speedType) tSpeed := iSpeed.Get(speedType)
@ -135,7 +133,7 @@ func (smart SMARTctl) mineInterfaceSpeed() {
} }
} }
func (smart SMARTctl) mineDeviceAttribute() { func (smart *SMARTctl) mineDeviceAttribute() {
for _, attribute := range smart.json.Get("ata_smart_attributes.table").Array() { for _, attribute := range smart.json.Get("ata_smart_attributes.table").Array() {
name := strings.TrimSpace(attribute.Get("name").String()) name := strings.TrimSpace(attribute.Get("name").String())
flags := strings.TrimSpace(attribute.Get("flags.string").String()) flags := strings.TrimSpace(attribute.Get("flags.string").String())
@ -163,7 +161,7 @@ func (smart SMARTctl) mineDeviceAttribute() {
} }
} }
func (smart SMARTctl) minePowerOnSeconds() { func (smart *SMARTctl) minePowerOnSeconds() {
pot := smart.json.Get("power_on_time") pot := smart.json.Get("power_on_time")
smart.ch <- prometheus.MustNewConstMetric( smart.ch <- prometheus.MustNewConstMetric(
metricDevicePowerOnSeconds, metricDevicePowerOnSeconds,
@ -176,7 +174,7 @@ func (smart SMARTctl) minePowerOnSeconds() {
) )
} }
func (smart SMARTctl) mineRotationRate() { func (smart *SMARTctl) mineRotationRate() {
rRate := GetFloatIfExists(smart.json, "rotation_rate", 0) rRate := GetFloatIfExists(smart.json, "rotation_rate", 0)
if rRate > 0 { if rRate > 0 {
smart.ch <- prometheus.MustNewConstMetric( smart.ch <- prometheus.MustNewConstMetric(
@ -191,7 +189,7 @@ func (smart SMARTctl) mineRotationRate() {
} }
} }
func (smart SMARTctl) mineTemperatures() { func (smart *SMARTctl) mineTemperatures() {
temperatures := smart.json.Get("temperature") temperatures := smart.json.Get("temperature")
if temperatures.Exists() { if temperatures.Exists() {
temperatures.ForEach(func(key, value gjson.Result) bool { temperatures.ForEach(func(key, value gjson.Result) bool {
@ -210,7 +208,7 @@ func (smart SMARTctl) mineTemperatures() {
} }
} }
func (smart SMARTctl) minePowerCycleCount() { func (smart *SMARTctl) minePowerCycleCount() {
smart.ch <- prometheus.MustNewConstMetric( smart.ch <- prometheus.MustNewConstMetric(
metricDevicePowerCycleCount, metricDevicePowerCycleCount,
prometheus.CounterValue, prometheus.CounterValue,

View File

@ -3,6 +3,7 @@ smartctl_exporter:
url_path: "/metrics" url_path: "/metrics"
fake_json: no fake_json: no
smartctl_location: /usr/sbin/smartctl smartctl_location: /usr/sbin/smartctl
collect_not_more_than_period: 20s
devices: devices:
- /dev/sda - /dev/sda
- /dev/sdb - /dev/sdb

51
smartctlinfo.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/tidwall/gjson"
)
// SMARTctlInfo object
type SMARTctlInfo struct {
ch chan<- prometheus.Metric
json gjson.Result
Ready bool
}
// NewSMARTctlInfo is smartctl constructor
func NewSMARTctlInfo(ch chan<- prometheus.Metric) SMARTctlInfo {
smart := SMARTctlInfo{}
smart.ch = ch
smart.Ready = false
return smart
}
// SetJSON metrics
func (smart *SMARTctlInfo) SetJSON(json gjson.Result) {
if !smart.Ready {
smart.json = json
smart.Ready = true
}
}
// Collect metrics
func (smart *SMARTctlInfo) Collect() {
smart.mineVersion()
}
func (smart *SMARTctlInfo) mineVersion() {
smartctlJSON := smart.json.Get("smartctl")
smartctlVersion := smartctlJSON.Get("version").Array()
jsonVersion := smart.json.Get("json_format_version").Array()
smart.ch <- prometheus.MustNewConstMetric(
metricSmartctlVersion,
prometheus.GaugeValue,
1,
fmt.Sprintf("%d.%d", jsonVersion[0].Int(), jsonVersion[1].Int()),
fmt.Sprintf("%d.%d", smartctlVersion[0].Int(), smartctlVersion[1].Int()),
smartctlJSON.Get("svn_revision").String(),
smartctlJSON.Get("build_info").String(),
)
}