package main import ( "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" ) 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("host", "nexenta", "Nexenta API host.") apiPort = flag.String("port", "8457", "Nexenta API port.") apiUser = flag.String("user", "admin", "Nexenta API username.") apiPass = flag.String("password", "password", "Nexenta API password.") apiSsl = flag.Bool("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 ApiResponse struct { TGFlash string `json:"tg_flash"` Result interface{} `json:"result"` Error interface{} `json:"error"` } type ResultObject struct { State []string `json:"state"` Errors []string `json:"errors"` } type Exporter struct { URI string mutex sync.Mutex client *http.Client up *prometheus.Desc scrapeFailures prometheus.Counter volumeOnline *prometheus.Desc } func NewExporter(uri string) *Exporter { return &Exporter{ URI: uri, up: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "up"), "Could the Nexenta API be reached", nil, nil), scrapeFailures: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: namespace, Name: "exporter_scrape_failures_total", Help: "Number of errors while scraping Nexenta API.", }), volumeOnline: prometheus.NewDesc( prometheus.BuildFQName(namespace, "volume", "online"), "Status of volume.", []string{"volume"}, nil, ), 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.volumeOnline } 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("Collect(): %v", err) } return } func (e *Exporter) collect(ch chan<- prometheus.Metric) error { volumes, err := e.getVolumes(ch) if err != nil { log.Errorf("getVolumes(): %v", err) } if len(volumes) > 0 { for _, volume := range volumes { err := e.getVolumeStatus(ch, volume) if err != nil { log.Errorf("getVolumeStatus(): %v", err) } } } if err != nil { ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, 0) return fmt.Errorf("Error scraping Nexenta API: %v", err) } ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, 1) return err } func (e *Exporter) getVolumes(ch chan<- prometheus.Metric) ([]string, error) { var reqJson = []byte(`{"object": "volume", "params": [""], "method": "get_names"}`) apiResponse, err := e.queryApi(ch, reqJson); if err != nil { return nil, err } apiResult, err := json.Marshal(apiResponse.Result) var volumes []string err = json.Unmarshal([]byte(apiResult), &volumes) return volumes, err } func (e *Exporter) getVolumeStatus(ch chan<- prometheus.Metric, volume string) error { var reqJson = []byte(`{"object": "volume", "params": ["` + volume + `"], "method": "get_status"}`) apiResponse, err := e.queryApi(ch, reqJson); if err != nil { return err } apiResult, err := json.Marshal(apiResponse.Result) var result= new(ResultObject) err = json.Unmarshal(apiResult, &result) var volumeOnline float64 = 0 if len(result.State) > 0 { if result.State[0] == "ONLINE" { volumeOnline = 1 } } ch <- prometheus.MustNewConstMetric(e.volumeOnline, prometheus.GaugeValue, volumeOnline, "data") return err } func (e *Exporter) queryApi(ch chan<- prometheus.Metric, reqJson []byte) (*ApiResponse, error) { resp, err := e.client.Post(e.URI, "application/json", bytes.NewBuffer(reqJson)) data, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != 200 { if err != nil { data = []byte(err.Error()) log.Errorf("Error scraping Nexenta API: %s", err) e.scrapeFailures.Inc() e.scrapeFailures.Collect(ch) } return nil, fmt.Errorf("Status %s (%d)", resp.Status, resp.StatusCode) } //log.Infof("response: %s", data) var apiResponse = new(ApiResponse) err_json := json.Unmarshal(data, &apiResponse) if err_json != nil { log.Infof("whoops:", err_json) } if apiResponse.Result == nil { e.scrapeFailures.Inc() e.scrapeFailures.Collect(ch) return nil, fmt.Errorf("Field 'result' in API response is null") } return apiResponse, nil } func main() { flag.Parse() if *showVersion { fmt.Fprintln(os.Stdout, version.Print("nexenta_exporter")) os.Exit(0) } exporter := NewExporter("http://" + *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)) }