Select Git revision
main.go 11.98 KiB
package main
import (
"github.com/namsral/flag"
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
"fmt"
"os"
"github.com/prometheus/common/log"
"github.com/prometheus/common/version"
"github.com/prometheus/client_golang/prometheus"
"sync"
"crypto/tls"
"io/ioutil"
"encoding/json"
"bytes"
"strconv"
"strings"
)
const (
namespace = "nexenta" // For Prometheus metrics.
apiPath = "/rest/nms"
)
var (
listenAddr = flag.String("listen-address", ":9457", "The address to listen on for HTTP requests.")
metricsEndpoint = flag.String("metrics-endpoint", "/metrics", "Path under which to expose metrics.")
apiHost = flag.String("api-host", "nexenta", "Nexenta API host.")
apiPort = flag.String("api-port", "8457", "Nexenta API port.")
apiUser = flag.String("api-user", "admin", "Nexenta API username.")
apiPass = flag.String("api-password", "password", "Nexenta API password.")
apiSsl = flag.Bool("api-ssl", false, "Use SSL for the Nexenta API.")
insecure = flag.Bool("insecure", false, "Ignore server certificate if using https.")
showVersion = flag.Bool("version", false, "Print version information.")
)
type ApiRequest struct {
Object string `json:"object"`
Method string `json:"method"`
Params []string `json:"params"`
}
type ApiResponse struct {
TGFlash string `json:"tg_flash"`
Result interface{} `json:"result"`
Error ApiError `json:"error"`
}
type ApiError struct {
Message string `json:"message"`
}
type VolumeStatus struct {
State []string `json:"state"`
Errors []string `json:"errors"`
}
type Sensor struct {
Name string `json:"name"`
Value string `json:"value"`
State string `json:"state"`
Dev string `json:"dev"`
Units string `json:"units"`
Type string `json:"type"`
}
type LicenseInfo struct {
DaysLeft string `json:"license_days_left"`
}
type Exporter struct {
URI string
mutex sync.Mutex
client *http.Client
apiReachable float64
up *prometheus.Desc
scrapeFailures prometheus.Counter
jbodStatus *prometheus.Desc
jbodTemp *prometheus.Desc
jbodVoltage *prometheus.Desc
jbodSlotStatus *prometheus.Desc
licenseDaysLeft *prometheus.Desc
volumeStatus *prometheus.Desc
volumeLunStatus *prometheus.Desc
}
func NewExporter(uri string) *Exporter {
return &Exporter{
URI: uri,
up: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "up"),
"Is the Nexenta API reachable",
nil,
prometheus.Labels{"host":*apiHost},
),
scrapeFailures: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Name: "exporter_scrape_failures_total",
Help: "Number of errors while scraping the Nexenta API",
ConstLabels: prometheus.Labels{"host":*apiHost},
}),
jbodStatus: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "jbod", "status"),
"Status of JBOD.",
[]string{"jbod"},
prometheus.Labels{"host":*apiHost},
),
jbodTemp: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "jbod", "temp"),
"Temperature of JBOD.",
[]string{"jbod", "name", "state"},
prometheus.Labels{"host":*apiHost},
),
jbodVoltage: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "jbod", "voltage"),
"Voltage of JBOD.",
[]string{"jbod", "name", "state"},
prometheus.Labels{"host":*apiHost},
),
jbodSlotStatus: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "jbod", "slot_status"),
"Status of JBOD slot.",
[]string{"jbod", "slot", "state"},
prometheus.Labels{"host":*apiHost},
),
licenseDaysLeft: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "license", "days_left"),
"License days left.",
nil,
prometheus.Labels{"host":*apiHost},
),
volumeStatus: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "volume", "status"),
"Status of volume.",
[]string{"volume", "state", "errors"},
prometheus.Labels{"host":*apiHost},
),
volumeLunStatus: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "volume", "lun_status"),
"Status of volume LUN.",
[]string{"volume", "lun", "state", "errors", "group"},
prometheus.Labels{"host":*apiHost},
),
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: *insecure},
},
},
}
}
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- e.up
e.scrapeFailures.Describe(ch)
ch <- e.jbodStatus
ch <- e.jbodTemp
ch <- e.jbodVoltage
ch <- e.jbodSlotStatus
ch <- e.volumeStatus
ch <- e.volumeLunStatus
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
e.mutex.Lock() // To protect metrics from concurrent collects.
defer e.mutex.Unlock()
err := e.collect(ch)
if err != nil {
log.Errorf("%v", err)
}
ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, e.apiReachable)
return
}
func (e *Exporter) collect(ch chan<- prometheus.Metric) error {
// Volumes
volumes, err := e.getVolumes(ch)
if err != nil {
return fmt.Errorf("getVolumes(): %v", err)
}
if len(volumes) > 0 {
for _, volume := range volumes {
err = e.getVolumeStatus(ch, volume)
if err != nil {
return fmt.Errorf("getVolumeStatus(): %v", err)
}
err = e.getVolumeLuns(ch, volume)
if err != nil {
return fmt.Errorf("getVolumeLuns(): %v", err)
}
}
}
// JBODs
jbods, err := e.getJBODs(ch)
if err != nil {
return fmt.Errorf("getJBODs(): %v", err)
}
if len(jbods) > 0 {
for _, jbod := range jbods {
err = e.getJBODSensors(ch, jbod)
if err != nil {
return fmt.Errorf("getJBODSensors(): %v", err)
}
}
}
// License
err = e.getLicenseInfo(ch)
if err != nil {
return fmt.Errorf("getLicenseInfo(): %v", err)
}
return err
}
func (e *Exporter) getVolumes(ch chan<- prometheus.Metric) ([]string, error) {
apiResponse, err := e.queryApi(ch, "volume", "get_names", []string{""}); if err != nil { return nil, err }
apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return nil, err }
var volumes []string
err = json.Unmarshal([]byte(apiResult), &volumes)
return volumes, err
}
func (e *Exporter) getVolumeStatus(ch chan<- prometheus.Metric, volume string) error {
apiResponse, err := e.queryApi(ch, "volume", "get_status", []string{volume}); if err != nil { return err }
apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err }
var volumeStatus = new(VolumeStatus)
err = json.Unmarshal(apiResult, &volumeStatus); if err != nil { return err }
var volumeOnline float64 = 0
if len(volumeStatus.State) > 0 {
if volumeStatus.State[0] == "ONLINE" { volumeOnline = 1 }
}
ch <- prometheus.MustNewConstMetric(e.volumeStatus, prometheus.GaugeValue, volumeOnline, volume, volumeStatus.State[0], volumeStatus.Errors[0])
return err
}
func (e *Exporter) getVolumeLuns(ch chan<- prometheus.Metric, volume string) error {
apiResponse, err := e.queryApi(ch, "volume", "get_luns", []string{volume}); if err != nil { return err }
apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err }
var volumeLuns map[string][]string
err = json.Unmarshal(apiResult, &volumeLuns); if err != nil { return err }
for lun, data := range volumeLuns {
// data: 0=state, 1=err_read, 2=err_write, 3=err_chksum, 4=?, 5=group, 6=?
var volumeLunStatus float64 = 0
var volumeLunErrors float64 = 0
errorsRead, _ := strconv.ParseFloat(data[1], 64)
errorsWrite, _ := strconv.ParseFloat(data[2], 64)
errorsChksum, _ := strconv.ParseFloat(data[3], 64)
volumeLunErrors = errorsRead + errorsWrite + errorsChksum
labelErrors := strconv.FormatFloat(volumeLunErrors, 'f', 0, 64)
if len(data) > 0 {
if (data[5] != "spares" && data[0] == "ONLINE") ||
(data[5] == "spares" && data[0] == "AVAIL") {
volumeLunStatus = 1
}
}
ch <- prometheus.MustNewConstMetric(e.volumeLunStatus, prometheus.GaugeValue, volumeLunStatus,
volume, lun, data[0], labelErrors, data[5])
}
return err
}
func (e *Exporter) getJBODs(ch chan<- prometheus.Metric) ([]string, error) {
apiResponse, err := e.queryApi(ch, "jbod", "get_names", []string{""}); if err != nil { return nil, err }
apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return nil, err }
var jbods []string
err = json.Unmarshal([]byte(apiResult), &jbods)
return jbods, err
}
func (e *Exporter) getJBODSensors(ch chan<- prometheus.Metric, jbod string) error {
apiResponse, err := e.queryApi(ch, "jbod", "get_sensors", []string{jbod}); if err != nil { return err }
apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err }
var sensors []Sensor
err = json.Unmarshal([]byte(apiResult), &sensors)
for _, sensor := range sensors {
var status float64 = 0
if sensor.Dev == "jbod" {
switch sensor.Type {
case "state":
if sensor.State == "ok" { status = 1 }
ch <- prometheus.MustNewConstMetric(e.jbodStatus, prometheus.GaugeValue, status, jbod)
break
case "temperature":
value, _ := strconv.ParseFloat(sensor.Value, 64)
ch <- prometheus.MustNewConstMetric(e.jbodTemp, prometheus.GaugeValue, value, jbod, sensor.Name, sensor.State)
break
case "voltage":
value, _ := strconv.ParseFloat(sensor.Value, 64)
ch <- prometheus.MustNewConstMetric(e.jbodVoltage, prometheus.GaugeValue, value, jbod, sensor.Name, sensor.State)
break
}
} else {
slot := strings.Split(sensor.Dev, ":")
if sensor.State == "ok" { status = 1 }
ch <- prometheus.MustNewConstMetric(e.jbodSlotStatus, prometheus.GaugeValue, status, jbod, slot[1], sensor.State)
}
}
return err
}
func (e *Exporter) getLicenseInfo(ch chan<- prometheus.Metric) error {
apiResponse, err := e.queryApi(ch, "appliance", "get_license_info", []string{}); if err != nil { return err }
apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err }
var licenseInfo LicenseInfo
err = json.Unmarshal([]byte(apiResult), &licenseInfo)
daysLeft, _ := strconv.ParseFloat(licenseInfo.DaysLeft, 64)
ch <- prometheus.MustNewConstMetric(e.licenseDaysLeft, prometheus.GaugeValue, daysLeft)
return err
}
func (e *Exporter) queryApi(ch chan<- prometheus.Metric, object string, method string, params []string) (*ApiResponse, error) {
reqObject := &ApiRequest{
Object: object,
Method: method,
Params: params,
}
e.apiReachable = 0
reqJson, _ := json.Marshal(reqObject)
resp, err := e.client.Post(e.URI, "application/json", bytes.NewBuffer(reqJson))
if err != nil {
return nil, fmt.Errorf("Error scraping Nexenta API: %v", err)
}
e.apiReachable = 1
data, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != 200 {
if err != nil {
data = []byte(err.Error())
}
e.scrapeFailures.Inc()
e.scrapeFailures.Collect(ch)
return nil, fmt.Errorf("Request Error: Status %s %s", resp.Status, data)
}
var apiResponse = new(ApiResponse)
err = json.Unmarshal(data, &apiResponse); if err != nil { return nil, err }
if apiResponse.Result == nil {
e.scrapeFailures.Inc()
e.scrapeFailures.Collect(ch)
return nil, fmt.Errorf("API Error: %v", apiResponse.Error.Message)
}
return apiResponse, nil
}
func main() {
flag.Parse()
if *showVersion {
fmt.Fprintln(os.Stdout, version.Print("nexenta_exporter"))
os.Exit(0)
}
apiProtocol := "http"
if *apiSsl { apiProtocol = "https" }
exporter := NewExporter(apiProtocol + "://" + *apiUser + ":" + *apiPass + "@" + *apiHost + ":" + *apiPort + apiPath)
prometheus.MustRegister(exporter)
prometheus.MustRegister(version.NewCollector("nexenta_exporter"))
// disable Go metrics
prometheus.Unregister(prometheus.NewProcessCollector(os.Getpid(), ""))
prometheus.Unregister(prometheus.NewGoCollector())
log.Infoln("Starting nexenta_exporter", version.Info())
log.Infoln("Build context", version.BuildContext())
log.Infof("Starting Server: %s", *listenAddr)
http.Handle(*metricsEndpoint, promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<html>
<head><title>Nexenta Exporter</title></head>
<body>
<h1>Nexenta Exporter</h1>
<p><a href='` + *metricsEndpoint + `'>Metrics</a></p>
</body>
</html>`))
})
log.Fatal(http.ListenAndServe(*listenAddr, nil))
}