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 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 ResultObject struct { State []string `json:"state"` Errors []string `json:"errors"` } type Exporter struct { URI string mutex sync.Mutex client *http.Client apiReachable float64 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"), "Is the Nexenta API reachable", nil, nil), scrapeFailures: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: namespace, Name: "exporter_scrape_failures_total", Help: "Number of errors while scraping the 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("%v", err) } ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, e.apiReachable) return } func (e *Exporter) collect(ch chan<- prometheus.Metric) error { volumes, err := e.getVolumes(ch) if err != nil { err = fmt.Errorf("getVolumes(): %v", err) } if len(volumes) > 0 { for _, volume := range volumes { err = e.getVolumeStatus(ch, volume) if err != nil { err = fmt.Errorf("getVolumeStatus(): %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 result = new(ResultObject) err = json.Unmarshal(apiResult, &result); if err != nil { return err } var volumeOnline float64 = 0 if len(result.State) > 0 { if result.State[0] == "ONLINE" { volumeOnline = 1 } } ch <- prometheus.MustNewConstMetric(e.volumeOnline, prometheus.GaugeValue, volumeOnline, volume) 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) } //log.Infof("response: %s", 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) } 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)) }